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