Hlavní navigace

Nový chřestýš (1)

11. 1. 2005
Doba čtení: 8 minut

Sdílet

Prvního prosince se objevila nová verze 2.4 oblíbeného jazyka Python. V následujícím dvoudílném seriálu si popíšeme některé nové vlastnosti této verze. V prvním dílu si ukážeme generátorové výrazy a dekorátory.

Překlad a instalace

Překlad a instalace nového Pythonu 2.4 je nadmíru snadná. Nyní stačí použít pouze „svatou trojici“ ./configure; make; make install a vše proběhne podle očekávání. Již není nutné modifikovat soubor Modules/Setup, protože (téměř) všechny moduly jsou správně nakonfigurovány. Spustíme-li našeho nově získaného chřestýše, uvidíme něco podobného:

Python 2.4 (#1, Dec 28 2004, 12:28:00)
[GCC 2.95.4 20011002 (Debian prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> 

Blízko k funkcionalitě

Z Pythonu, ač jde o jazyk procedurální, je z mnoha stran cítit trocha funkcionální vůně. Vše od funkcí typu map() či filter() přes lambda funkce až po výrazy, jako je stručný seznam (list comprehension), lze najít ve funkcionálních jazycích. Následně k nim přibyly iterátory a ve verzi 2.2 generátory. Konečně ve verzi 2.3 se obejvil modul itertools a verze 2.4 přináší další nášup – stručné generátory (generator expressions).

Tento výraz na první pohled vypadá obdobně jako stručný seznam. Zásadní rozdíl je ale ve funkci. Zatímco stručný seznam vrátí CELÝ vygenerovaný seznam, stručný generátor vrátí pouze iterátor, pomocí něhož se potom list generuje „za běhu“. Mezi stručným seznamem a stručným generátorem je obdobný rozdíl jako mezi obyčejnou metodou a generátorem.

Velice výhodné je jeho použití na místech, kde lze očekávat iterování přes velké množství objektů, ale při dalším zpracování není nutné používat všechny naráz. Následující příklad spočítá součet čísel dělitelných třemi ze seznamu cisla.

>>> cisla = [1, 4, 6, 8, 12, 31, 49, 51, 59, 60, 68, 70]
>>> vyber = (i for i in cisla if i % 3 == 0)
>>> soucet = sum(vyber)
>>> print soucet
129

Stručný generátor je výraz na druhé řádce kódu. Pokud bychom místo něho použili stručný seznam, nejprve by se vygeneroval ze vstupního seznamu cisla nový seznam, jež by obsahoval čísla dělitelná třemi, a následně by se spočítal jejich součet, což by v případě enormní délky seznamu cisla nebylo to pravé ořechové.

Výraz stručného generátoru je uzavřen mezi kulaté závorky (namísto hranatých závorek u stručného seznamu), jinak je jeho syntaxe stejná. V případě, že stručný generátor použijeme rovnou jako argument funkce, nemusíme další pár závorek uvádět:

>>> print sum(i for i in cisla if i % 3 == 0)
129

Nezapomeňte na to, že výrazy obsažené v generátoru se vyhodnocují až v případě potřeby, je tedy nutné dát pozor na vznik chyb. Pokud totiž v generátoru použijeme neexistující proměnnou, generátor vznikne a k chybě dojde až při jeho použití.

>>> vyber = (i for i in cisla if j % 3 == 0)
>>> soucet = sum(vyber)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 1, in <generator expression>

NameError: global name 'j' is not defined

Poslední upozornění se týká řídící proměnné. Ve verzi 2.4 a nižších po skončení stručného seznamu zůstane řídící proměnné (popř. proměnným) přiřazena poslední hodnota. Řídící proměnná stručného generátoru je však definována pouze uvnitř tohoto generátoru a po jeho skončení je nepřístupná:

>>> slova = ['Fakulta', 'aplikovanych', 'ved']
>>> print ''.join([i[0].upper() for i in slova])
FAV
>>> print i
ved
>>> print ''.join(j[0].upper() for j in slova)
FAV
>>> print j
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: name 'j' is not defined

Toto chování je logickým důsledkem „línosti“ stručného generátoru. Vykonáním výrazu generátoru se pouze vytvoří nový objekt, ale není vyhodnocen žádný výraz a ani řídící proměnné není přiřazena žádná hodnota. V budoucnu se počítá s úpravou chování stručného seznamu tak, aby kopíroval chování svého funkcionálního bratříčka.

Dekorátory

V Pythonu verze 2.2 spolu se zavedením tzv. „new-style“ tříd přibyla možnost definování statických a třídních metod pomocí funkcí staticmethod() a classmethod(). Tyto metody přebírají odkaz na metodu nějaké třídy a vrátí novou metodu s požadovaným chováním. Např:

>>> class retezec(str):
...     def ze_souboru(cls, jmeno):
...         f = file(jmeno)
...         ret = cls(f.read())
...         f.close()
...         return ret
...     ze_souboru = classmethod(ze_souboru)
...
>>> s = retezec.ze_souboru('/etc/fstab')
>>> print s[:40]
# /etc/fstab: static file system informa

Jak je vidět, modifikace funkcí classmethod() se musí provést až po definici metody. Tím může dojít k různým opomenutím a k nechtěným chybám. Proto Python ve verzi 2.4 přináší tzv. dekorátory. Kolem tohoto doplnění jazyka se vedla velká diskuze a mnoho lidí ji dodnes nepřeneslo přes srdce. Je ovšem třeba dát za pravdu hlasům, které říkají, že zavedení dekorátorů zpřehlední samotný zápis kódu a jeho následnou údržbu. Výše uvedený příklad by s použitím dekorátorů vypadal následovně:

>>> class retezec(str):
...     @classmethod
...     def ze_souboru(cls, jmeno):
...         <telo funkce>
...
>>> s = retezec.ze_souboru('/etc/fstab')

Jak je vidět, transformace přiřazením ze_souboru=clas­smethod(ze_sou­boru) se zapíše pomocí dekorátoru. Dekorátor – jméno funkce – je uveden za znakem @. Dekorátorů může být i více a jsou uvedeny PŘED samotnou funkcí, což zvyšuje čitelnost výsledného kódu.

Pro jednu funkci lze použít i více dekorátorů. Jako jednoduchou ukázku vytvoříme dva dekorátory, dvojnasobek() a odmocnina(), které budou po aplikování na určitou funkci modifikovat její návratovou hodnotu tak, že vytvoří její dvojnásobek resp. odmocninu:

import math

def dvojnasobek(func):
  def wrapper(*args, **kwargs):
    ret = func(*args, **kwargs)
    return ret*2
  return wrapper

def odmocnina(func):
  def wrapper(*args, **kwargs):
    ret = func(*args, **kwargs)
    return math.sqrt(ret)
  return wrapper

@dvojnasobek
def f1():
  return 3.0

@odmocnina
def f2():
  return 3.0

@odmocnina
@dvojnasobek
def f3():
  return 1.0

@dvojnasobek
@odmocnina
def f4():
  return 1.0

print f1()
print f2()

Výstupem programu bude následující výpis:

6.0
1.73205080757
1.41421356237
2.0

Nyní si popíšeme funkci těchto dekorátorů. Jak již bylo řečeno, dekorátor je vlastně funkce vracející novou funkci. Díky tomuto „obalu“ je možné různými způsoby modifikovat chování původní funkce. V souladu s výše uvedeným naše dekorátory vracejí novou funkci se jménem wrapper(), která je definována v jejich těle. Samotný dekorátor přebírá jediný argument – funkci, jež se má dekorovat.

Parametr func přejde do vnořené funkce wrapper() pomocí mechanismu vnořených oborů jmen (nested scopes). Poté se při volání f1() vlastně vykoná volání odpovídajícího wrapperu. Bez použití dekorátorů by se definice a volání funkce f1() dalo zapsat jako:

def f1(): ...
f1 = dvojnasobek(f1)
f1()

V případě použití více dekorátorů je situace obdobná, ovšem dojde k dekorování více funkcí najednou. Zápisy definice a volání funkce f3() by bez použití syntaxe dekorátoru vypadaly takto:

def f3(): ...
f3 = dvojnasobek(odmocnina(f3))
f3()

Dekorátory mohou mít i své parametry. V tomto případě funkce reprezentující dekorátor převezme tento parametr a vrátí nový dekorátor odpovídající tomuto parametru. Jde dle mého názoru o poněkud krkolomnou implementaci, nicméně svůj účel splní. Ukážeme si vykonstruovaný příklad, jak se na základě parametru rozhodovat, zda vytvořit statickou, nebo třídní metodu:

def method(type):
  if type == 'static':
    return staticmethod
  elif type == 'class':
    return classmethod
  else:
    raise ValueError

class C(object):
  @method('static')
  def staticka():
    print 'staticka metoda'

  @method('class')
  def tridni(cls):
    print 'tridni metoda tridy %s' % cls.__name__

C.staticka()
C.tridni()

Výstup programu vypadá následovně:

staticka metoda
tridni metoda tridy C

Dekorátor meth() na základě svého parametru type vrací buď dekorátor staticmethod(), nebo classmethod(), který je následně aplikován na metodu C.staticka() resp. C.tridni().

Je-li v určitém místě kódu třeba kontrolovat, popř. unifikovat typ parametrů nějaké metody, lze k tomu s úspěchem použít právě dekorátory. Následující ukázka kódu vytvoří dekorátor, jemuž lze předat funkce pro konverzi parametrů. Chování demonstrujeme na příkladu moje_funkce(), která od dekorátoru obdrží první parametr zkonvertovaný na typ bool, druhý na typ seznam a třetí nechá bez úpravy.

NoUni = lambda v: v   # necha parametr beze zmeny

def uniargs(*uni):
  def create_wrapper(func):
    def wrapper(*args):
      new_args = map(lambda u,a:u(a), uni, args)
      return func(*new_args)
    return wrapper
  return create_wrapper

@uniargs(bool, list, NoUni)
def moje_funkce(b, l, i):
  print b
  print l
  print i

moje_funkce(1, (1,2), 30)
print '-'*10
moje_funkce([], 'jedna', None)

Výstup z programu:

True
[1, 2]
30
----------
False
['j', 'e', 'd', 'n', 'a']
None

Jak vidíme, náš dekorátor uniargs() pracující s obecnými hodnotami parametrů obsahuje konkrétní dekorátor create_wrapper(), jenž konečně vrací wrapper() modifikující parametry. Nakonec si uvedeme ještě jednoduché rozepsání definice moje_funkce() bez použití dekorátorů.

def moje_funkce(): ...
_muj_dekorator = uniargs(bool, list, NoUni)
moje_funkce = _muj_dekorator(moje_funkce)

Je třeba podotknout, že každý dekorátor musí být zapsán na nové řádce. Osobně nechápu, proč je to požadováno. Stejně tak by bez ztráty čitelnosti mohly být dekorátory na jedné řádce následovány definicí funkce na řádce druhé. Dokonce dokument „What's New in Python 2.4“, který je součástí dokumentace nového Pythonu, obsahuje ukázku, v níž jsou použity tři dekorátory na jedné řádce. Takováto inkonzistence vypovídá cosi o horké jehle, i když výsledek je bezesporu užitečný.

Závěrem

Obě výše probíraná rozšíření jistě přinášejí mnoho nového. Názor vývojářské komunity nebyl při jejich implementaci jednotný. Je zajímavé začíst se do různých PEPů (Python Enhancement Proposals), což jsou víceméně specifikace nových vlastností.

root_podpora

Velice zajímavé je třeba přečíst si starší revize PEP 289 (okolo počátku roku 2002) věnovaného stručným generátorům. Původně se zamýšlelo konstruovat stručné generátory stejným způsobem jako stručné seznamy rozšířené o klíčové slovo yield. Samotný autor Pythonu Guido van Rossum byl proti, neboť se domníval, že generátory jsou pouze okrajovou záležitostí a posunulo by to Python směrem k funkcionálním jazykům. A jak je vidět, rok se s rokem sešel a Python byl o generátorové výrazy obohacen.

V příštím dílu si povíme něco o dalších vlastnostech Pythonu 2.4, jako jsou množiny, decimální datový typ a především modul pro testování kódu – doctest.

Byl pro vás článek přínosný?