Python profesionálně: metatřídy

Python logo

V předchozích dílech tohoto seriálu jsme si řekli spoustu zajímavých tipů, jak vyvíjet v Pythonu lépe a rychleji. Od syntaktických tipů přes různé tipy na vestavěné funkce, moduly atp. až po zajímavé řešení některých návrhových vzorů. Zbývá už jen poslední a pravděpodobně nejnáročnější povídání – o metatřídách. Co to vlastně je, jak se tvoří a kde se dají využít.

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

Hned na úvod: pokud se momentálně moříte s nějakým návrhem aplikace, metatřída vaše spása nebude. Většinou metatřída není řešením. Minimálně pro běžné aplikace, metatřídy se hodí spíše pro nějaké knihovničky, vychytávky a tak podobně. Ale doporučuji si o nich něco málo zjistit, i když to využijete málo či vůbec, abyste pochopili samotný Python nebo minimálně cizí kód, kde jsou metatřídy použité.

Jednu metatřídu jsme už v seriálu použili, bylo to pro snadnější zápis návrhového vzoru singleton. Připomeňme si kód, který se budeme snažit dnes pochopit:

class SingletonMeta(type):
    def __new__(cls, classname, bases, classdict):
        classdict.setdefault('__slots__', ())
        newcls = type.__new__(cls, classname, bases, classdict)
        return newcls()

class singleton(object):
    __metaclass__ = SingletonMeta

Trocha teorie

Konstruktor

Než se ale dáme do zjišťování, co se v ukázce děje, musíme si chvilku povídat. Začněme metodou __init__  – kdo ji nazývá konstruktor? Prosím, nesmějme se těm, kdo se právě hlásí. Všichni jsme ji tak dříve nazývali. Metoda __init__ není konstruktor. Je to z velmi jednoduchého důvodu – __init__ přece žádnou instanci nevytváří. Tu už dostává v parametru jako všechny ostatní instanční metody. Důkazem nechť je dokumentace: „Called when the instance is created.“

Co by to ale bylo za programovací jazyk, kdyby zde konstruktor nebyl. Ba je, a je skryt v metodě __new__. Tato metoda skutečně něco vytváří a není to klasická instanční, nýbrž statická metoda. Ale žádné obavy, je to automatické a nemusíme sami metodu dekorovat, že se jedná o statickou.

Metoda __new__ přijímá jeden parametr, a to třídu, kterou máme zkonstruovat. V této metodě vytvoříme novou instanci, upravíme, jak je třeba a poté vrátíme. Vrátit nově vytvořenou instanci je důležité! Jinak se defaultně vrátí (jak už to v Pythonu bývá) None hodnota a tím se instance nevytvoří, tím se ani nevyvolá metoda __init__ (zcela logicky) a prostě nic nemáme.

>>> class C(object):
...     def __new__(cls):
...         print "new"
...     def __init__(self):
...         print "init"
...     def f(self):
...         print "f"
...
>>> c = C()
new
>>> c.f()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'f'

# vs.

>>> class C(object):
...     def __new__(cls):
...         print "new"
...         return super(C, cls).__new__(cls)
...     def __init__(self):
...         print "init"
...     def f(self):
...         print "f"
...
>>> c = C()
new
init
>>> c.f()
f

Vnímavější může namítnout „ale vždyť v ukázce (SingletonMeta) jsou parametry čtyři!“ Nebojte, dostaneme se k tomu.

type

Co to je vlastně type? Je to buď funkce, která nám sděluje typ předávaného objektu, nebo to je standardní metatřída (něco jako object pro třídy), a nebo to je funkce, která nám umožňuje dynamicky vytvářet třídy. A nebo všechno dohromady.

>>> class C:
...     pass
...
>>> type(C())
<type 'instance'>
>>> type(C)
<type 'classobj'>

>>> class C(object):
...     pass
...
>>> type(C())
<class '__main__.C'>
>>> type(C)
<type 'type'>

>>> type(type)
<type 'type'>

Z ukázky je dobře patrná změna ze starých tříd na ty nové objektové a taky provázanost objectu s typem. Instance třídy C je typu třída C a třída C je typu metatřída type. Metametatřída už neexistuje, takže type zůstává  typem.

Zkusme si vytvořit třídu dynamicky (tj. ne přes klíčové slovo class). Použijeme, jak jsme si řekli, funkci type, jen nyní jako vstup použijeme víc a jiné parametry. Jako první předáme název třídy, poté tuple se seznamem, z čeho naše nová třída bude poděděna, a nakonec třídní slovník s atributy nové třídy.

>>> C = type('C', (), {'f': lambda self: 'Hello!'})
>>> c = C()
>>> c.f()
'Hello!'

Pokud se pozastavujete nad tím, proč se musí říct název, když stejně pak musím vytvořit proměnnou s referencí (když už ta proměnná má název), tak je to právě kvůli tomu. Proměnná je proměnná a třída je třída. Vždyť přece lze i normálně udělat toto:

class C(object):
    pass

D = C

Zde je taky proměnná D, ale třída se jmenuje C.

Demonstrace metatřídy

S touto znalostí se můžeme pustit do ukázky první metatřídy.

class M(type):
    def __new__(cls, classname, bases, classdict):
        return type.__new__(cls, classname, bases, classdict)

class C(object):
    __metaclass__ = M

Tato ukázka je vlastně zbytečná, protože takhle to funguje defaultně, pokud se dědí z  object u (samozřejmě se žádná M metatřída nevytváří, použije se type). Ale je to dobré pro ukázku, jak to vlastně pracuje. Každá třída poděděná z  object u má tak i nějakou metatřídu, nejčastěji type, která vlastně říká, jak se má třída vytvořit a chovat. Podobně jako to říká třída svým instancím.

Myslím, že dobře to lze pochopit taky takto: pomocí třídy se vytvářejí instance a pomocí metatřídy se vytvářejí třídy. Metoda __new__ ve třídě ( object) vytváří nové instance a metoda __new__ v metatřídě ( type) vytváří nové třídy. Není to úplně pravda, ale pro pochopení se to tak dá vysvětlit.

Analogicky funguje metoda __init__. Ve třídě ji všichni známe a v metatřídě přijímá 3 (resp. 4) parametry stejně jako výše popisovaná metoda  __new__.

Metametody

V metatřídě můžeme definovat metody (metametody), které potom získá i třída, která používá danou metatřídu. Instance se však k těmto metodám nedostane.

>>> class M(type):
...     def metamethod(cls):
...         return "metamethod of %s" % cls.__name__
...
>>> class C(object):
...     __metaclass__ = M
...     @classmethod
...     def classmethod(cls):
...         return "classmethod of %s" % cls.__name__
...
>>> c=C()

>>> print M.metamethod()
TypeError: unbound method must be called with M instance as first argument (got nothing instead)
>>> print M.metamethod(C)
metamethod of C
>>> print C.metamethod()
metamethod of C
>>> print c.metamethod()
AttributeError: 'C' object has no attribute 'metamethod'

>>> print C.classmethod()
classmethod of C
>>> print c.classmethod()
classmethod of C

Vhodná funkčnost se ukazuje většinou na srandičkách typu předefinování __str__ a ukážu to na tom také, protože jsem se zatím s jiným vhodným využitím nesetkal (nebo setkal, ale neuvědomil si to).

>>> class M(type):
...     def __str__(cls):
...         return '<trida %s>' % cls.__name__
...
>>> class C(object):
...     pass
...
>>> str(C)
"<class '__main__.C'>"
>>>
>>> class C(object):
...     __metaclass__ = M
...
>>> str(C)
'<trida C>'

Shrnutí definice

Pokud trochu tápete, prostě si pamatujte, že třídy jsou definice pro instance a metatřídy jsou definice pro třídy. Zbytek se dostaví časem samo.

Rozbor metatřídy  SingletonMeta

Nyní, když už máme potřebné informace, se už můžeme dát do rozboru metatřídy pro náš singleton.

Pro připomenutí, co bylo potřeba pro náš singleton ručně zařídit: definovat třídu singletonu (samozřejmě), v ní definovat sloty, aby nešlo jen tak všemožně editovat (nejlépe zakázat vše a zkonfigurovat při vytvoření), a definici přepsat jedinou instancí. Díky metatřídě tuto celou funkčnost můžeme zapouzdřit do znovupoužitelného kódu a na detaily nemyslet. A stačí na to jediná metoda.

Co vlastně ta metoda dělá? To co jsme si řekli. Definici hlavičky pravděpodobně mohu přeskočit, další řádek pouze vezme slovník s třídními atributy a nastaví mu nový atribut __slots__ s prázdným tuplem, aby nešlo nic nastavovat. Díky použití metody setdefault nebude případně již nastavený atribut přepsán. Na dalším řádku zavoláme skutečnou továrničku na třídy, nic extra. A v posledním kroku vytvoříme novou instanci nově vytvořené třídy a rovnou ji vrátíme. Díky tomu, že nevracíme nově vytvořenou třídu, ale rovnou její jednu instanci, získáme po napsání třídy s touto metatřídou instanci (a definice je skryta pouze v atributu __class__ naší nové instance).

A to je celé kouzlo. :-)

Komplikovanější problém

Abychom metatřídám porozuměli ještě lépe, zkusíme vyřešit jeden problém, který jsem nedávno řešil. Máme takovéto prostředí: aplikace, kde je hodně moc různých tříd. V té aplikaci je někde zapeklitý bug, který musíme najít. Je to tak zapeklité, že se nelze soustředit na určitou část kódu, poněvadž absolutně nechápeme, co naší aplikaci přeletělo přes nos (takový vzdálený příbuzný Skynetu) – tedy jednoduše si něco vypisovat nepomůže ( print, pprint, repr, …). Máme dobré logování, ale jako naschvál loguje pro tento konkrétní problém nešťastným způsobem. A klasický debugger nelze použít. Co teď? Jak si usnadnit práci s hledáním problému?

Můžeme si postupně zjišťovat, jakou cestou aplikace zpracovává naše požadavky a po té cestičce vyskládat nějaké záchytné body. A podle zjištění nějaké ubrat a nové přidat. A podle nového zjištění zase nějaké ubrat a přidat. A stále dokola, dokud se nenarazí na příčinu problému.

Nebo si pomoci trochu jinak…

Řekněme, že máme takovýto kód:

class C(object):
    def __init__(self):
        self.x = 'x'
        self.y = 'y'

    def func_with_bug(self):
        # ...

A teď bychom si chtěli logovat, co se v třídě děje. Například co se nastavuje za proměnné. To lze udělat metodou  __setattr__:

class C(object):
    def __setattr__(self, key, value):
        print "Nastavuju %s na %s." % (key, repr(value))
        super(C, self).__setattr__(key, value)

Tak, nyní se nám před nastavením hodnoty vypíše co se nastavuje a na co. Mohli bychom si třeba vypisovat i původní hodnotu… Dále by se nám hodilo zjišťovat, které proměnné se používají. To lze udělat pomocí metody  __getattribute__:

class C(object):
    def __getattribute__(self, key):
        print "Vracim %s." % key
        super(C, self).__getattribute__(key)

Pozor! Existují dvě metody, jmenují se velmi podobně, ale každá dělá něco jiného. Mluvím o zmíněné __getattribute__ a __getattr__. První ( __getattribute__) se zavolá vždy, ať už daný atribut existuje, či nikoliv, zato druhá ( __getattr__) se zavolá pouze, když hledaný atribut neexistuje.

>>> class C(object):
...     x = 'x'
...     def __getattribute__(self, key):
...         print 'getattribute', key
...         super(C, self).__getattribute__(key)
...     def __getattr__(self, key):
...         print 'getattr', key
...         super(C, self).__getattribute__(key)
...
>>> c = C()
>>> c.x
getattribute x
>>> c.y
getattribute y
getattr y
AttributeError: 'C' object has no attribute 'y'

V ukázce nemám chybu. object nemá metodu __getattr__, má pouze __getattribute__, která buď vrátí hodnotu atributu nebo vyhodí výjimku.

Dále by se mi líbilo logovat, které metody se volají, nejlépe s jakými parametry. Též není problém, od toho máme dekorátory:

def logdecorator(func):
    def wrapper(*args, **kwds):
        print 'Volani metody %s s parametry %s, %s' % (func.__name__, args, kwds)
        return func(*args, **kwds)
    return f

class C(object):
    @logdecorator
    def f(self):
        pass

    @logdecorator
    def g(self, a, b=2):
        pass

Nyní mám poměrně dobře pokrytou jednu třídu detailním logováním. Má to ale problém – vidíte, co se musí všechno nadefinovat a na co je potřeba myslet? To už je opravdu jednodušší si ručně rozházet logovací příkazy. Pojďme si to zjednodušit, tedy použít metatřídy. Víte, jak to napsat? V tom případě si to zkuste sami, než budete pokračovat…

Předělání na metatřídu

Začneme tím, že si vytvoříme prázdnou metatřídu s metodou __new__, ve které vytvoříme a vrátíme novou třídu.

class DebugMetaClass(type):
    def __new__(metacls, className, bases, classdict):
        cls = type.__new__(metacls, className, bases, classdict)
        return cls

Zatím nic extra, pouze zbytečný kód. Rozšiřme ho. Budeme postupovat ve stejném pořadí, takže přidáme naší metatřídě třídní metodu, která nám vytvoří výše ukázanou metodu logující setované hodnoty instancím nově vytvářené třídy a té třídě ji nastaví.

def __new__(...):
    ...
    cls.__setattr__ = metacls.create_set_attr_method(cls)
    ...

@classmethod
def create_set_attr_method(metacls, cls):
    def f(self, key, value):
        print "Nastavuju %s na %s." % (key, repr(value))
        super(cls, self).__setattr__(key, value)
    return f

Obdobně bychom vytvořili i metodu vytvářející metodu zaznamenávající, které proměnné našich instancí se používají. Co je ale složitější, je odekorování metod. Musíme projít všechny atributy a pouze callable atributy odekorovat. Menší kvíz: odekorovat metody před nebo za nastavením metod __setattr__ a __getattribute__? … Odpověď: Určitě před, protože kdybychom to dali za, tak bychom si tyto metody také odekorovali, což je zbytečné až nežádoucí.

def __new__(...):
    ...
    for attributename, attribute in classdict.items():
        if hasattr(attribute, '__call__'):
            dbgdecorator = metacls.create_debug_decorator(cls)
            setattr(cls, attributename, dbgdecorator(attribute))
    ...

@classmethod
def create_debug_decorator(metacls, cls):
    def f(func):
        def wrapper(*args, **kwds):
            print 'Volani metody %s s parametry %s, %s' % (func.__name__, args, kwds)
            return func(*args, **kwds)
        return wrapper
    return f

Pro ověření, zda je atribut metoda (nebo funkce), jsem naschvál použil zápis hasattr(attribute, '__call__') místo built-in funkce callable, protože chceme podporovat co nejvíce verzí Pythonu. Jinými slovy funkci callable v Pythonu 3.0 a 3.1 nenalezneme. Naštěstí vývojáři dostali rozum a v Pythonu 3.2 se vrací zpět.

Výsledek

Takovéto logování může být fajn, ale nemusí postačovat. Například by bylo dobré logovat dobu trvání a výsledek funkcí. Nebo odkud se s instancí pracuje. Vše je řešitelné a cílem této ukázky není řešit tyto drobnosti. Cílem je ukázat, jak jednoduše a kdy například lze použít metatřídy.

Jak jsem řekl v úvodu řešení logovacího problému: už jsem tento problém řešil. Kód už jsem tedy sepsal do znovupoužitelné podoby a umístil na GitHub. Také jsem ho přidal do PyPI. Nemusíte tak tento kód přepisovat do vašeho modulu a dořešit detaily, o kterých jsem mluvil o odstavec výše. Podívat se na kód můžete na https://github­.com/…thon-debugger a nainstalovat z PyPI příkazem  pypi-install debugger.

Závěr

Metatřídy se většinou jako řešení nějakého problému nehodí, ale jsou užitečné. Pomohou nám pochopit, jak Python pracuje a jednou za čas elegantně vyřešit problémy, které nejdou jednoduše řešit klasickou cestou. Další zajímavé použití metatříd je například v Djangu, kde se používají u databázových modelů, mrkněte se jim na kód (ale pozor, je to docela elektrárna). Další zajímavé čtení o metatřídách lze nalézt v mini seriálu na stránkách IBM s dalšími ukázkami, kde se dají metatřídy vhodně využít:

Dalším zajímavým zdrojem může být oficiální popis od Guido van Rossum v článku Unifying types and classes in Python 2.2.

A tím ukončuji naše povídání o zajímovstech z Pythonu. Netvrdím však, že jsem vám sdělil vše zajímavé a must know. Stále je o čem mluvit a kde se dál vzdělávat. Další spoustu informací lze najít v dokumentaci. Python dokáže vše nějak vyřešit a prohledání dokumentace (případně stackoverflow) mi vždy pomohlo. A pomůže určitě i vám. Stačí jen zkusit hledat, protože již napsaný modul v céčku od chytřejších lidí je vždy lepší, než si něco bastlit sám v Pythonu.

Dokumentace nejvíce používané poslední stabilní verze je na adrese http://docs.pyt­hon.org/ a určitě doporučuju si ji projít.

Michal Hořejšek začal programovat už při studiu průmyslové školy v Jičíně v PHP, dnes pracuje jako Pythonista v Seznam.cz.

Čtení na léto

Jaké knihy z oboru plánujete přečíst během léta? Pochlubte se ostatním ve čtenářské skupině Zdrojak.cz na Goodreads.com.

Komentáře: 22

Přehled komentářů

pavel s. autorovi
Ivan Nový No to je sice nesmysl,
pavel s. Re: No to je sice nesmysl,
Ivan Nový Re: No to je sice nesmysl,
pavel s. Re: No to je sice nesmysl,
jen tak Re: asi tak nejak ?
Opravdový odborník :-) Třída vs. funkce
M747 classDict a classdict
Michal Hořejšek Re: classDict a classdict
Ivan Nový Metatřída = makro
FiŠ konstruktor
Michal Hořejšek Re: konstruktor
Honza Kral Skutecne neprofesionalni programovani
Michal Hořejšek Re: Skutecne neprofesionalni programovani
yad Re: Skutecne neprofesionalni programovani
Honza Kral Re: Skutecne neprofesionalni programovani
Honza Kral Re: Skutecne neprofesionalni programovani
jen tak Re: Skutecne neprofesionalni programovani
Martin Hassman Re: Skutecne neprofesionalni programovani
Honza Kral Re: Skutecne neprofesionalni programovani
Michal Hořejšek Re: Skutecne neprofesionalni programovani
Ales Zoulek Re: Skutecne neprofesionalni programovani
Zdroj: http://www.zdrojak.cz/?p=3657