pre up next title end end

11. Rekurze, matice a různé

  1. Rekurze
    1. Povšechný úvod
    2. Faktoriál, fibonacci
    3. Memoizace
    4. Vnořené seznamy
  2. Matice
    1. Matice jako seznam
    2. Matice jako slovník
  3. Chyby a výjimky
    1. Vlastní výjimky
    2. Ošetření výjimek
  4. Kopírování objektů
    1. Zdánlivá kopie
    2. Mělká kopie
    3. Důkladná kopie
  5. Modul pydoc
  6. Glosář
  7. Cvičení

11.1 Rekurze

11.1.1   Povšechný úvod

Rekurze je technika, při které funkce opakovaně volá samu sebe (přímá rekurze) - případně se mohou dvě funkce volat navzájem (nepřímá rekurze). Přitom je nutné, aby tato opakovaná volání měla konečný počet - v tom případě se jedná o konečnou rekurzi.

Správně definovaná konečná rekurze vyhovuje těmto dvěma požadavkům:

Pokud by rekurzivní volání nenarazilo na základní případ, šlo by o nekonečnou rekurzi s neúměrným nárokem na paměť počítače.Tato situace je ošetřena tak, že se Python zastaví po dosažení nastavené hloubky rekurze (implicitně ~ 1000 at 3000 cyklů - v závislosti na typu paměti RAM počítače) a vrací chybu při běhu programu.

Příbuznou procedurou jako rekurze je iterace (viz Kap. 3.1), která je méně náročná na paměť a je rychlejší než rekurze.

Iterativní funkci countdown(n) z kapitoly 5.4

def countdown(n):       
    while n > 0:                    # omezující podmínka
        print(n, end=" ")
        n = n-1                     # změna stavu počítadla
		
# >>> countdown(4)  -->  4 3 2 1		
můžeme s pomocí rekurze přepsat takto:

def cntdwn(n):       
    if n > 0:                       # zákl. případ
        print(n, end=" ")          
        cntdwn(n-1)                   # koncová (tail) rekurze
		
# >>> cntdwn(4)  --> 4 3 2 1		

Průběh výpočtu funkce cntdwn(4) si můžeme demonstrovat v tabulce:

     ni   ni - 1   ni > 0   print(ni)     Poznámka
     4          3      T            4     
     3          2      T            3     
     2          1      T            2     
     1          0      F            1      cntdwn (4)   -->   4  3  2  1

Stojí za pozornost, že varianta funkce "cntdwn(n)" s jinak umístěnými řádky vrátí jiný výsledek:

def codo(n):                   
    if n > 0:                       # základní případ
        codo(n-1)                   # čelní (head) rekurze
        print(n, end=" ")
# >>>codo(4)   --> 1 2 3 4 		

A ještě jinak se stejnými řádky:

def ctdn(n):
    print(n, end=" ")
    if n > 0:             
        ctdn(n - 1)                 # koncová (tail) rekurze
 # >>> ctdn(4)   --> 4 3 2 1 0		

Časté je použití příkazu return. Zde je přehlednější obdoba (s jinou podmínkou) funkce ctdn:

def cntd(n):          
    print(n, end=" ")               
    if n == 0:                      # základní případ
        return                      # konec rekurze
    else:
        cntd(n - 1)                 
# cntd(4) --> 4 3 2 1 0

Není snadné se v tom vyznat. Ve čtyřech uvedených příkladech velmi podobné rekurzivní funkce jsme viděli dva různé výstupy. Proč tomu tak je, souvisí se zákulisní aktivitou interpreta a paměti typu stack (zásobník) s procedurou FIFO (first in, first out).

11.1.2   Faktoriál a fibonacci

Faktoriál

V matematice známý pojem faktoriál čísla n (n!) je označení pro součin souvislé číselné řady 1 až n:   n! = n * (n-1) * (n-2) * ... *1
Ukážeme si jeho různé vyjádření:

Matematická definice faktoriálu:

0! = 1                             # faktoriál nuly = 1
n! = n(n-1)

# například: 25! = 15511210043330985984000000

Vyjádření pomocí iterace while...

def fact_w(n):
    f, i = 1, 0                    # hromadné přiřazení
    while i < n :
       i += 1; f = f*i             # rozšířené přiřazení
    return f

Vyjádření pomocí iterace for...

def fact_f(n):
    f = 1
    for i in range(1, n+1):   # range končí před koncovou mezí
	f = f*i
    return f

Vyjádření pomocí rekurze

def fact_r(n):
    if n < 2:                 # základní případ
        return 1
    return n * fact_r(n-1)    # rekurzivní volání funkce 

Dtto jinak:

def fact_rec(n):
    return 1 if n == 1 else n * fact_rec(n - 1)

Fibonacciho sekvence

Fibonacciho sekvence je řada čísel 0 ÷ n, v níž každé číslo je součet dvou čísel předcházejících:

F0, F1 = 0, 1;  Fn = Fn-1 + Fn-2  pro n > 1

Iterativní výpočet může mít tento tvar:

def fibo_seq(n):
    n1, n2 = 0, 1
    next_num = n2
    count = 1
    while count <= n:
        print(next_num, end=" ")
        count += 1
        n1, n2 = n2, next_num
        next_num = n1 + n2
>>> fibo_seq(10)
 1 2 3 5 8 13 21 34 55 89 

Fibonacciho číslo

Fibonacciho číslo je poslední hodnota Fibonacciho sekvence.

Vyjádření pomocí iterace for i in ...

def fibo_iter (n):
    old, new = 0, 1
    if n == 0:
        return 0 
    for i in range(n-1):
	    old, new = new, old + new
    return  old + new
>>>fibo_iter(10)
 89 

Opakovaný výpočet pomocí rekurze bývá pomalejší než výpočet pomocí iterace, protože zabírá více místa v paměti.

def fibo_rec(n):
    if n == 0 or n == 1:
        return 1
    else:
        return fibo_rec(n-1) + fibo_rec(n-2)
>>>fibo_rec(10)
 89 

11.1.3   Memoizace

Když jsme si v předchozím odstavci pohrávali s výpočtem Fibonacciho čísla, mohli jsme si všimnout, že čím větší argument jsme použili, tím déle trvalo provádění výpočtu. Se zvětšujícím se argumentem časové nároky velmi rychle rostly.

Abychom pochopili proč, pohleďme na toto schéma volání funkce fibonacci(4):

Schéma zobrazuje sadu funkcí v rámečcích se šipkami, ukazujícími na volané funkce. Nejvýše umístěná fce, fibonacci(4) volá fibonacci(3) a fibonacci(2). Fce fibonacci(3) zas volá fibonacci(2) a fibonacci(1). A tak dále.

Spočítejme, kolikrát je voláno například fibonacci(0). Opakovaný výpočet hodnoty funkce pro tentýž argument je řešení neefektivní a rychle se zhoršuje při zvětšující se hodnotě argumentu.

Dobrým řešením, zvaným memoizace (memoization), je ukládat si záznamy o spočítaných hodnotách do slovníku pro potřebu dalšího použití. Takovýmto záznamům říkejme třeba memo. Zde vidíme realizaci funkce fibonacci s podporou náznaků:

def fibo_mem(n):
    memo = {0:0, 1:1}            # slovník (dictionary)          
    if n not in memo:
        memo[n] = fibo_mem(n-1) + fibo_mem(n-2)
    return memo[n]

Slovník memo zaznamenává fibonacciho čísla, která již známe. Začínáme pouhými dvěma páry: 0 ukazuje na 0, 1 na 1.

Kdykoli je funkce fibo_mem volána, prozkoumá svůj slovník, zda neobsahuje výsledek. Jestliže ano, vrací výsledek bez dalších okolků. Jestliže ne, počítá novou hodnotu. Tato hodnota je přidána do slovníku před tím, než se funkce vrátí k rekurzivnímu volání.

S použitím této verze náš počítač spočítá fibo_mem(30) v jednom oka mžiku, fibo_mem(40) už trvá asi minutu.

>>> fibo_mem(30)
832040

Výrazné zlepšení výpočtu Fibbonacciho čísla pomocí memoizace představuje výpočet s podporou dekorátorů (kap.10.8)

def memoize(fun):                # dekorátorová funkce
    memo = {}
    def wrapper(x):
        if x not in memo:            
            memo[x] = fun(x)
        return memo[x]
    return wrapper
    
@memoize                         # dekorace funkcí
def fibomem(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibomem(n-1) + fibomem(n-2)

# >>> fibomem(100) ==> 354224848179261915075
class Memoize:                    # dekorátorová třída
    def __init__(self, fn):
        self.fn = fn
        self.memo = {}
    def __call__(self, *args):
        if args not in self.memo:
            self.memo[args] = self.fn(*args)
        return self.memo[args]

@Memoize                         # dekorace třídou
def fibomem(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibomem(n-1) + fibomem(n-2)

# >>> fibomem(100) ==> 354224848179261915075

Všechny tři způsoby používají slovník memo a podmínku if x not in memo.


11.1.4   Vnořené seznamy

K vytvoření součtu všech čísel v seznamu s vnořeným seznamem potřebujeme traverzovat seznamem, navštívit každou položku uvnitř vnořené struktury a připočítávat číselné hodnoty k našemu součtu.

Potřebný program pro vytvoření součtu čísel v seznamu s vnořeným seznamem je díky rekurzi překvapivě krátký:

def recursive_sum(nested_num_list):      (vnořený seznam)
    sum = 0
    for element in nested_num_list:
        if(element) == type([]):  # je-li položkou seznam
            sum = sum + recursive_sum(element)
        else:
            sum = sum + element
    return sum

Tělo fce se skládá hlavně ze smyčky for, která traverzuje seznamem nested_num_list. Je-li položkou seznamu numerická hodota nikoliv seznam (větev else), přičte se číslo jednoduše k proměnné sum. Je-li položkou seznam, potom je opět volána fce recursive_sum pro argument, jímž je vnořený seznam. Příkaz uvnitř těla funkce, jímž je volána táž funkce, se nazývá rekurzivní volání.

Poněkud komplikovanější úlohou je nalezení největší hodnoty v seznamu s vnořeným seznamem. Následující definice funkce obsahuje doctest (viz kap. 3.10), kterým si jednak ilustrujeme řešený problém a zároveň ověříme správnost naší funkce:

def recursive_max(nested_num_list):
    """
    >>> recursive_max([2, 9, [1, 13], 8, 6])
    13
    >>> recursive_max([2, [[100, 7], 90], [1, 13], 8, 6])
    100
    >>> recursive_max([2, [[13, 7], 90], [1, 100], 8, 6])
    100
    >>> recursive_max([[[13, 7], 90], 2, [1, 100], 8, 6])
    100
    """
    
    largest = nested_num_list[0]
    while type(largest) == type([]):
        largest = largest[0]

    for element in nested_num_list:
        if(element) == type([]):
            max_of_elem = recursive_max(element)
            if largest < max_of_elem:
                largest = max_of_elem
        else:                 # element is not a list
            if largest < element:
                largest = element

    return largest

Čertovým kopýtkem tohoto problému je určení numerické hodnoty pro inicializaci proměnné largest. Nemůžeme jednoduše zadat, že je to nested_num_list[0], protože by touto položkou mohlo být jak číslo, tak seznam. Problém je řešen použitím smyčky while, která přiřadí proměnné largest první číselnou hodnotu bez ohledu na hloubku jejího vnoření.

Mnohé dobře známé matematické funkce jsou definovány rekurzivně. Například Fibonacciho číslo, nebo fibonacciho sekvence, definovaná:

fibonacci(0) = 1
fibonacci(1) = 1
fibonacci(n) = fibonacci(n-1) + fibonacci(n-2)

To lze také snadno přepsat do Pythonu:

def fibonacci (n):
    if n == 0 or n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

Koncová rekurze je v Pythonu považována za špatnou techniku, protože používá více systémových zdrojů než ekvivalentní iterativní řešení. Volání fibonacci(500) uvede interpreta Pythonu do rozpaků.

Mnohem rychlejší než rekurzivní verze je verze iterační, která by mohla vypadat takto:

def fiboiter (n):
    old, new = 0, 1
    if n == 0:
        return 0 
    for i in range(n-1):
	old, new = new, old + new
    return  old + new

Vyzkoušejte volání fiboiter(500) a budete příjemně překvapeni.
Při cvičení si zkusíte napsat iterační verzi funkce factorial a v dalším odstavci si ukážeme memoizovanou alternativu funkce fibonacci, která je ze všech tří verzí nejrychlejší.


11.2 Matice

Práce s maticemi vyžaduje specifické znalosti maticového počtu. Na rozdíl od algebry v něm například obecně neplatí komutativní zákon: A*B != B*A; platí ale C = A+B == B+A, kde ci,j = ai,j +bi,j.

Násobení matice A o velikosti m (řádků) * n (sloupců) maticí B o velikosti n (řádků) * p (sloupců) je možné v případě, že počet sloupců (n) matice A souhlasí s počtem řádků (n) matice B. Rozměr výsledné matice je m * p.

Pokud je tato podmínka splněna, lze prvek ci,j matice C = A*B určit jako algebraický součet součinů prvků z i-tého řádku matice A s prvky z i-tého sloupce matice B, jak je naznačeno v tomto schematu:

Rozměr výsledné matice c = a*b je tedy dán počtem řádků matice a ( m rows = len(a)) a počtem sloupců matice b (p cols = len(b[0])).

    a          b                                      c 
| 1 2 3 | * | 7 8 |   1*7+2*9+3*3 | 1*8+2*2+3*1   | 34 15 |
| 4 5 6 |   | 9 2 | = ------------|------------ =  
            | 3 1 |   4*7+5*9+6*3 | 4*8+5*2+6*1   | 91 48 |
a = [[1, 2, 3], [4, 5, 6]]      # 2 řádky, 3 sloupce: a(2,3)
b = [[7, 8], [9, 2], [3,1]]     # 3 řádky, 2 sloupce: b(3,2)
c = [[34, 15], [91], [48]]      # 2 řádky, 2 sloupce: c(2,2)

# c(1,1)= 1*7 + 2*9 + 3*3 = 34   
# c(2,2)= 4*8 + 5*2 + 6*1 = 48

11.2.1 Matice jako seznam

K vyjádření matic se často používají vnořené seznamy. Například, matice o rozměru 3x3 (3 řádky x 3 sloupce):

může být vyjádřena jako seznam s vnořenými seznamy stejné délky. Jednotlivé vnořené seznamy prezentují příslušné řádky matice:

>>> matrix = [[1, 2, 3], 
...           [4, 5, 6], 
...           [7, 8, 9]]

Tentýž zápis v úspornějším provedení:
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

V maticovém počtu lze tyto vnořené seznamy označit jako řádkové matice, neboli vektory.

Řádek matice vybereme jako prvek vnějšího seznamu (indexy zde počínají nulou):

>>> matrix[1]
[4, 5, 6]

Položku určitého řádku označíme indexem řádku a indexem položky:

>>> matrix[1][1]
5

Výpočet součinu matic lze provést trojím použitím idiomu for i in range(len(...)):

def matrix_mult (m1, m2):
    result = [[0 for row in range(len(m1))]     # maketa matice
                 for col in range(len(m2[0]))]
    for i in range(len(m1)):                    # rows in m1        
        for j in range(len(m2[0])):             # cols in m2  
            for k in range(len(m2)):            # rows in m2
               
                result[i][j] += m1[i][k] * m2[k][j] 
    return result
========= RESTART: F:\Codetest\HowTo\matice\Test.py ========
>>> a = [[1,2,3], [4,5,6]]
>>> b = [[7,8], [9,1], [2,3]]
>>> matrix_mult(a,b)
[[31, 19], [85, 55]]

Sevřenější tvar má toto řešení s komprehencí seznamu a s vestavěnými funkcemi sum a zip:

def matrix_mult (m1, m2):
    return [[sum(x * y
             for x, y in zip(m1_r, m2_c))
             for m2_c in zip(*m2)]
             for m1_r in m1]

Nejsnazší práci s maticemi poskytuje modul NumPy.


11.2.2 Matice jako slovník

Matice jako seznam seznamů je dobrý způsob pro matice s převážně nenulovými hodnotami, uvažme však řídkou matici jako je tato:

Vyjádření řídké matice prostřednictvím seznamu obsahuje množství nul:

matrix = [ [0,0,0,1,0],
           [0,0,0,0,0],
           [0,2,0,0,0],
           [0,0,0,0,0],
           [0,0,0,3,0] ]

Poloha prvku v matici se vyjádří enticí (poř_číslo_řádku,
poř_číslo_sloupce). Číslování počíná nulou.

Alternativou je použití slovníku. Jako klíče můžeme použít entice, obsahující pořadová čísla řádků a sloupců. Zde je slovníkové vyjádření téže matice:

>>> matrix = {(0,3): 1, (2,1): 2, (4,3): 3}

Nenulové prvky matice jsou přístupné pomocí operátoru [ ]:

>>> matrix[0,3]              0,3 je souřadnice mista
1

Pokud však narazíme na nulový prvek matice, dostaneme chybu:

>>> matrix[1,3]
KeyError: (1, 3)

Tento problém řeší metoda get. Prvním argumentem je souřadnice místa, druhým argumentem je "univerzální nula":

>>> matrix.get((0,3),0)
1
>>> matrix.get((1,3),0)
0

11.3 Chyby a výjimky

Jak už bylo uvedeno v první kapitole, mohou se při kontrole zdrojového kódu kompilací vyskytnout chyby skladebné (syntaktické) a při interpretaci objektového kódu za běhu programu (run-time) takzvané chyby výjimkové, neboli výjimky (exceptions).
Při výskytu těchto událostí dojde k zastavení výpočtu a případně k poskytnutí stručné informace o (možné) příčině události.

Příklad chyby skladebné

>>> print(55/2))
   File "<stdin>", line 1
    print(55/2))
               ^
SyntaxError: unmatched ')'         # neshodný počet závorek     

Při výskytu takovéto chyby se zastaví běh kompilace a zobrazí se informace s označením místa na řádku, kde k chybě došlo (^)

Příklad výjimkové události

Při výskytu předpokládané výjimky (např. dělení nulou) zastaví Python běh programu a pokusí se zobrazit informaci o vzniklé chybě:

>>> print(55/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

Poznámka: Výjimky jsou definovány jako instance třídy, odvozené od třídy BaseException. Úplný výpis vestavěných výjimek Pythonu lze nalézt v dokumentaci Built-in Exceptions.

11.3.1   Vlastní výjimky

Kromě standardních (built-in) výjimek je možné používat vlastní výjimky a to deklarací:

Klauzulí raise lze vyvolat výjimku kdykoliv to je potřebné:

>>> x = "Hello!"
>>> if not type(x) is int:                       # sledovaná podmínka
        raise TypeError(": Jen celá čísla!")     # reakce programu
Traceback (most recent call last):               # chybové hlášení>
  File "<stdin>", line 2, in <module>
TypeError: : Jen celá čísla!                     # sledovaná podmínka

Další příklad:

>>> x = -5
>>> if x < 0 : 
        raise Exception(": Pouze x > 0!")
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
Exception: : Pouze x > 0!

Ověření správnosti uživatelem zadaného výrazu lze také zajistit předsazeným příkazem assert:

>>> x = -5
>>> assert x > 0                    # lze doplnit komentář
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError           # název chyby je zde dosazen automaticky

Jiný příklad s komentářem:

>>> import sys
>>> assert ('linux' in sys.platform), "Tento kód běží jen ve Windows!"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: Tento kód běží jen ve Windows!

Použití příkazu 'assert' je výhodné při ladění (debugging) kódu, tedy při vyhledávání potenciálně chybujícího místa kódu.
Příkaz 'assert' je vhodný pro jednoduché ověření přijatého údaje:

>>> spin = int(input('Zadej kladné číslo: '))   - viz Poznámka
Zadej kladné číslo: -7
>>> assert (spin > 0), 'Jen kladná čísla, prosím!'
Traceback (most recent call last):
  File <stdin>, line 1, in <module>
AssertionError: Jen kladná čísla, prosím!

Poznámka: Výstup z funkce 'input' je nutné konvertovat na formát 'integer'


11.3.2   Ošetření výjimek

Chceme-li zařídit aby provádění programu pokračovalo i po výskytu určité chyby, provedeme takzvané "ošetření výjimky" (exception handling).
To zpravidla spočívá v použití klauzule try v kombinaci s klauzulemi assert, except, else či finally s následujícími významy:

try: 
    uvádí ověřovaný blok kódu - provedou se všechny výrazy, 
    pokud se nenarazí na výjimku
assert 
    uvádí ověřovací podmínku, která rozdělí tok výpočtu do dvou
    větví - pokračování programu nebo chybové hlášení	
except {-, NameError, TypeError, ...}:
    následný blok kódu ošetří chybu či nesplněnou podmínku z bloku 'try' nebo 'assert'
else: 	
    uvádí blok kódu při splnění podmínky v bloku 'try'
finally:
    následný kód se provede vždy, bez ohledu na 'výjimky' 	

Klauzulí assert a except může být v případě potřeby více, případně se jedna klauzule může vztahovat na více výjimek.
Klauzule assert je neaktivní, použije-li se při spuštění programu flag -o nebo je příponou souboru ~.pyv (místo ~.pyc).

Rozšíření příkladu se zadávanou hodnotou spin:

# try_spin_int.py

spin = int(input('Zadej kladné číslo: '))
try:  
    assert (spin > 0), print('Jen kladná čísla!')
except:
    spin = int(input('Zadej kladné číslo: '))
print("Přijato: ", spin) 	

Evokací v konzole IDLE (F5) zjistíme, že některý chybný vstup je ignorován:

===== RESTART: F:\Codetest\HowTo\ch-13\spin.py ====
Zadej kladné číslo: "šle"              # ticho po pěšině
Zadej kladné číslo: -5
Přijato : -5
>>>   Tuto nepovedenou proceduru napravíme v cvičení 11.7.6                              

Pěkný příklad řešení kořenů kvadratické rovnice ax² + bx + c = 0:

import math
 
def KvadrRovnice(a, b, c): 
    try: 
        assert a != 0, "Chyba, nulový kvadratický člen"
        D = (b*b - 4*a*c)                      # hodnota diskriminantu 
        assert D > 0, "Chyba, diskriminant je menší než 0"
        r1 = (-b + math.sqrt(D))/(2 * a)       # první kořen rovnice  
        r2 = (-b - math.sqrt(D))/(2 * a)       # druhý kořen rovnice 
        print("Kořeny rovnice jsou: ", r1, "", r2) 
    except AssertionError as msg:              # vestavěná formule  
        print(msg) 

Klauzule except tiskne komentáře z klauzulí assert.

>>> KvadrRovnice(-1, 5, -6)
Kořeny rovnice jsou : 2.0  3.0
>>> KvadrRovnice(0, 5, -6)
Chyba, nulový kvadratický člen
>>> KvadrRovnice(1, 1, 6)
Chyba, diskriminant je menší než 0

Kombinaci try, except, else lze použít spolu s deklarací třídy pro ošetření vlastní výjimky, jíž v tomto případě je zadání krátkého hesla:

class MyExcept(Exception):
    def __init__(self, length, atleast):
	Exception.__init__(self)
	self.length = length
	self.atleast = atleast
	
try
    s = input("Zadej heslo: ")
    if len(s) < 7:
        raise MyExcept(len(s), 7)
except MyExcept as x:
    print ("MyExceptError: Heslo je krátké ({0}<{1})"
	           .format(x.length, x.atleast))
else
    print ("Heslo je v pořádku")	

Jak vidno, naše třída dědí z vestavěné třídy Exception. K použití deklarované třídy dochází při (opětovné) evokaci celého bloku kódu v aplikaci IDLE:

========== RESTART: F:\Codetest\HowTo\myExcept.py ==========
Zadej heslo: woke
MyExceptError: Heslo je krátké (4<7)
>>>
========== RESTART: F:\Codetest\HowTo\myExcept.py ==========
Zadej heslo: BarelPanel
Heslo je v pořádku
>>>

11.4 Kopírování objektů

Účelem kopírování je vytvoření nezávislé, autonomní kopie objektu. Tuto kopii lze provést jako mělkou (shallow) nebo důkladnou (deep).

Rozhodující vlastností kopírovaného objektu je jeho složení. Jednoprvkový objekt nebo jednoduché prvky kontejneru (list, tuple, set, dict) lze nezávisle reprodukovat jako shallow copy. Kontejner s vnořenými složenými prvky lze nezávisle reprodukovat pouze jako deep copy.
Za kopii nelze považovat přiřazení téhož objektu k více proměnným - viz alias.

11.4.1   Zdánlivá kopie

Zdánlivou kopii neboli alias vytvoříme přiřazením téhož objektu k více jménům, což lze provést najednou v jednom řádku:

>>> a = b = [1, 2, 3]                      
>>> id(a), id(b), id([1, 2, 3])
(59931496, 59931496, 59931400)

nebo postupně

>>> orig = [2, [True, False], "asi"]      # prosté přiřazení
>>> kopie = orig                          # kopie přiřazením (alias)
>>> id(orig), id(kopie), id([2, [True, False], "asi"])
(59931464, 59931464, 59931688)

Schema vztahů může vypadat takto:

Proměnné se shodnými ID odkazují na stejný objekt. Změny objektu provedené prostřednictvím jednoho aliasu se projeví i u dalšího aliasu:

>>> b[0] = 5
>>> a
[5, 2, 3]

11.4.2   Mělká kopie

Při mělké (shallow) kopii se v možném rozsahu vytvoří nový samostatný klon originálu. Mělkou kopii můžeme provést čtverým způsobem - například pro seznam:

>>> orig = [2, [True, False], "asi"]

Ve všech čtyřech uvedených případech se nezávisle kopírují pouze jednoduché členy kopírovaného seznamu. Prvky vloženého seznamu jsou závisle propojeny se svým originálem:

>>> orig[2] = "yes"; orig[1][1] = 8
>>> orig_shall_three; orig
[2, [True, 8], 'asi']               # orig_shall_three 
[2, [True, 8], 'yes']               # orig 

11.4.3   Důkladná kopie

Při důkladné (deep) kopii složeného objektu se vytvoří nový složený objekt, do něhož se vkládají nezávislé kopie objektů, nalezených v originálu.
Důkladnou kopii vytvoříme importovanou metodou deepcopy z modulu copy:

>>> from copy import deepcopy
>>> orig_deep = deepcopy(orig)
>>> orig[2] = "snad"; orig[1][1] = True
>>> orig_deep; orig
[2, [True, False], 'asi']               # orig_deep
[2, [True, True], 'snad']               # orig

Text, zaměřený specielně na kopírování entic, viz kap. 9.2.


11.5 Modul pydoc

Modul pydoc použijeme k prohledávání knihoven Pythonu, instalovaných na počítači. Na příkazový řádek napíšeme:

F:\HowTo> python -m pydoc -b
Server ready at http://localhost:52142/
Server commands: [b]rowser, [q]uit
server> 

Příkaz spustí libovolný nepoužitý port v počítači a otevře stránku ve webovém počítači. Seanci ukončíme zavřením stránky a zápisem q za poslední port v konzole.

Je to výčet všech knihoven Pythonu na vašem počítači. Klikem na jméno modulu otevřeme novou stránku s dokumentací o vybraném modulu. Například, klik na slovu keyword otevře následující stánku:

Dokumentace pro většinu modulů obsahuje tři barevně označené sektory:

Modul keyword obsahuje jedinou funkci iskeyword, která - jak její jméno naznačuje - je booleovskou funkcí, vracející True je-li zadaný řetězec klíčovým slovem:

>>> from keyword import *
>>> iskeyword('for')
True
>>> iskeyword('all')
False

Datový prvek kwlist obsahuje seznam všech současných klíčovych slov Pythonu:

>>> from keyword import *
>>> print(kwlist)
['and', 'as', 'assert', 'async', 'await','break', 'class',
'continue','def', 'del', 'elif', 'else', 'except','finally',
'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda',
'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try',
'while', 'with', 'yield', 'False', 'None', 'True']

Doporučujeme časté používání služby pydoc ke zkoumání rozsáhlých knihoven Pythonu. Mnoho pokladů čeká na své objevení!


11.6 Glosář

rekurzivní definice (recursive definition)
Definice, která se odkazuje sama na sebe. Aby byla smysluplná, musí obsahovat nerekurzivní základní případ. Tímto se liší od definice v kruhu. Rekurzivní definice často skýtají elegantní způsob vyjádření složitých datových struktrur.
rekurze (recursive call)
Příkaz v rekurzivní funkci, který je volán právě prováděnou funkci (to jest samu sebe).
základní případ (base case)
Větev podmíněného příkazu v rekurzivní funkci, který nevede k rekurzivnímu volání.
nekonečná rekurze (infinite recursion)
Funkce, která rekurzivně volá sama sebe, aniž by dospěla k základnímu případu. Způsobí chybu při běhu programu.
koncová rekurze (tail recursion)
Rekurzivní volání umístěné na konci definice funkce. Tato metoda se příliš nedoporučuje, protože bývá méně účinná než iterativní funkce (see the Wikipedia article on tail recursion for more information).
výjimka (exception)
Registrovaná chyba při běhu programu.
ošetřit výjimku (handle an exception)
Zabránit registrované chybě (výjimce) aby ukončila běh programu s použitím příkazů try a except.
vyvolat výjimku (raise)
Aktivovat výjimku s použitím příkazu raise.

11.7 Cvičení

  1. Napište funkci recursive_min, která vrátí nejmenší číselnou hodnotu seznamu s vnořenými seznamy:
    def recursive_min(nested_num_list):
        """
        >>> recursive_min([2, 9, [1, 13], 8, 6])
        1
        >>> recursive_min([2, [[100, 1], 90], [10, 13], 8, 6])
        1
        >>> recursive_min([2, [[13, -7], 90], [1, 100], 8, 6])
        -7
        >>> recursive_min([[[-13, 7], 90], 2, [1, 100], 8, 6])
        -13
        """
    
    Funkce by měla vyhovět všem doctestům.
  2. Napište funkci recursive_count, která vrátí počet výskytů číselné hodnoty target v seznamu nested_number_list:
    def recursive_count(target, nested_num_list):
        """
        >>> recursive_count(2, [2, 9, [2, 1, 13, 2], 8, [2, 6]])
        4
        >>> recursive_count(7, [[9, [7, 1, 13, 2], 8], [7, 6]])
        2
        >>> recursive_count(15, [[9, [7, 1, 13, 2], 8], [2, 6]])
        0
        >>> recursive_count(5, [[5, [5, [1, 5], 5], 5], [5, 6]])
        6
        """
    
    Funkce vyhoví všem doctestům?
  3. Napište funkci flatten, která vrátí jednoduchý seznam, obsahující všechny honoty z nested_number_list:
    def flatten (nested_num_list):
        """
        >>> flatten([2, 9, [2, 1, 13, 2], 8, [2, 6]])
        [2, 9, 2, 1, 13, 2, 8, 2, 6]
        >>> flatten([[9, [7, 1, 13, 2], 8], [7, 6]])
        [9, 7, 1, 13, 2, 8, 7, 6]
        >>> flatten([[9, [7, 1, 13, 2], 8], [2, 6]])
        [9, 7, 1, 13, 2, 8, 2, 6]
        >>> flatten([[5, [5, [1, 5], 5], 5], [5, 6]])
        [5, 5, 1, 5, 5, 5, 5, 6]
        """
    
  4. Napište funkci readposint(), která vyzve uživatele k zadání kladného celého čísla a to případně opakovaně, dokud není zadána korektní vstupní hodnota.
    V ukázce se zadávanou hodnotou 'spin' nám procedura chodí korektně jen pro jeden chybný vstup, "což není to pravé ořechové".
    Pro ověření správnosti uživatelského vstupu použijte mechanizmus ošetření výjimky.
  5. Přepište funkci factorial s použitím iterace místo rekurze. Volejte svou funkci pro argument 1000 a změřte jak rychle vrátí výsledek.
  6. Upravte první příklad (try_spin_int.py) v odstavci Ošetření výjimek tak, aby procedura reagovala i na "šle".

prev up next title end end