Python - jsou deskriptory pomalé?
Konečně jsem se po dlouhé době dostal ke shlédnutí výborné přednášky Python3 metaprogramming. Příklady, co David Beazley ukazoval, mě inspirovali k napsání pár testů rychlosti deskriptorů a dalších pokročilejších technik v Pythonu.
Co je deskriptor
Deskriptor je libovolný objekt, který implementuje metody __get__, __set__ a __delete__. Pokud pak v nějaké třídě přiřadíme nějakému třídnímu atributu tento deskriptor jako hodnotu, změníme způsob, jakým se bude k tomuto atributu přistupovat. Místo přímého přístupu se pak volají právě metody deskriptoru.
Např.:
class NejakyDeskriptor(object): def __set__(self, obj, value): print('set') def __get__(self, obj, type=None): print('get') class Foo(object): boo = NejakyDeskriptor() foo = Foo() foo.boo = 1 foo.boo
Různé implementace
Pro benchmark naimplementuji třídu Stock (podobnou jako David v přednášce) pěti způsoby:
- klasickými settery a gettery metodami (ala Java)
- pomocí property dekorátorů
- vlastními jednoduchými deskriptory
- hierarchií deskriptorů
- hierarchií deskriptorů s metatřídou
Klasické settery a gettery
class Stock(object): def get_shares(self): return self._shares def set_shares(self, shares): if not isinstance(shares, int): raise TypeError('Expected int') if shares < 0: raise ValueError('Expected value >= 0') self._shares = shares
Máme tedy třídu StockClassic, která má setter set_shares. Ten provádí kontrolu na typ a hodnotu a poté nastaví "privátní" atribut. Podobně nainmplementujeme i ostatní metody.
Tento způsob je velmi jednoduchý a samozřejmě také rychlý. Znamená ale opakované psaní spousty kódu, zkusme to lépe.
Property
class Stock(object): @property def shares(self): return self._shares @shares.setter def shares(self, shares): if not isinstance(shares, int): raise TypeError('Expected int') if shares < 0: raise ValueError('Expected value >= 0') self._shares = shares
V tomto případě použijeme dekorátor property, který vytvoří deskriptor volající námi definované metody.
S takovýmto kódem se trochu lépe pracuje (nemusíme volat metody, můžeme přistupovat "přímo" k atributům), ale psaní tříd je stále hodně zdlouhavé a opakuje se nám stejný kód pro validace.
Jednoduché deskriptory
Property můžeme snadno přepsat na jednoduchý znovupoužitelný deskriptor.
class PositiveFloat(object): def __init__(self, name): self.name = name def __set__(self, obj, value): if not isinstance(value, float): raise TypeError('Expected float') if value < 0: raise ValueError('Expected value >= 0') obj.__dict__[self.name] = value def __get__(self, obj, type=None): obj.__dict__.get(self.name, None) class Stock(object): name = String('name') shares = PositiveInteger('shares') price = PositiveFloat('price')
Pokaždé když přistupujeme k atributům name, shares a price, volají se metody deskriptorů. V metodě __set__ deskriptor provede validace a poté přistoupí přímo ke slovníku objektu a zapíše na něj novou hodnotu atributu. Stejným způsobem můžeme snadno přepsat i PositiveFloat a String.
Tady už nějaký kód při psaní tříd ušetříme, protože můžeme deskriptory používat stále dokola. Samotné deskriptory ale obsahují duplicitní kód validací, zkusme to vyřešit dědičností.
Hierarchie deskriptorů
class Descriptor(object): def __init__(self, name=None): self.name = name def __set__(self, obj, value): obj.__dict__[self.name] = value def __get__(self, obj, type=None): return obj.__dict__.get(self.name, None) class Typed(Descriptor): def __set__(self, obj, value): if not isinstance(value, self.type): raise TypeError('Expected %s' % self.type) super(Typed, self).__set__(obj, value) class String(Typed): type = str class Integer(Typed): type = int class Float(Typed): type = float class Positive(Descriptor): def __set__(self, obj, value): if value < 0: raise ValueError('Expected value >= 0') super(Positive, self).__set__(obj, value) class PositiveInteger(Integer, Positive): pass class PositiveFloat(Float, Positive): pass
Nyní již se nám žádný kód validací neopakuje.
V deskriptorech používáme vícenásobnou dědičnost, která může na první pohled vypadat dost složitě. Co se tedy ve skutečnosti děje když se zavolá metoda __set__ na instanci PositiveInteger? Na pořadí předků záleží, zavolají se tedy metody __set__ na Typed, Positive a Descriptor. Jak přesně vypadá pořadí tříd, kde se budou hledat metody, můžeme zjistit zavoláním PositiveInteger.mro().
Stále jsme se ale nezbavili jedné duplicity a to opakování názvu atributu v konstruktoru deskriptorů, což můžeme vyřešit pomocí metatřídy.
Hierarchie deskriptorů s metatřídou
Třída je továrna na objekty. Podobně metatřída je továrna na třídy. Metatřídy nám umožňují provádět různé šikovné úpravy na třídách při jejich vytvoření.
class MyMeta(type): def __init__(cls, classname, bases, clsdict): for name, item in clsdict.iteritems(): if isinstance(item, Descriptor): item.name = name class Stock(object): __metaclass__ = MyMeta name = String() shares = PositiveInteger() price = PositiveFloat()
V Pythonu2 určíme metatřídu pomocí atributu __metaclass__. Metatřída je třída jejímž rodičem je type místo object. V naší metatřídě v metodě __init__ proiterujeme celý slovník třídy a pokud je nějaký prvek náš deskriptor, tak mu nastavíme jméno.
Máme hotovo. V Pythonu 3 bychom mohli ještě pomocí __signature__ vytvořit generickou __init__ metodu, ale do toho se již pouštět nebudu, mrkněte případně na Davidovu přednášku.
Benchmark
Nad každou implementací jsem milionkrát provedl zapsání a načtení hodnoty. Zde jsou výsledky:
Python 2.7.9
metoda | čas | poměr |
---|---|---|
Classic get | 0.668046 | 1 |
Classic set | 1.484425 | 1 |
Property get | 0.921953 | 1.38 |
Property set | 1.936587 | 1.3 |
Descriptor get | 1.635823 | 2.45 |
Descriptor set | 2.303607 | 1.55 |
DescriptorHierarchy get | 1.595446 | 2.39 |
DescriptorHierarchy set | 4.933396 | 3.32 |
DescriptorHierarchyWithMeta get | 1.678463 | 2.51 |
DescriptorHierarchyWithMeta set | 4.915713 | 3.31 |
Python 3.4.2
metoda | čas | poměr |
---|---|---|
Classic get | 1.028834 | 1 |
Classic set | 1.848903 | 1 |
Property get | 1.073665 | 1.04 |
Property set | 1.860438 | 1.01 |
Descriptor get | 1.9231 | 1.87 |
Descriptor set | 2.660129 | 1.44 |
DescriptorHierarchy get | 1.889621 | 1.84 |
DescriptorHierarchy set | 6.273938 | 3.39 |
DescriptorHierarchyWithMeta get | 1.880042 | 1.83 |
DescriptorHierarchyWithMeta set | 6.252514 | 3.38 |
PyPy 2.4.0
metoda | čas | poměr |
---|---|---|
Classic get | 0.015187 | 1 |
Classic set | 0.013867 | 1 |
Property get | 0.009597 | 0.63 |
Property set | 0.015619 | 1.13 |
Descriptor get | 0.01784 | 1.17 |
Descriptor set | 0.018436 | 1.33 |
DescriptorHierarchy get | 0.082476 | 5.43 |
DescriptorHierarchy set | 0.098304 | 7.09 |
DescriptorHierarchyWithMeta get | 0.088534 | 5.83 |
DescriptorHierarchyWithMeta set | 0.101391 | 7.31 |
Závěr
Použítí složitých hierarchií deskriptorů a metatříd rozhodně není zadarmo, zároveň nám ale mohou ušetřit psaní spousty kódu. Nad jejich použitím bychom tedy měli přemýšlet a vyvarovat se nadužívání těchto technik.
Jako velmi zajímavý se v testu ukázal výkon JIT překladače PyPy, který rychlostí mnohonásobně překonal výchozí CPython.
K dispozici je i celý zdrojový soubor benchmarku.