Python profesionálně: dynamické parametry, generátory, lambda funkce a with

Python logo

V minulém díle jsme se podívali na několik jednoduchých syntaktických tipů, které nám usnadní vývoj v programovacím jazyce Python. Dnes navážeme generátory, lambda funkcemi, with konstrukcemi a dynamickými parametry.

Seriál: Programujte v Pythonu profesionálně (5 dílů)

  1. Python profesionálně: úvod 3.4.2012
  2. Python profesionálně: dynamické parametry, generátory, lambda funkce a with 10.4.2012
  3. Python profesionálně: co jazyk nabízí 16.4.2012
  4. Python profesionálně: návrhové vzory 14.5.2012
  5. Python profesionálně: metatřídy 21.5.2012

Dynamické parametry

Na předchozí článek navážeme další zajímavostí Pythonu. Jedná se o neznámý počet parametrů. To určitě všichni známe a ti, kteří si četli minimálně nějaký tutoriál, na to i narazili. Jedná se o hvězdičku v parametru funkce/metody. Ale věděli jste, že to jde na obou místech; jak ve volání, tak v definici?

def f(first, second, *rest):
    print first, second, rest

f(*range(1, 6)) # 1 2 (3, 4, 5)

Jak je vidět, stačí přidat hvězdičku a podle kontextu se seznam rozloží nebo složí. To je užitečné například pro funkce k formátování řetězců, sumarizační funkce a podobně. Ale co kdybych chtěl udělat vyhledávající funkci, která využívá pojmenovávaných parametrů? A samozřejmě bych nechtěl definovat všechny možné parametry ručně… I na to Python myslí a řešením je – jak jinak než další hvězdička.

def search(**kwds):
    map(check_search_key, kwds.keys())
    # Do something...

def check_search_key(key):
    if key not in ('id', 'name', 'mail', 'url'):
        raise AttributeError('You can't search by key "%s".' % key)

A opět to jde oběma směry.

params = {'first': 1, 'second': 2}
f(**params)

Mimochodem u těchto speciálních parametrů, pokud není vhodnější název pro konkrétní situaci, se většinou používají názvy args a kwds. Je to něco jako parametry self a cls u metod.

Kvíz: co se stane?

dict(params, **{'first': 0, 'third': 3})

Řešení: Built-in type  dict se slovníkem v parametru vytváří jeho mělkou kopii. Pomocí této funkce lze vytvořit slovník i přes pojmenované parametry, kde název parametru je klíč a hodnota je (nečekaně) hodnota. (A také lze vytvořit slovník pomocí seznamu obsahující položky opět typu seznam s dvěma položkami – první se použije jako klíč a druhá jako hodnota.) Pokud tyto vlastnosti sloučíme, tak nejprve vytvoříme kopii slovníku params a poté tento nově vytvořený slovník updatneme druhým slovníkem, který však musíme předat jako pojmenované parametry. Je to vlastně to samé jako následující.

dict(params, first=0, third=3)

A proč jsem to napsal předtím se slovníkem? Nu protože se mi zdá čitelnější první varianta, kde vidím dva slovníky a ne slovník a nějaké pojmenované parametry. Navíc klíče mohou nabývat obecných názvů, a to může být matoucí. A navíc pomocí této syntaxe lze mergnout reference dvou slovníků a ani jeden nezměnit.

Výsledek bude tedy takovýto:

{'second': 2, 'third': 3, 'first': 0}

Generátory

Když jsem ještě nebyl v Pythonu zběhlý, považoval jsem generování seznamů za špatnost. Protože se mi to nezdálo přehledné. Jenže Python není jako každý jiný jazyk – je potřeba si na něj zvyknout a pak zjistíte, že je v některých věcech bezvadný.

Dnes už mám generátory rád. Když někomu ukazuji Python, tak se strašně rád ptám, jak by ta dotyčná osoba udělala dvojrozměrné pole s třeba malou násobilkou. A pak ukážu, jak bych to dělal já.

s = []
for x in range(11):
    row = []
    for y in range(11):
        row.append(x*y)
    s.append(row)

# vs.

s = [[x*y for y in range(11)] for x in range(11)]

Jednou se mi stalo, že se pak dotyčná osoba zeptala, jak bych tam přidal podmínku, kdybych chtěl třeba jenom řádky se sudými čísly. Prý bych teď určitě musel celý kód přepsat na normální cyklus a přidat podmínku. Tak jsem tedy ukázal, jak to musím přepsat…

s = [[x*y for y in range(11)] for x in range(11) if x % 2 == 0]

Dál už se mě nikdo raději na nic nezeptal, ale to neznamená, že toho není víc!

Nevýhoda generování seznamů (list comprehension) je v tom, že se musí celé vytvořit v paměti. Představme si problém: potřebujeme zinicializovat hodně moc instancí produktů, každý nějak zpracovat a po zpracování zahodit, protože není dále potřeba (vhodné například pro vygenerování XML souboru). Ale inicializace a zpracování z nějakých důvodů nelze mít na jednom místě (třeba MVC) a je tedy potřeba předat referenci na seznam s těmito instancemi.

def get_products():
    return [Product(product_id) for product_id in range(1000, 2000, 2)]

def do_something_with_products(products):
    # Do something...

do_something_with_products(get_products())

Toto řešení by v paměti vytvořilo zbytečně 500 instancí. Je lepší způsob a stačí jen vyměnit typ závorek. Místo hranatých použijeme kulaté. Tím se nevytvoří seznam s instancemi, ale pravý generátor, který lze použít v iteraci a kód pro každý prvek se vyvolá, až když je opravdu potřeba. Tedy instance produktu se zavolá vždy až když je ten produkt potřeba; nikoliv že se vytvoří nejprve všechny a pak se přes ně jen iteruje. Tím tedy v paměti budu mít pouze jednu instanci (pokud na produkt nevytvořím jinou proměnnou s referencí).

l = [x for x in range(10)] # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
g = (x for x in range(10)) # <generator object <genexpr> at 0xb6d5b4dc>

Má to ale své úskalí. Kdybych chtěl produkty řadit, tak nemohu, s generátorem to nelze. Generátor iteruje tak, jak byl napsán a pořadí nelze změnit. Takže generátor lze použít jen tehdy, když chci položkami iterovat v nezměněném pořadí nebo když ho chci používat s klíčovým slovem  in.

l[:4] # [0, 1, 2, 3]
g[:4] # TypeError

l[::-1] # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
g[::-1] # TypeError

l.reverse() # ok
g.reverse() # AttributeError

for item in l: pass # ok
for item in g: pass # ok

(item for item in l if item < 5) # ok
(item for item in g if item < 5) # ok

if 5 in l: pass # ok
if 5 in g: pass # ok

Python obsahuje spoustu generátorů. Už několikrát jsem v ukázkách použil built-in funkci range, která vrací seznam s čísly, která žádám. Existuje k ní analogická funkce, která vrací něco na způsob generátoru a jmenuje se xrange (ve skutečnosti vrací xrange, který se od generátoru liší tím, že nemá metody next a podobně).

range(4) # [0, 1, 2, 3]
xrange(4) # xrange(4)

Za zmínku stojí, že v Pythonu 3 funkce xrange už není a range se chová jako xrange. Když je v Pythonu 3 opravdu potřeba seznam místo iterátoru, tak se musí zavolat  list(range(*args)).

Jak by tedy vypadal ve výsledku náš problém (pouze změna v jedné funkci):

def get_products():
   return (Product(product_id) for product_id in xrange(1000, 2000, 2))

Od Pythonu 3 přibudou dva noví kolegové pro jednoduší generování slovníků a množin.

# Generovani slovniku v Pythonu 2.
dict((x, x**2) for x in range(10))

# Nove generovani slovniku v Pythonu 3 (predchozi samozrejme funguje taky).
{x: x**2 for x in range(10)}

# Generovani mnoziny v Pythonu 2.
set(x for x in range(10))

# Nove generovani mnoziny v Pythonu 3 (predchozi samozrejme funguje taky).
{x for x in range(10)}

Na závěr si ještě jednou všechny generátory roztřídíme, ať v tom máme jasno. První ukázaný (generátor seznamů) lze v češtině spatřit také jako „generátorová notace seznamů“ a v angličtině to je „list comprehensions“, případně „dictionary comprehensions“ a „set comprehensions“ přidané v Pythonu 3. Další ukázaná syntaxe byla generátorová notace, v angličtině „generator expressions“ a slouží pro snadné vytvoření generátoru. Poslední ukázkou byl xrange, který sice nevrací generátor, ale chová se tak.

Aby to nebylo tak snadné – v Pythonu existuje ještě tzv. iterátor, přičemž generátor implementuje rozhraní iterátoru a navíc ho o něco rozšiřuje. O co konkrétně si můžete přečíst v dokumentaci. A téměř cokoliv lze převést na iterátor pomocí built-in funkce iter.

Vlastní generátory

Generátory jsou hezké, dají se šikovně použít. Ale neřeší situace, kde potřebuji vyřešit velmi specifický problém. Například máme seznam instancí produktů a každý může mít načteny jen základní nebo kompletní informace – zákazník si objednal několik produktů a ve svém účtu se dívá na stav své objednávky. My máme v databázi informace o produktech (ID, název, cena, …), ale informace o pohybu produktů si načítáme z externího systému. Naneštěstí načítání z externího systému není nejrychlejší, a tak se tomu chceme co nejvíce vyhnout. Takže zákazníkovi zobrazíme jen základní informace a až když si klikne na detail, načteme informace z externích systémů.

Máme tedy seznam instancí a každý produkt může být v jiném stavu. Některé jsou načteny kompletně a některé mají jen základní informace o sobě. A teď je situace, kde potřebuji mít načteny kompletně všechny. Jedna možnost je na začátku každé iterace se zeptat, v jakém stavu produkt je, a případně produkt donačíst. A tento kód kopírovat na místa, kde to je všude potřeba.

Zřejmě už cítíte, že to není dobré. „DRY!“. Proto si na to vytvoříme vlastní generátor, je to jednoduché. Napíšeme normální funkci, jen místo klíčového slova return použijeme yield a nepoužijeme ho až na konci funkce, ale v iteraci.

def iterate_and_load_over_products(products):
    for product in products:
        if not product.is_fully_loaded():
            product.load()
        yield product

for product in iterate_and_load_over_products(products):
    # ...

Výhoda spočívá v tom, že se nemusí čekat, až se všechny produkty načtou, a produkt se může zpracovávat ihned, jakmile je kompletně načten. Jinými slovy nemusím půl hodiny čekat, než se mi vše načte, aby mi program spadl na nějaké chybě v kódu se zpracováním dat. Představme si situaci: přijde produkťák a zeptá se nás, za jak dlouho to bude hotové a my odpovíme, že teď se bude půl hodiny něco načítat a samotné zpracování potom bude otázka pár vteřin. Skript půl hodiny jede a pak najednou spadne na chybě, na kterou se v testovacím prostředí nenarazilo…

Anonymní lambda funkce

Jsou situace, kde je potřeba funkce na „jedno“ použití někde uvnitř jiné funkce. Deklarace funkce ve funkci nevypadá moc hezky (ale nebráním se tomu) a proto existují lambda funkce.

square = lambda x: x**2
square(5) # 25

Je to velmi jednoduché. V ukázce jsem vytvořil anonymní funkci, která přijímá jeden parametr a vrací výsledek výrazu za dvojtečkou. Referenci na funkci jsem si uložil do proměnné square a hned na dalším řádku použil.

Lambda funkce může přijímat parametrů, kolik je libo (oddělené čárkami) a dokonce nemusí být žádný (kde se dá smysluplně využít takové funkce si ještě povíme). Jednodušeji: platí to, co pro normální funkce. Výsledek funkce je vždycky výsledek výrazu, který smí být jen jeden; ale je jedno, jak moc složitý. Toť vše, už jen přidám nějaké další ukázky.

# Opravdu anonymni funkce.
(lambda x: x**2)(5)

# Moznost vytvorit ve volani jine funkce jako parametr.
dates = [datetime.date(year, 1, 1) for year in range(2000, 2020)]
dates.sort(cmp=lambda x, y: cmp(x.isoweekday(), y.isoweekday()))

# Dalsi podobne pouziti.
map(lambda d: d.isoformat(), dates)

A ještě na ně narazíme…

Konstrukce  with

Největší problém jsou opakující se kusy kódu. Ještě horší je, když jsou velmi důležité. A nejhorší je, když se jedná o kusy kódu, které se provedou jednou za čas, typicky odchytávání chyb a podobně. Dobře to může být vidět při práci se soubory, kde bychom se určitě neměli spoléhat na to, že se stream sám zavře a data fyzicky zapíšou na disk.

try:
    f = open(fileName, 'a')
    f.write('xyz')
except IOError, e:
    f.close()
    # ...
else:
    f.close()
    # ...

Zkuste si takhle řešit více různých zápisů a čtení ze souboru; zblázníte se z toho. A poměrně s vysokou pravděpodobností se i dopustíte chyby. Python 2.6 přidává novou vlastnost, která s tímto problémem pomůže a jedná se o context managers.

try:
    with open(fileName, 'a') as f:
        f.write('xyz')
except IOError, e:
    # ...
else:
    # ...

Sice je to v tomto konkrétním případě více psaní, ale už určitě nezapomeneme na zavření streamu. Důvěřujme, ale prověřujme:

f = open('asdf', 'w')
with f:
    f.write('xyz')

f.write('abc')
# ValueError: I/O operation on closed file

Z ukázky si všimněte dvou věcí – první: není potřeba použít as a hlavně té druhé: po vyskočení z bloku with se soubor automaticky zavřel. Zavřel by se, ať už by to skončilo v pořádku či nikoliv (= vyhozením výjimky).

Celé je to možné díky tomu, že file objekt (který vrací funkce open) má implementované speciální metody __enter__ a __exit__. První metoda nastavuje (cokoliv nás napadne) před vstupem do bloku with a vrací to, co se nám zrovna může hodit (a nemusíme to použít, když nepotřebujeme). Druhá metoda se volá po dokončení bloku with a jak už jsme si řekli, zavolá se v jakémkoliv případě – ať už vše proběhlo v pořádku a nebo ne.

S těmito vědomostmi si můžeme vytvořit jakoukoliv třídu, kterou lze využít s klíčovým slovem with. Například jsem si udělal třídu Transaction pomáhající mi s transakcemi, abych je nemusel neustále vytvářet a commitovat, případně rollbackovat. Ukázka je zjednodušená:

class Transaction(object):
    """Transaction object for use in with statement."""

    def __init__(self, dbconnection):
        self._dbconnection = dbconnection

    def __enter__(self):
        self._dbconnection.transaction()
        cur = self._dbconnection.cursor()
        return cur

    def __exit__(self, type_, value, traceback):
        if type_ is None:
            self._dbconnection.commit()
        elif issubclass(type_, DatabaseException):
            self._dbconnection.rollback()
            # ...
        else:
            self._dbconnection.rollback()
            # ...

with Transaction(singleton.dbconnection.master) as cur:
    sql = sqlpuzzle.insertInto('city').values({'name': 'Springfield'})
    cur.execute(str(sql))

Modul sqlpuzzle z ukázky je k nalezení na GitHubu a lze nainstalovat z PyPI příkazem  pypi-install sqlpuzzle.

Poznámka: with můžete použít už od Pythonu 2.5, ale musíte si tuto vlastnost zpřístupnit přes speciální import. Tento import však musí být jako úplně první kód v souboru.

from __future__ import with_statement

Závěr

Tak, a tím jsme ukončili povídání o syntaktických tipech a tricích. Tipy v dalším díle budou ukazovat na možnosti Pythonu – jeho built-in funkce, oficiální moduly a další. Dnes opět zakončím odkazy se zajímavým čtením k dalšímu studiu:

Michal dělá team leadera v Seznam.cz a hraje si na BOObook.cz. Jeho nejoblíbenějším jazykem je Python, ale nevadí mu třeba ani JavaScript a rád zkouší nové jazyky i technologie. Ve volném čase cestuje, fotí, píše, ale taky plave, jezdí na kole či tancuje.

Komentáře: 24

Přehled komentářů

BeryCZ díky
Rbas Generatorove slovniky
Rbas Re: Generatorove slovniky
Domogled díky
jaryH3 Re: Python profesionálně: dynamické parametry, generátory, lambda funkce a with
Havri Více myšlenek
Martin Hassman Re: Více myšlenek
blizz Re: Více myšlenek
Martin Hassman Re: Více myšlenek
ByCzech Re: Více myšlenek
Martin Hassman Re: Více myšlenek
blizz Re: Více myšlenek
Lukas Re: Více myšlenek
Lukas Seriál
kutr Generátor
Radek Miček Re: Generátor
Ales Zoulek Re: Generátor
Honza Kral Re: Generátor
Petr Jediný Re: Generátor
Honza Kral Sort + DRY
PMD Pythonista
RiHL Asi se potřebuju vyspat...
. Re: Python profesionálně: dynamické parametry, generátory, lambda funkce a with
starenka Titulek musí být dlouhý alespoň 4 znaky
Zdroj: http://www.zdrojak.cz/?p=3623