Přejít k navigační liště

Zdroják » Různé » Python profesionálně: dynamické parametry, generátory, lambda funkce a with

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

Články Různé

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.

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:

Komentáře

Subscribe
Upozornit na
guest
24 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
BeryCZ

fakt díky,dobrý článek… jen bych dodal, že kwds se (možná i častěji) pojmenovává jako kwargs

a jsem opravdu rád za dictionary comprehensions v py3.x :) super, díky

Rbas

Ahoj, generatorove slovniky jsou zavedeny jiz ve verzi 2.7.

Rbas
Domogled

Moc děkuji za tento seriál. Dneska to pro mne bylo sice spíše opakování známého, ale zase v minulém dílu byly triky, které jsem neznal a které se mi líbily. Těším se na další pokračování a ať Vám chuť do psaní vydrží.
Petr

jaryH3

Na contextmanagery se výborně hodí yield, try/finally a contextlib:

http://docs.python.org/library/contextlib.html#contextlib.contextmanager

Havri

Pokud lze ještě nějak ovlivnit obsah budoucích článků, tak bych chtěl poprosit, aby to bylo více zaměřené na myšlenky, best practice nebo zkrátka něco, co je více vzdáleno syntaxi jazyka. Seriál zatím na mě působí jako ukecanější seznam „tipů a triků” pro Python a to si můžu přečíst jinde. Na Stack Overflow je jich celá hromada a zatím se v těchto dvou dílech neobjevilo snad nic, co by nebylo i v tomto seznamu.

http://stackoverflow.com/questions/101268/hidden-features-of-python

Martin Hassman

První dva díly seriálu byly zaměřené na syntaxi a dle ohlasů to čtenářům pomohlo, takže se i nadále tomuhle přístupu nijak striktně vyhýbat nebudeme. Další díly jsou už posunuté jinam, uvidíme, zda vyhoví i vám.

blizz

a syntax highlighter nebude? alebo je taky problem pridat par riadkov js kodu? ak ano tak mi dajte pristup na ftp a ja to updatnem aj sam.

Martin Hassman

Highlighter bude. Co znamená onen *taky* problém? S tím ftp je to v dnešní době už trochu naivní, ten tu nepěstujeme 8-)

ByCzech

Slovensky „taký“ je česky „takový“ ;-)

Martin Hassman

Ach tak. Na slovenčinu bez diakritiky si na příště budu muset pořídit tlumočníka 8-)

blizz

Tak to je super.. vďaka.

Lukas

Osobně mi stávající přístup zcela vyhovuje. Stack Overflow je sice web ke nezaplacení, ale domnívám se, že formát, který je zde zvolený, je ideální (detailnější vysvětlování, popisnost, dobře volené příklady).

Lukas

Díky za perfektní seriál, těším se na další článek :-)

kutr

Já bych měl dotaz k těm generátorům. Je to speciální pojem v Pythonu nebo se to obvykle používá? Já to znám z jiných jazyků pod anglickým termínem „list comprehension“ a už dlouho hledám český termín.

Radek Miček

List comprehension je něco jiného než generátor.

Ales Zoulek

Ta terminologie je tam trosku zmatena.

– *Iterator* je objekt, ktery ma specialni API (hlavne __inter__ metoda), diky kteremu jde vkladat jednoduse od for cyklu (apod.) Tzn
for x in my_custom_iterator:
pass

– *Generator funkce*, ktera zjednodusuje vyvareni interatoru tak, ze nemusite definovat celou tridu, pouzice fci, ktera ma yield.
def my_custom_ite­rator():
yield 1
yield 2

– *Generator expression* je dalsi zjedoduseni, kde neni treba pouzivat ani funkci, pouze vyraz kratky vyraz ve stylu:
my_custom_iterator = (i for i in …)

Tzn. oboje je v zasade syntactic sugar, jak vytvorit objekt s iterator API.

– *List comprehension* je zjednoduseny zapis na vytvareni listu.
[ x for x in … ]

Interatory to jsou v zasade vsechno.
Za generatory se obvykle oznacuje generator expression, nebo generator fce. A dava se tim (podle kontextu) najevo, ze nejde o list comprehension (apod) a tedy ze se nevygeneruji vsechny prvky predem, ale generuji je postupne s kazdym pruchodem cyklu.

Honza Kral

Souhlas, clanek je trochu zmateny co se tyce rozdilu mezi iterator, iterable, generator, generator expression a list/dict comprehension.

Jinak pro upresneni (kdyz uz jsem za hnidopicha ;) ): __iter__ je magicka metoda ktera ma vracet iterator (je volana built-in funkci iter()), iterator je v podstate cokoliv co ma metodu .next() ktera vraci prvky a vyhodi StopIteration na konci. Iterable je pak cokoliv co ma metodu __iter__. V praxi to funguje tak, ze kdyz napisu

for x in cokoliv

tak se na cokoliv zavola __iter__() a na vracene hodnote se vola .next() do te doby dokud nenastane vyjimka StopIteration a predava se do promenne x.

Petr Jediný

Ještě malé doplnění – v Pythonu verze 3 byla Honzou zmíněná metoda next přejmenována na __next__

Honza Kral

Diky za clanek, pridavam par pripominek a doplnujicich informaci.

Prosim nepouzivejte parametr cmp pro sort pokud to neni absolutne nutne. Parametr key dokaze v 99.9% pripadu dosahnout toho sameho jednoduseji a efektivneji (je zavolany jen jednou na kazde hodnote v listu):

dates.sort(ke­y=lambda x: x.isoweekday())

casto ani neni potreba psat vlastni lambdu a da se pouzit operator.attrgetter a operator.item­getter, navic v pythonu 3.X uz cmp= parametr u sort funkci ani neni. Vice info na: http://wiki.python.org/moin/HowTo/Sorting

Autor spravne kritizuje ze neni dobre kopirovat na vsechna mista kde se pouziva nejaky iterator nejaky kod a pak navrhuje zlepseni ktere spociva v tom ze na dana mista nakopiruje jiny kod – obalujici funkci. To je zajiste zlepseni ale stale to neni v souladu s „DRY“ – v tomhle pripade by bylo mnohem vhodnejsi naimplementovat lazy loading tak, aby kdyz nekdo pristoupi na atribut (ci cokoliv jineho) na objektu ktery neni nacten, tak se objekt dotahne. Tim padem se programator nemusis starat o obalovani kodu vsude (coz je znacny prohresek proti DRY) a zaroven se tak zaruci to ze se nikdy nedotahuje nic zbytecne.

PMD

Nechci autora urazit, ale připadá mi, že s Pythonem zas tak moc velké zkušenosti nemá. Nemá jasno v terminologii, zvyklostech (kwargs), ani best-practices. Nezná Honzu Krále:) Jinak článek je to docela dobrý, pěkně se čte, rád si osvěžím svoje znalosti, ale pro někoho, kdo si nedokáže opravit drobné nepřesnosti, může být zavádějící.

RiHL

Asi se potřebuju vyspat a přečíst si to zejtra znova O_o

.

Zřejmě ještě stále nejste v Pythonu zběhlý.

starenka

Jestli je soubor zavrenej se imo cistejc zjistuje pomoci f.closed a ne tim, ze se do nej pokousim zapsat ;)

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.