![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Všechny dosud poznané datové typy mohou být v seznamech a enticích uspořádány přerůzným způsobem. Seznamy a entice mohou být také vnořené.
Povšimněme si, že termín seznam s vnořenými seznamy
je použit ve své vlastní definici. Rekurzivní definice jako tato jsou zcela obvyklé v matematice a v programování. Poskytují stručný a účinný způsob popisu
rekurzivních datových struktur, které jsou částečně složeny z menších a jednodušších výskytů sebe sama. Nejedná se o definici v kruhu, neboť v jisté úrovni dospějeme k seznamu, který nemá žádné položky, jež by byly seznamem.
Předpokládejme, že našim úkolem je napsat funkci, která sečte všechny hodnoty v seznamu s vnořenými seznamy. Python má vestavěnou funkci, která vytvoří součet řady čísel:
>>> sum([1, 2, 8]) 11 >>> sum((3, 5, 8.5)) 16.5 >>>
Pro náš
>>> sum([1, 2, [11, 13], 8])Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'int' and 'list'
Problém spočívá v tom, že třetí položka seznamu je také seznam
Rekurze je technika, při které funkce volá (s jinými parametry) samu sebe.
Funkci
def countDown (n):if n < 1:# omezující podmínka (základní případ) return print ("Nemelem")# melem, n=0 nebo n>n countDown(n-1)# rekurzivní volání se změnou argumentu
Správně definovaná 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.Tuto situaci si Python sám ošetří tak, že po určitém počtu opakování výpočet zastaví.
V matematice známý pojem 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:
0! = 1# faktoriál nuly = 1 n! = n(n-1)
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 = 1for iin range (1, n+1):# range končí před koncovou mezí f = f*ireturn f
Vyjádření pomocí rekurze
def fact_r (n):if n < 2:# základní případ) return 1return n * fact_r(n-1)# rekurzivní volání funkce
Opakovaný výpočet pomocí rekurze je někdy pomalejší než výpočet pomocí iterace. Na konci odstavce 13.3.2 si ukážeme výpočet Fibonacciho čísla
rekurzí, iterací a rychlý výpočet pomocí memoizace.
Má se za to, že každé rekurzivní řešení lze vyjádřit iterační smyčkou a naopak. Oběma způsobům je společná existence mezní podmínky, určující konec opakovaného výpočtu.
Nekonečná rekurze může skončit krachem systému, zatímco nekonečná iterace je odchycena a ukončena Pythonem.
K vytvoření součtu všech čísel v našem rekurzivním seznamu s vnořeným seznamem potřebujeme traverzovat seznamem, navštívit každou číselnou 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 = 0for elementin nested_num_list:if type (element) ==type ([]):# je-li položkou seznam sum = sum + recursive_sum(element)else : sum = sum + elementreturn sum
Tělo fce se skládá hlavně ze smyčky
Poněkud komplikovanější úlohou je nalezení největší hodnoty v seznamu s vnořeným seznamem. Následující definice funkce obsahuje
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 elementin nested_num_list:if type (element) ==type ([]): max_of_elem = recursive_max(element)if largest < max_of_elem: largest = max_of_elemelse :# element is not a list if largest < element: largest = elementreturn largest
Čertovým kopýtkem tohoto problému je určení numerické hodnoty pro inicializaci proměnné
Při rekurzivním volání téže funkce se generuje řada výsledků, z nichž jeden má být určen jako výstup volání posledního. Takový výsledek se nazývá základní případ (base case). Bez základního případu dostaneme nekonečnou rekurzi. Tato situace je technicky ošetřena tak, že se Python zastaví po dosažení implicitně nastavené hloubky rekurze (1000 cyklů) a vrací chybu při běhu programu.
Ve funkci
Příkladem nekonečné rekurze budiž tato funkce:
infinite_recursion.py def recursion_depth (number):
Ve stejném adresáři, ve kterém máte uložený svůj program, zapište na příkazový řádek:
F:/codetest/> python infinite_recursion.py
Po bezmocném sledování ubíhajících řádků na monitoru se po chvíli dočkáte následujícího konce záznamu:
File "infinite_recursion.py", line 3, in recursion_depth recursion_depth(number + 1) RuntimeError: maximum recursion depth exceeded
Nekonečná rekurze může být těžko něčemu prospěšná, takže si při každé deklaraci rekurzivního volání bedlivě ohlídáme okrajové podmínky, vytvářející
Když se rekurzivní volání objeví na posledním řádku těla funkce, označujeme jej jako koncovou rekurzi.
Zde je koncová rekurze použita u funkce
def countdown (n):if n == 0:else :
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 == 0or n == 1:return 1else :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í
Mnohem rychlejší než rekurzivní verze je verze iterační, která by mohla vypadat takto:
def fiboiter (n): old, new = 0, 1if n == 0:return 0for iin range (n-1): old, new = new, old + newreturn new
Vyzkoušejte volání
Při cvičení si zkusíte napsat iterační verzi funkce
Když jsme si v předchozím odstavci pohrávali s funkcí
Abychom pochopili proč, pohleďme na toto schéma volání funkce
Schéma zobrazuje sadu funkcí v rámečcích se šipkami, ukazujícími na volané funkce. Nejvýše umístěná fce,
Spočítejme, kolikrát je voláno
Dobrým řešením, zvaným memoizace (memoization), je ukládat si záznamy (hints) o spočítaných hodnotách do slovníku pro potřebu dalšího použití.
Takovýmto záznamům říkejme třeba náznaky. Zde vidíme realizaci funkce
memo = {0:0, 1:1}def fibomem (n):if n notin memo: memo[n] = fibomem(n-1) + fibomem(n-2)return memo[n]
Slovník
Kdykoli je funkce
S použitím této verze náš počítač spočítá
>>> fibomem(100) 354224848179261915075
Alternativní způsob výpočtu Fibbonacciho čísla pomocí memoizace představuje výpočet s podporou dekorátorů (viz kap.12.7 a 12.8)
def memoize (fun):# dekorátorová funkce memo = {}def wrapper(x):if xnot in memo: memo[x] = fun(x)return memo[x]return wrapper @memoize# dekorace funkcí def fibomem (n):if n == 0:return 0elif n == 1:return 1else :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 argsnot in self.memo: self.memo[args] = self.fn(*args)return self.memo[args] @Memoize# dekorace třídou def fibomem (n):if n == 0:return 0elif n == 1:return 1else :return fibomem(n-1) + fibomem(n-2)# >>> fibomem(100) ==> 354224848179261915075
Všechny tři způsoby používají slovník
Iterátorem v tomto případě je instance třídy s definovanými metodami
Jako příklad si uvedeme skript, který si poté pustíme v konzole:
class Count :def __init__ (self, m): self.m = m-1 self.n = 0def __iter__ (self):return selfdef __next__ (self):if self.n <= self.m: cur, self.n = self.n, self.n + 1return curelse :raise StopIteration()
>>> cnt = Count(5)# instance třídy a zároveň iterátor >>> next(cnt)# první iterace 0 >>> for i in cnt: print(i, end=" ") 1 2 3 4# opakovaná iterace pro zbytek členů >>> for in cnt: print(i, end=" ") ...# zdvořilé mlčení, iterátor je prázdný
Alternativa funkce
class Squares (object):def __init__ (self, start, stop): self.start = start self.stop = stopdef __iter__ (self):return selfdef __next__ (self):if self.start >= self.stop:raise StopIteration current = self.start * self.start self.start += 1return current
>>> sqr = Squares(4,8)# instance třídy a zároveň iterátor >>> >>> for i in sqr: print(i, end=" ") 16 25 36 49# dychtivá (eager) iterace >>> for in sqr: print(i, end=" ") ...# zdvořilé mlčení, iterátor je prázdný
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
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.
>>> 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ř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
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
>>> 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!
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: uvádí ověřovaný blok kódu - provedou se všechny výrazy, pokud se nenarazí na výjimkuassert: 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: 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í
Klauzule
Problematické rozšíření příkladu se zadávanou hodnotou 'spin':
# in_text/try_spin_int.py try: spin = int(input('Zadej kladné číslo: '))assert (spin > 0), 'Jen kladná čísla, prosím!'except: spin = int(input('Zadej kladné číslo: '))
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í 13.8.6
Pěkný příklad řešení kořenů kvadratické rovnice:
import mathdef 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 except AssertionError as msg:# vestavěná formule
Klauzule
>>> 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
class MyExcept (Exception):def __init__ (self, length, atleast): Exception.__init__(self) self.length = length self.atleast = atleasttry: s =input ("Zadej heslo: ")if len (s) < 7:raise MyExcept (len(s), 7)except MyExcept as x:else:
Jak vidno, naše třída dědí z vestavěné třídy
========== RESTART: F:\Codetest\HowTo\aTrampol.py ========== Zadej heslo:woke MyExceptError: Heslo je krátké (4<7) >>> ========== RESTART: F:\Codetest\HowTo\aTrampol.py ========== Zadej heslo:BarelPanel Heslo je v pořádku >>>
Funkce by měla vyhovět všem doctestům.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 vyhoví všem doctestům?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 """
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] """
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |