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ů)

  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

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 me­tod.

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ý „checkRightBad­Decorator() 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:

Michal dělá team leadera v Seznam.cz a hraje si na BOObook.cz. Jeho nejoblíbenějším jazykem je Python, ale nevadí mu třeba ani JavaScript a rád zkouší nové jazyky i technologie. Ve volném čase cestuje, fotí, píše, ale taky plave, jezdí na kole či tancuje.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 4

Přehled komentářů

Honza Kral Design patterns v Pythonu
Aminux No nevim
Honza Kral Re: No nevim
Honza Kral detaily a hacky
Zdroj: https://www.zdrojak.cz/?p=3625