3. Výjimky a dekorátory
Obsah
Výjimky
Více informací zde.
Během našeho programování jsme se již setkali s několika výjimkami (exceptions). Jednu ukázkovou můžeme vyvolat následovně:
Python 3.9.4 (default, Apr 5 2021, 01:50:46)
[Clang 12.0.0 (clang-1200.0.32.29)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 2 / 0
Traceback (most recent call last):
File "", line 1, in
ZeroDivisionError: division by zero
Nejdůležitější je řádek číslo 8, který obsahuje název výjimky ZeroDivisionError a doplňující popis division by zero.
Jazyk Python obsahuje sadu vestavěných výjimek, které můžete ve svých programech používat. Jejich podrobnější popis naleznete zde. Jejich krátký přehled je uveden níže.
AssertionError # podmínka příkazu assert není splněna
IndexError # přístup na neexistující index v kolekci
NameError # přístup k nedefinované proměnné
TypeError # operace s nekompatibilními datovými typy
ValueError # operace s kompatibilními datovými typy ale s chybnou hodnotou
OverflowError # výsledek operace je příliš velký a nelze reprezentovat v paměti
ZeroDivisionError # druhý operant dělení nebo modulo roven nule
RuntimeError # blíže nespecifikovaná chyba
Exception # obecná výjimka
Ošetření výjimek
V předchozím příkladu jsme viděli kód, který vyvolá výjimku. Teď si ukážeme,1jak na vyvolanou výjimku reagovat.
try:
a = 2 / 0
except ZeroDivisionError as err:
print(f"Following exception was raised: {err}")
finally:
print("Finishing program")
Ošetření výjimek realizují příkazy try, except, finally. Blok začínající příkazem try obsahuje kód, ve kterém se výjimky odchytávají. Následují bloky except s typy výjimek a reakcemi na ně. Volitelný blok finally se provede v každém případě, ať už výjimka nastane nebo nikoli.
V příkazu except můžeme zadat více vyjímek, pokud jsou uloženy v datové struktuře tuple.
try:
func():
except (SpecificErrorOne, SpecificErrorTwo) as err:
print(f"Following exception was raised: {err}")
Vyvolání výjimek
Výjimky můžeme nejen ošetřovat ale rovněž vyvolávat. V následujícím příkladu funkce vyvolá vyjimku ValueError pokud není její vstup validní. Výjimka by vždy měla mít anglický dolpňující popis.
def subtract_lists(list1, list2):
"""Subtract two list piecewise."""
if len(list1) != len(list2):
raise ValueError(f"Lists are not same length, {len(list1)} and {len(list2)} was given.")
result = []
for a, b in zip(list1, list2):
result.append(a - b)
return result
subtract_lists([1, 2], [4, 3])
Existují dva přístupy k práci s výjimkami EAFP (it’s easier to ask for forgiveness than permission) a LBYL (look before you leap). Rozdíl je vidět v následujícím příkladě.
# LBYL
if "key" in dict_:
value += dict_["key"]
# EAFP
try:
value += dict_["key"]
except KeyError:
pass
V Pythonu s ohledem na jeho dynamičnost častěji upřednostnujeme EAFP. Je možné používat obojí, v případě LBYL však není dobré kontroly příliš přehánět.
Funkce vyššího řádu
Funkce které vracejí funkce nebo přijímají funkce jako argument nazýváme funkce vyššího řádu.
# funkce vyššího řádu přijímající funkci jako argument
def apply_operation(list_, operation):
"""Applies operation on the given list"""
return operation(list_)
# běžná funkce
def list_sum_squared(list_):
"""Squared sum of all members of given list"""
return sum(list_) ** 2
apply_operation([1, 2, 3], list_sum_squared)
# funkce vyššího řádu vracející funkci
def parent_function():
print("Parent function is running.")
local_x = 10
def local_function():
print("Child function is running.")
return local_x
return local_function
# k vnitřní funkci nelze primo přistoupit
local_function()
# k vnitřní funkci se můžeme dostat
local_function = parent_function()
# a následně ji použít
local_function()
# připadně vše v jenom kroku
parent_function()()
Dekorátory
Co je dekorátor můžeme demonstrovat na jednoduchém příkladu. Představme si, že nami definovanou funkci chceme "obalit" další funkcionalitou (například vytisknout řetězec před a po volání).
# dekorátor
def my_decorator(func):
"""Decorator description."""
def wrapper():
print("Something before function call")
func()
print("Something after function call")
return wrapper
# námi definovaná funkce
def my_function():
print("Super function!")
# volání původní funkce
my_function()
my_function
# obalení funkce dekorátorem
my_function_decorated = my_decorator(my_function)
# volání modifikované funkce
my_function_decorated()
my_function_decorated
Všimněme si, že dekorátor splňuje definici funkce vyššího řádu. Pro jednodušší práci s dekorátory Python nabízí "syntaktický cukr" @nazev_dekoratoru.
# dekorátor
def do_twice(func):
"""Calls a function twice."""
def wrapper():
func()
func()
return wrapper
# dekorování funkce pomoci @
@do_twice
def my_function():
print("Super function!")
Dekorování funkce s argumenty
V případě, že námi dekorovaná funkce přijímá argumenty narazíme na následující problém.
# dekorátor
def do_twice(func):
"""Calls a function twice."""
def wrapper():
func()
func()
return wrapper
# dekorování funkce pomoci @
@do_twice
def my_function(arg):
print(f"Super function! {arg}")
# chyba - dekorovaná funkce nepočítá s argumentem
my_function("Ahoj!")
Tuto situaci opravíme následovně.
# dekorátor - podporuje předání argumentů vnitřní funkci
def do_twice(func):
"""Calls a function twice."""
def wrapper(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper
# dekorování funkce pomoci @
@do_twice
def my_function(arg):
print(f"Super function! {arg}")
my_function("Ahoj!")
Dekorování funkce vracející hodnotu
Podobný problém nastane pokud funkce vrací hodnotu. Její dekorovaná verze bude tuto hodnotu zahazovat.
# dekorátor
def do_twice(func):
"""Calls a function twice."""
def wrapper(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper
# dekorování funkce pomoci @
@do_twice
def multiply_list(list_, by):
"""Multiply items from list by given value.
Args:
list_: list to be multiplied
by: value by which items are multiplied
Returns:
multiplied list
"""
result = []
for item in list_:
result.append(item * by)
return result
# problém - výsledek nebude vrácen
multiply_list([1, 2, 3], 5)
Jednoduchou modifikací dekorátoru situaci vyřešíme.
# upravený dekorátor
def do_twice(func):
"""Calls a function twice."""
def wrapper(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper
Problém identity funkce
Při dekorování funkce dochází ke ztrátě identity funkce, což ovlivňuje i nedosažitelnost původního docstringu. Demonstrace z předchozího příkladu:
multiply_list
multiply_list.__name__
help(multiply_list)
Situaci vyřešíme použitím dekorátoru functools.wraps, který zachová identitu dekorované funkce.
import functools
def do_twice(func):
"""Calls a function twice."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper
@do_twice
def multiply_list(list_, by):
"""Multiply items from list by given value.
Args:
list: list to be multiplied
by: value by which items are multiplied
Returns:
multiplied list
"""
result = []
for item in list_:
result.append(item * by)
return result
multiply_list
multiply_list.__name__
help(multiply_list)
Příklad z praxe
Základní šablona dekorátoru je následující:
import functools
def decorator(func):
"""Description."""
@functools.wraps(func)
def wrapper_decorator(*args, **kwargs):
# kód vykonaný před voláním funkce
value = func(*args, **kwargs)
# kód vykonaný po volání funkce
return value
return wrapper_decorator
Jedním z příkladu je takzvaný timing funkce (měření doby běhu).
import functools
import time
def timer(func):
"""Print the runtime of the decorated function"""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__} in {run_time:.4f} secs")
return value
return wrapper_timer
@timer
def waste_some_time(num_times):
for _ in range(num_times):
1 + 1
waste_some_time(1000000)
# Finished waste_some_time in 0.0276 sec
Nelokální proměnné
Pokud chceme přistoupit k proměnné z lexikálně nadřazeného prostředí, použijeme deklaraci nonlocal. To můžeme použít například pro dekorátor, který počítá celkový běh dekorované funkce.
import functools
import time
def timer(func):
"""Print the runtime of the decorated function"""
total_time = 0
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
nonlocal total_time # total_time se bude hledat v lexikálním předkovi funkce wrapper_timer
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
total_time += run_time
print(f"Finished {func.__name__} in {run_time:.4f} sec (total running time {total_time:.4f} sec)")
return value
return wrapper_timer
@timer
def waste_some_time(num_times):
for _ in range(num_times):
1 + 1
waste_some_time(1000000)
# Finished waste_some_time in 0.0276 sec (total running time 0.0276 sec)
waste_some_time(1000000)
# Finished waste_some_time in 0.0188 sec (total running time 0.0464 sec)
Funkční atributy
Jiné řešení předchozího příkladu nabízí funkční atributy, tj. přímo funkci můžeme do pojmenovaného atributu nastavit nějakou hodnotu. Oproti nelokálním proměnným, lze tuto hodnotu používat i mimo samotnou funkci (v příkladu níže je ovšem vrácena obalující funkce wrapper_timer, namísto func, která má atribut total_time). Tuto funkcionalitu lze také použít například pro přidání nějakého příznaku funkci atd.
Předchoí příklad dekorátoru, který počítá celkový běh dekorované funkce, nyní vyřešený pomocí funkčního atributu.
import functools
import time
def timer(func):
"""Print the runtime of the decorated function"""
func.total_time = 0 # Přidáme atribut přímo na funkci
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
func.total_time += run_time # Aktualizujeme atribut funkce
print(f"Finished {func.__name__} in {run_time:.4f} sec (total running time {func.total_time:.4f} sec)")
return value
return wrapper_timer
Zanořování dekorátorů
Dekorátory je možné zanořovat, pozor, záleží na pořadí!
@do_twice
@timer
def waste_some_time(num_times):
for _ in range(num_times):
1 + 1
waste_some_time(100)
@timer
@do_twice
def waste_some_time(num_times):
for _ in range(num_times):
1 + 1
waste_some_time(100)
Dekorátory s argumenty
Dekorátoru je možné předávat argumenty, ale potřeba jej obalit do další funkce, která definuje potřebné paramatery.
def repeat(num_times):
"""Repeats function n times."""
def decorator_repeat(func):
@functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
for _ in range(num_times):
value = func(*args, **kwargs)
return value
return wrapper_repeat
return decorator_repeat
@repeat(num_times=4)
@timer
def waste_some_time(num_times):
for _ in range(num_times):
1 + 1
waste_some_time(100)
Další dekorátory z modulufunctools
functools.lru_cache je dekorátor používaný k uložení výsledků funkce do mezipaměti, takže následná volání se stejnými argumenty mohou vrátit uložený výsledek namísto opakovaného provádění stejného výpočtu.
from functools import lru_cache
@lru_cache(maxsize=None) # pamatujeme si všechny výsledky, jinak zde uvádíme počet záznamů
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
def fibonacci_no_cache(n):
if n < 2:
return n
return fibonacci_no_cache(n-1) + fibonacci_no_cache(n-2)
# 0.04s
fibonacci(40)
# 9.8s
fibonacci_no_cache(40)
functools.singledispatch je dekorátor, který umožňuje definovat generické funkce schopné pracovat s různými typy argumentů. Poskytuje mechanismus jednorázové distribuce (na základě typu prvního argumentu) pro přetížení funkcí, což znamená, že chování dané funkce se mění v závislosti na typu argumentu.
from functools import singledispatch
# definujeme základní chování pro jinak nespecifikované hodnoty
@singledispatch
def process(value):
# Default behavior
print(f"Processing a general type: {value}")
# definujeme chování pro argumety typu int
@process.register(int)
def _(value):
print(f"Processing an integer: {value}")
# definujeme chování pro argumety typu str
@process.register(str)
def _(value):
print(f"Processing a string: '{value}'")
# definujeme chování pro argumety typu list
@process.register(list)
def _(value):
print(f"Processing a list with {len(value)} items: {value}")
process(42) # zavolá funkci process definovanou pro int
process("hello") # zavolá funkci process definovanou pro str
process([1, 2, 3]) # zavolá funkci process definovanou pro list
process(3.14) # zavolá základní funkci process