Python profesionálně: návrhové vzory

V předchozích dílech tohoto seriálu jsme se zabývali tipy, které by měl znát určitě každý, kdo programuje v Pythonu, aby si dokázal usnadnit práci. Dnes se posuneme trošku dál. Podíváme se, jak lze v Pythonu elegantně uplatnit několik návrhových vzorů. Konkrétně si vyzkoušíme udělat singleton, flyweight, dekorátor a další.
Seriál: Programujte v Pythonu profesionálně (5 dílů)
- Python profesionálně: úvod 3. 4. 2012
- Python profesionálně: dynamické parametry, generátory, lambda funkce a with 10. 4. 2012
- Python profesionálně: co jazyk nabízí 16. 4. 2012
- Python profesionálně: návrhové vzory 14. 5. 2012
- Python profesionálně: metatřídy 21. 5. 2012
Nálepky:
Singleton
class Singleton(object): pass Singleton.loggedUser = User(1)
Takhle jednoduše lze napsat singleton pattern v Pythonu. Sice se s touto třídou může dělat cokoliv, ale pokud můžeme důvěřovat, tak není problém prostě použít takhle primitivní řešení. Někteří můžou ale chtít nějakou tu ochranu – například znemožnění vytvoření instance, natož udělat jich více, dovolit nastavovat jen některé atributy a další. Tak to zkusme udělat. Zkusíme znemožnit vytvoření instance, zda to zabere.
class Singleton(object): def __init__(self): raise Exception('This is Singleton! You can't initialize it.') not_singleton = Singleton() # Exception: This is Singleton! You can't initialize it.
Zdá se, že jsme vyhráli, ale nepředbíhejme…
Singleton.__init__ = lambda self: None new_instance = Singleton()
Ve skutečnosti lze stále vytvořit instance. Ale hackem, takže osobně bych to dál neřešil. Řešení pro tento problém přesto zkusíme najít. Co sloty?
class Singleton(object): __slots__ = () def __init__(self): raise Exception('This is Singleton! You can't initialize it.') Singleton.__init__ = lambda self: None
Smůla, sloty nám sice pomůžou určit, které atributy půjdou (pouze) nastavovat, ale platí to jen pro instance, nikoliv pro třídy. Že bychom tedy nakonec použili skutečnou instanci?
class Singleton(object): __slots__ = () loggedUser = User(1) Singleton = Singleton() Singleton.loggedUser # OK Singleton.__init__ = lambda self: None # AttributeError: 'Singleton' object attribute '__init__' is read-only Singleton.x # AttributeError: 'Singleton' object has no attribute 'x' Singleton.x = 42 # AttributeError: 'Singleton' object has no attribute 'x'
Tohle řešení už vypadá dobře. Minimálně po stránce funkčnosti. Ale kdo by se chtěl s tímhle otravovat? Pokaždé definovat (prázdné) sloty a ještě přepsat referenci s definicí na třídu referencí na instanci. Není to zase tak velký problém, ale nelze to znovu použít (i když by to nemělo vadit, přece jen singletonem by se mělo šetřit). Zkusme trochu meta programování:
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 loggedUser = User(1)
Pomocí metatřídy jsme vytvořili úplně to samé řešení, jako v předchozím bodu, ale můžeme napsat další singleton, aniž bychom byli nuceni psát ten samý kód. Ba dokonce přemýšlet, co je pro singleton potřeba napsat. Prozatím nebudu popisovat, jak to funguje, o metatřídách budu psát později.
Poznámka: i když jsme definici třídy přepsali instancí, stále je někde v paměti. Jinak by ta instance přece nemohla existovat. Dokonce se k ní dá dostat, aniž bychom potřebovali hackerské znalosti. Jak to lze obejít:
Singleton.loggedUser # Promenna loggedUser z instance. Singleton.__class__ # Reference na tridu ulozena v instanci. Singleton2 = Singleton.__class__() # Vytvoreni nove instance.
A i proti tomu se lze bránit. Stačí zařídit, aby metoda __init__
vyhazovala výjimku jako v první ukázce.
Flyweight pro funkce
Flyweight pattern zřejmě nebude tak dobře znám jako singleton, proto si dovolím jeho stručný popis: aplikační cache.
Podrobnější popis: Flyweight pattern se většinou vysvětluje nad textovým editorem, tak nebudu vybočovat. Pomocí textového editoru píšeme texty; pro počítač znaky. Každý takový znak se musí nějak zobrazit. Nabízí se jednoduché (a výkonově a paměťově nevýhodné) řešení, kde pro každý znak vytvoříme novou a novou instanci. Tedy když budu mít v dokumentu tisíc znaků, tak budu mít v paměti tisíc instancí. Flyweight pattern je s pamětí skrblík a každý (jednotlivý) znak je v paměti pouze jednou a použijí se pouze reference na instance. Znamená to mít tedy nějaký seznam se všemi (použitými) znaky a když napíšeme nový znak, tak se nejprve podívat do toho seznamu a případně jen vytvořit novou referenci. V opačném případě daný znak vytvořit, zařadit do seznamu se znaky a vytvořit pro něj referenci.
Vytvoříme to jednoduše, například:
class _Character(object): """Skutecny znak.""" count_of_instances = 0 def __init__(self, character): self.__class__.count_of_instances += 1 self.__character = character def __repr__(self): return '<Character: %s>' % self.__character class Character(object): """Znak s referenci na skutecny znak a s tridnim seznamem skutecnych znaku.""" _list_of_characters = {} def __init__(self, character): self.character = character @property def character(self): return self.__character_instance @character.setter def character(self, character): list_of_characters = Character._list_of_characters if character in list_of_characters: self.__character_instance = list_of_characters[character] else: self.__character_instance = _Character(character) list_of_characters[character] = self.__character_instance def __repr__(self): return str(self.__character_instance) a = Character('x') b = Character('y') c = Character('x') d = Character('y') print Character._list_of_characters # {'y': <Character:: y>, 'x': <Character: x>} print _Character.count_of_instances # 2
To není ani tak zajímavé. Co je ale zajímavější, je možnost podobný princip použít u obyčejných funkcí! Představme si problém, že máme funkci, která provádí nějaký složitý výpočet. Nikdy nevíme, zda se funkce použije, kde se použije a kolikrát. A přesto nemá smysl stejný (náročný) výpočet provádět vícekrát. Takže nelze při prvním volání výsledek někam uložit a později znovu použít. V instanci by to bylo v pohodě, například:
class C(object): def __init__(self): self.__result = None def f(self): if self.__result is not None: return self.__result # Narocny vypocet... self.__result = 42 return self.__result
Ale jak na to u obyčejných funkcí? Respektive bez použití nějaké instanční, třídní nebo (dokonce!) globální proměnné?
def f(): if hasattr(f, 'result'): return f.result # Narocny vypocet... f.result = 42 return f.result f() # Provede se 'narocny vypocet' a vysledek ulozi do promenne. f() # Uz se jen vrati ulozeny vysledek bez provadeni 'narocneho vypoctu'. f.result # Jakmile se funkce jednou zavola, je toto take mozne.
Ano, vidíte správně. Funkce v Pythonu mohou mít své atributy. Párkrát jsem to s úspěchem použil a ušetřilo mi to spoustu strojového času a také spoustu psaní.
Takové řešení je vhodné hlavně pro jednoduché operace (= nemusíme výsledky třídit podle parametrů). Pokud je to však nutné, je potřeba použít jiné řešení:
class Memoize: def __init__(self, fn): self.fn = fn self.memo = {} def __call__(self, *args): if not self.memo.has_key(args): self.memo[args] = self.fn(*args) return self.memo[args] @Memoize def f(...): ...
Dynamické funkce
V jednom kódu jsem narazil na problém, kde se používaly globální proměnné pro řízení běhu řadící funkce. Vypadalo to nějak takto:
attribute_name = None def some_sort_method(x, y): global attribute_name return cmp(getattr(x, attribute_name), getattr(y, attribute_name)) attribute_name = 'attr1' objects.sort(some_sort_method) attribute_name = 'attr2' objects.sort(some_sort_method)
Problém spočíval v tom, že bylo potřeba řadit dynamicky podle různých parametrů. Řazení bylo složitější, než je v ukázce, takže nešlo převést na využití atributu key
jako například zde.
objects.sort(key=lambda x: x.attr1)
To však neznamená, že budeme trpět globální proměnné. Nelze sice jednoduše přidat třetí parametr pomocné metodě, protože řadící metoda volá metody předané argumentem cmp
s dvěma argumenty – dva prvky. Řešení však existuje v podobě vytváření dynamických funkcí. V Pythonu to je velmi podobné jako v JavaScriptu – tedy Closure.
def get_compare_function(attribute_name): def cmp_function(x, y): return cmp(getattr(x, attribute_name), getattr(y, attribute_name)) return cmp_function persons.sort(get_compare_function('attr1')) persons.sort(get_compare_function('attr2'))
Abych mohl vytvořit dynamicky funkce, které jsou totožné, ale přesto se chovají jinak, musím provést přes jinou funkci. Protože vždy mám přístupné proměnné z vyšších úrovní, nikoliv však z nižších. Tedy v ukázce si můžete všimnout, že ve funkci cmp_function
mohu číst proměnnou attribute_name
z nadřazené funkce, která je o úroveň výše.
Mimochodem si můžeme samozřejmě dynamicky vytvořenou funkci přidat do proměnné a využít vícekrát. Další užitečný příklad je například na Wikipedii.
Na závěr – pamatujete na předchozí tip a na trochu jiný switch? Pojďme předchozí tip vylepšit tímto tipem a switchnutím metod.
class C(object): def f(self): # Velmi narocny vypocet, ktery se provede pouze jednou. result = 42 self.f = lambda: result return result
Nezapomeňte, že nahrazující funkce musí mít úplně totožné rozhraní jako ta hlavní, i když už pro výpočet nebude potřeba.
Dekorátory
Možná by tato sekce měla být mezi syntaktickými tipy, ale k pochopení je potřeba znát vytváření funkcí dynamicky a přeci jen když budete číst o návrhových vzorech, najdete mezi nimi i dekorátor. A co to přesně dekorátor je? Jednoduše, dekorátory obalují existující kód o nějakou další funkčnost. Ukážeme si to na příkladu s právy, kde před každým vykonáním metody je potřeba ověřit, zda na funkci má uživatel právo. Jednoduše bychom mohli napsat:
def check_right(right): """Overi prava. Pokud nejsou, vyhodi vyjimku.""" singleton.loggedUser.check_right(right) class C(object): def f(self): check_right('right_f') # ... # ...
Jenže to se nám samozřejmě nechce. Obtěžuje nás to a dobře víme, že to je dobrý zdroj chyb. Zde dokonce bezpečnostních. Můžeme to zkusit odekorovat – jednoduše po vytvoření metody proměnnou uchovávající referenci na metodu přepíšeme dekorující funkcí, která bude ověřovat práva a poté volat skutečnou metodu. Vlastně takové upravené prohození metod.
def check_right_bad_decorator(func, right): def wrapper(*args, **kwds): singleton.loggedUser.check_right(right) func(*args, **kwds) return wrapper class C(object): # ... def g(self): # ... g = check_right_bad_decorator(f, 'right_g') # ...
Pomocí checkRightBadDecorator
vytvoříme dynamicky novou metodu, která nejprve ověří právo a poté zavolá původní metodu, kterou přepíšeme touto nově vytvořenou. Někomu se to může zdát dostačující, ale my se s tím nespokojíme, protože musíme metodu neustále po vytvoření přepisovat. Nemluvě o tom, že když změním název metody, musím název změnit na třech místech. A nemluvě o tom, že se na celou funkčnost metody musím podívat až za její konec (v tom lepším případě). Zkusíme už použít pravé Pythonovské dekorátory, použití je jednoduché: na řádek před metodu napíšeme zavináč následovaný názvem dekorátoru (funkce).
class C(object): # ... @check_right_bad_decorator def h(self): # ... # ...
Pokud jste si to šli hned zkusit, tak už víte, že to takhle nefunguje. Jednak nikde nepředávám právo a jednak prý „checkRightBadDecorator() takes exactly 2 arguments (1 given)“. Je to z jednoduchého důvodu: Python za nás automaticky předává jeden parametr, a to dekorovanou funkci/metodu. Pro lepší pochopení následující dvě třídy jsou úplně totožné.
class C(object): @decorator def f(self): pass class C(object): def f(self): pass f = decorator(f)
OK, název práva by šlo zjistit z volání funkce…
def check_right_bad_decorator2(func): def wrapper(*args, **kwds): singleton.loggedUser.check_right('right_%s' % func.__name__) func(*args, **kwds) return wrapper class C(object): # ... @check_right_bad_decorator2 def h(self): # ... # ...
…ale s tím se nespokojíme. Chceme mít jedno právo společné pro více metod, bez diskuze. Proto dekorátor znovu upravíme. Nyní bude přijímat v parametru pouze právo a vytvoří dynamicky nový dekorátor (funkci), který bude už tak, jak ho známe z checkRightBadDecorator
. V anotaci dekorátoru tak budeme moci mít jednoduše volání funkce s právem, které požadujeme.
def check_right_decorator(right): def decorator(func): def wrapper(*args, **kwds): singleton.loggedUser.check_right(right) return func(*args, **kwds) return wrapper return decorator class C(object): # ... @check_right_decorator('right_i') def i(self): # ... # ...
Jasné?
Pokud ne (nebojte, já s tím měl ze začátku taky trochu problém): Voláním @check_right_decorator('right_i')
vytvoříme dynamicky nový dekorátor, který už bude vědět, o které právo nám jde. Tento nový dekorátor se použije na odekorování naší metody, tedy něco jako i = decorator(i)
. Což vytvoří dynamicky funkci wrapper
, která se vždy postará o ověření toho požadovaného práva a o zavolání skutečné metody. Ještě pro lepší srozumitelnost napíšu předchozí kód trochu jinak:
class C(object): # ... def i(self): # ... new_decorator = check_right_decorator('right_i') i = new_decorator(i) # ...
Teď už to musí být jasné všem. :-)
Nakonec stejné řešení pomocí třídy.
class CheckRightDecorator(object): def __init__(self, perm): self.perm = perm def __call__(self, func): self.func = func return self.wrapped def wrapped(self, *args, **kwargs): singleton.loggedUser.check_right(right) return self.func(*args, **kwargs) class C(object): # ... @CheckRightDecorator('right_j') def j(self): # ...
Jak vidíme, použití Pythonovských dekorátorů je výhodné. Společný kód si zapouzdříte na jedno místo a pak jednoduše použijete kdekoliv. Navíc je hezky vidět, že je nějaká metoda odekorovaná a snižují se tím WTF momenty, kdy si přečtete kód a nedělá to, co jste právě četli.
Závěr
Po dnešku už toho umíme spoustu. Známe šikovné syntaktické zápisy, známe šikovné built-in funkce či module, nástrahy, možnosti, víme, kde případně hledat (většinou dokumentace, případně Google či Stack Overflow). Zbývá poslední věc, kterou bych vám chtěl přiblížit a na kterou jsme dnes už narazili – příště se těšte na metatřídy, kde popíšu, k čemu jsou a praktické příklady, kde je použít.
Dnešek opět zakončím několika odkazy k dalšímu studiu:
- Článek o dekorátorech na stránkách IBM.
- Sloty v oficiální dokumentaci.
- Kešování výsledků funkce na ActiveState Code.
- Co to je Closure na Wikipedii.
- Vyguglit si design patterns nebo si přečíst knížku Design Patterns.
V pythonu se Design Patterns venuje hlavne Alex Martelli, doporucuji jeho materialy a prednasky:
https://www.google.com/search?q=alex+martelli+design+patterns
Jinak singleton se vetsinou dela tak ze se udela instance tridy na modulu:
class DefaultConnectionProxy(object): …
connection = DefaultConnectionProxy()
a pak se podle toho pouziva:
from django.db import connection
je to mnohem jednodussi prace nez s tridami, da se to lepe testovat atd (v testech si vytvorime vlastni instanci a nemusime machinovat s globalnim stavem aplikace (coz je vzdy spatne).
Jinak drobna poznamka: ukazane pouziti singletonu je hodne diskutabilni pro jakekoliv web aplikace (samozrejme jako demonstrace je ok) – neni to thread safe a hrozily by velke bezpecnostni diry v zavislosti na externich faktorech (kontejner ve kterem aplikace pobezi). Pokud bychom tak trvali na pouziti singleton patternu, museli bychom se uchylit k _threading_local.
Jestli se takhle „jednoduše“ dělaji jedináčci, tak děkuju pěkně.
V pythonu se singleton dela proste tak ze se nadefinuje promena v modulu jako v mem predchozim komentari, vse ostatni je v clanku jen pro demonstraci toho co je mozne a rozhodne to neni cokoliv co by se kdekoliv pouzivalo.
has_key je deprecated, pouzivejte prosim:
if args in self.memo: …
misto get_compare_function pouzijte proste:
from operator import attrgetter
persons.sort(key=attrgetter(‚attr1‘))
persons.sort(key=attrgetter(‚attr2‘))
je to citelnejsi a efektivnejsi nez vase reseni.
switchnuti metody na classe je proste hack a nemelo by se to nikdy vyuzivat – jeden if v implementaci obalovaci metody vas neznici a prispeje citelnosti, testovatelnosti a udrzovatelnosti.