Tomáš Profant

Ústav mechaniky těles, mechatroniky a biomechaniky, odbor lomové mechaniky a mesomechaniky materiálů
místnost: A2/708
telefon: +420 54114 2891
e-mail: profant@fme.vutbr.cz
Zpět na úvod

Funkce a jejich dekorátory v Pythonu

O Pythonu (ostatně jako o mnoha jiných věcech) je zcela zbytečné cokoliv psát, tisíce a tisíce řádků o něm jsou totiž na webu. Nicméně, ne vždy je to srozumitelné i pro „lajky“. Ještě když navíc daný pisatel z tak praktické věci, jakou programování je, začne dělat teoretickou vědu. Dlouhou dobu mi takto unikal tzv. dekorátor funkce (on existuje i dekorátor tříd a kdo ví, čeho všeho ještě), ale narazil jsem na stránky http://simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/, kde je tedy velice pěkně popsán. Takže z toho blogu takový výtažek a pár poznámek.
Vytvořit funkci je velice jednoduché, např.

>>> def foo():
...   return 1

Zavoláním této funkce

>>> foo()

vypadne jednoduše číslo 1. Je super, že se člověk nemusí zabývat globalitou a lokalitou proměnných, jak ukazuje následující příklad.

>>> a,b=1,2
>>> def foo(c):
...   a,d=3,4
...   print a,b,c

Proměnné a a b jsou globální. Proměnné c, d a a definované v proceduře jsou lokální a platí jen v těle procedury a po provedeni procedury jsou zapomenuty. To znamená, že zavolání procedury, např. takto,

>>> foo(5)

vypíše čísla 3, 2 a 5. Tedy globální proměnné mohou být použity uvnitř procedury (proměnná b=2), aniž by změnily hodnotu, nebo mohou být lokálně přepsány (proměnná a). Avšak lokální a=3 a globální a=1 jsou z pohledu Pythonu dvě rozdílné proměnné. Lokální proměnná a=3 je po provedení procedury nenávratně zapomenuta a platná zůstane jen její globální varianta a=1.
Argumenty funkce lze také vkládat různými způsoby. Mějme funkci,

>>> def foo(x,y=0):
...   return x-y

ve které je „difóltně“ nastavena hodnota 0 pro proměnnou y. Po zavolání

>>> foo(3,1)

je však hodnota 0 u y přepsána na 1 a funkce vrátí hodnotu 2. Po zavolání

>>> foo(3)

funkce vrátí hodnotu 3, protože druhá pozice v závorce je nezadaná a bere tedy y jako nezadané a vezme jeho „difóltní“ hodnotu 0. Dodržení pořadí argumentů není nutné dodržovat, je ale nutné při volání funkce říct, která je která. Zavolání funkce

>>> foo(y=1,x=3)

vypíše tedy hodnotu 2. V Pythonu je všechno objekt, včetně proměnných a funkcí samotných. Funkce pracují s objekty, takže nejen proměnné, ale i samotné funkce mohou být lokálně definovány v těle procedury se všemi výše popsanými vlastnostmi, jak je vidět v dalším příkladu.

>>> def outer():
...   x=1
...   def inner():
...     print x
...   inner()

Po zavolání

>>> outer()

se vypíše hodnota 1 proměnné x, přičemž x a procedura inner jsou následně zapomenuty. Nejdříve se v proceduře outer lokálně nadefinuje x, pak také lokálně procedura inner a poté je inner zavolána a provede se. Proměnná x je globální vzhledem k inner, ale lokální vzhledem k outer. Dále, procedura inner se vytváří pokaždé znovu, jakmile je procedura outer opětovně zavolána. To je vcelku důležité v dalších příkladech. Zatím však ukázka klasického chování funkcí jako argumentů v jiných funkcích.

>>> def add(x,y):
...   return x+y
>>> def sub(x, y):
...   return x-y

>>> def apply(func,x, y):
...   return func(x,y)

Procedura add resp. sub vrací součet resp. rozdíl dvou čísel. Mohou se samozřejmě použít jako argumenty jiné funkce nebo procedury, což umožňují i primitivnější programovací jazyky než Python. Takže po zavolání

>>> apply(add, 2, 1)

resp.

>>> apply(sub, 2, 1)

vypadnou hodnoty 3 resp. 1. Nic nového pod sluncem. Co se však stane, když je jako hodnota vrácena samotná funkce, ne jen její hodnota?

>>> def outer():
...   def inner():
...     print "Inside inner"
...   return inner

Stane se málo (na první pohled), ale jak je výše poznamenáno, vše v Pythonu je objekt, včetně funkce či procedury, tak není důvod v rámci filozofie Pythonu s nimi zacházet jinak než jako s jinými objekty, tj. čísly apod. Tzn. že by neměl být problém přiřadit takovýto „funkcionální“ výstup nějaké proměnné. Což samozřejmě problém není.

>>> foo=outer()

Avšak po zavolání

>>> foo

vypíše Python hlášku, že jde o funkci. Teprve až po vypsání

>>> foo()

se provede přiřazená procedura outer, tj. vypíše se text Inside inner. Prostě a jednoduše, přiřazením procedury outer proměnné foo jsme vytvořili funkci foo, proto ty závorky při volání proměnné foo. Jinými slovy jsme funkci inner uložili do proměnné foo. Geniální! Tahle vlastnost umožňuje vygenerovat procedury třeba následovně.

>>> def outer(x):
...   def inner():
...     print x
...   return inner

Nyní se přiřadí procedura inner pro různé hodnoty x různým proměnným.

>>> print1=outer(1)
>>> print2=outer(2)

A po jejich zavolání

>>> print1()

resp.

>>> print2()

vypadnou hodnoty 1 resp. 2. Neodborně řečeno, funkce inner se „zakonzervovala“ do proměnných (funkcí, procedur) print1 a print2 vzhledem k lokální proměnné x. Jak ale bylo zmíněno výše, x je lokální vzhledem k outer, ale globální vzhledem k inner. Tahle vlastnost vložených funkcí je nějaký terminus technicus - Closure. Je to však vlastnost, která spolu s klasickou vlastností použití funkce jako argumentu v jiné funkci, umožňuje upravovat tyto funkce v argumentu pomocí inner v podstatě za chodu programu, prostě je ozdobit a hrát si s nimi - dekorovat. Dekorovaná funkce vstoupí jako argument do procedury outer, kde bude jako globální proměnná vzhledem k inner, která ji zavolá a výsledek upraví - dekoruje. Nakonec funkce outer vrátí inner jako funkci. Jako příklad si vezměme elipsu v rovině, kterou chceme natočit o libovolný úhel. Definice elipsy,

>>> from math import cos,sin
>>> def elipsa(u,coefs):
...   rx,ry=1/coefs[0],1/coefs[1]
...   x=rx*cos(u)
...   y=ry*sin(u)
...   return (x,y)

která pro u z intervalu (0, π) vrací souřadnice x a y elipsy o poloměrech rx a ry. Nyní tuto funkci dekorujeme tak, že funkce bude vracet hodnoty elipsy natočené o hodnotu phi. Nejdříve ale dekorátor samotný.

>>> def natoceni(krivka,phi=0):
...   mC=((cos(phi),-sin(phi)), \
...   (sin(phi),cos(phi)))
...   def inner(u,coefs):
...     v=krivka(u,coefs)
...     v1=(mC[0][0]*v[0]+mC[0][1]*v[1], \
...     mC[1][0]*v[0]+mC[1][1]*v[1])
...     return v1
...   return inner

Jestliže napíšeme

>>> x,y=elipsa(0.0,(1.0,2.0))
>>> x,y

dostaneme pro x a y hodnoty 1.0 a 0.0. Nyní funkci elipsa dekorujme, tedy

>>> from math import pi
>>> elipsa=natoceni(elipsa,pi)

a zavolejme si elipsa znova, stejně jako předtím.

>>> x,y=elipsa(0.0,(1.0,2.0))
>>> x,y

Světe div se, z proměnných x a y vypadne -1 a 0, což jsou souřadnice stejného bodu elipsy, ale otočené o úhel π. Co se tedy stalo? Při dekorování vstoupila funkce krivka=elipsa jako parametr (a tedy globální proměnná z pohledu vnitřní funkce inner) do funkce natoceni spolu s proměnnou phi=pi. Z výše napsaného plyne, že přiřazení dekorátoru natočení opětovně funkci elipsa způsobí „zakonzervování“ vnitřní funkce inner vzhledem k těmto vstupním parametrům zpět do proměnné (funkce) elipsa. Funkce elipsa je teď vlastně předefinovaná. Když se nyní zavolá se stejnými parametry použitými u její nedekorované varianty, tedy u=0.0 a coefs=(1.0,2.0), převezme si tyto parametry funkce inner, která si vyjádří souřadnice bodu elipsy v pomocí původní funkce elipsa, transformuje je pomocí matice mC a vrátí jako v1 a na řádku return inner ji předá dekorované funkci elipsa. Funkci lze dekorovat přímo při její definici pomocí symbolu.

>>> @dekorátor
... def funkce_k_dekorování():
...   tělo funkce

V Pythonu, podobně jako v jiných programovacích jazycích (umí to dokonce i Fortran), je možné předávat funkci či proceduře i před samotnou definicí funkce neznámé množství parametrů. Parametr reprezentující místo, odkud je pozice parametrů a množství parametrů libovolná, je označen *, viz následující příklad.

>>> def one(*args):
...   print args

Pokud se zavolá

>>> one()

vypíše se prázdná závorka (), pokud se zavolá

>>> one(1,2,3)

vypíše se (1, 2, 3). Další způsob použití ukazuje následující příklad.

>>> def two(x,y,*args):
...   print x,y,args

Zavoláním

>>> two(’a’,’b’,’c’)

se vypíše a b (’c’,). Hvězdička * před argumentem může být použita k rozbalení argumentu, jak ukazuje další příklad.

>>> def add(x,y):
...   return x+y

Funkci je možné volat dvěma způsoby.

>>> lst=[1,2]
>>> add(lst[0],lst[1])

První je klasický a vyhodí číslo 3.

>>> add(*lst)

Druhý je pomocí hvězdičky * a vypíše samozřejmě také 3. V případě slovníku se musí použít dvě hvězdičky.

>>> def foo(**kwargs):
...   print kwargs

Po zavolání

>>> foo()

se vypíše {}. Při zavolání ve tvaru

>>> foo(x=1,y=2)

se vypíše slovník {’y’: 2, ’x’: 1}. A podobně jako u seznamu, i v tomto případě lze při předávání argumentu funkci použít dvě hvězdičky ** k rozbalení slovníku, jak ukazuje další příklad.

>>> dct={’x’:1,’y’:2}
>>> def bar(x,y):
...   return x+y

Po zavolání funkce bar pomocí argumentu s ** se dostane výsledek x+y, tj. 3.

>>> bar(**dct)

Tato vlastnost libovolného množství argumentů se dá využít při dekorování funkcí. Ono totiž nastavení fixovaného množství parametrů ve vnitřní funkci inner dekorátoru je značně omezující a dekorátor je možné zobecnit následovně.

>>> def natoceni(krivka,phi=0):
...   mC=((cos(phi),-sin(phi)), \
...   (sin(phi),cos(phi)))
...   def inner(*args,**kwargs):
...     v=krivka(*args,**kwargs)
...     v1=list(v)
...     v1[0]=mC[0][0]*v[0]+mC[0][1]*v[1]
...     v1[1]=mC[1][0]*v[0]+mC[1][1]*v[1]
...     return v1
...   return inner

Tímto způsobem je možné dekorovat (natočit) nejen elipsu elipsa tak, jak je uvedeno výše, ale třeba i elipsoid, který může být definován následovně.

>>> def elipsoid(u,v,coefs):
...   rx,ry,rz=1/coefs[0],1/coefs[1],1/coefs[2]
...   x=rx*cos(u)*sin(v)
...   y=ry*sin(u)*sin(v)
...   z=rz*cos(v)
...   return (x,y,z)

Funkce elipsoid má oproti funkci elipsa o argument v navíc. To však nevadí, i tak ji můžeme dekorovat pomocí dekorátoru natoceni, protože jeho funkce inner má jako argumenty hvězdičkované *args a **kwargs. To znamená, že dekorování funkce,

>>> elipsa=natoceni(elipsa,pi/2.)

po jejím zavolání

>>> x,y=elipsa(0.0,(1.0,2.0))
>>> x,y

místo původního (nedekorovaného) bodu x=1.0 a y=0.0 vypíše x=0.0 a y=1.0. Tedy otočí elipsu o π ⁄ 2. A stejně tak se může stejným dekorátorem dekorovat funkce elipsoid, i když s jiným množstvím argumentů.

>>> elipsoid=natoceni(elipsoid,pi/2.)

A jeho zavoláním,

>>> x,y,z=elipsoid(0.0,pi/2.0,(1.0,2.0,3.0))
>>> x,y,z

vrátí místo původních (nedekorovaných) souřadnic bodu elipsoidu x=1.0, y=0.0 a z=0.0 hodnoty souřadnic bodu otočeného v rovině xy o úhel π ⁄ 2, tj. x=0.0, y=1.0 a z=0.0.
To je v podstatě vše. Dekorování funkcí je vcelku zbytečná záležitost, člověk se bez ní v pohodě obejde. Zpestřuje však Python a dělá ho zase o něco zábavnější.
Zpět na úvod