O mică poveste despre decoratori

28
O mică poveste despre decoratori

Transcript of O mică poveste despre decoratori

O mică poveste despre decoratori

O mică poveste despre decoratoriIonel Cristian Mărieș — Partizan Python / OSS

blog.ionelmc.rogithub.com/ionelmc

DecoratoriCine nu a scris un decorator ?

Arată cunoscut ?>>> from functools import wraps>>> def log_errors(func):... @wraps(func)... def log_errors_wrapper(*args, **kwargs):... try:... return func(*args, **kwargs)... except Exception as exc:... print("Raised %r for %s/%s" % (exc, args, kwargs))... raise... return log_errors_wrapper

>>> @log_errors... def broken_function():... raise RuntimeError()

>>> from pytest import raises>>> raises(RuntimeError, broken_function)Raised RuntimeError() for ()/{}...

Cum funcționeazădef log_errors(func): def log_errors_wrapper(arg): return func(arg) return log_errors_wrapper

@log_errorsdef broken_function(): pass

broken_function = log_errors(broken_function)

Fără closures>>> class log_errors(object):... def __init__(self, func):... self.func = func... def __call__(self, *args, **kwargs):... try:... return self.func(*args, **kwargs)... except Exception as exc:... print("Raised %r for %s/%s" % (exc, args, kwargs))... raise

>>> @log_errors... def broken_function():... raise RuntimeError()

>>> from pytest import raises>>> raises(RuntimeError, broken_function)Raised RuntimeError() for ()/{}...

O mică parantezăA fost odată ca niciodatăCă de n-ar fi bad practiceNu s-ar povesti

>>> def log_errors(func):... def wrapper(*args, **kwargs):... try:... return func(*args, **kwargs)... except Exception as exc:... print("Raised %r for %s/%s" % (exc, args, kwargs))... raise exc... return wrapper

Ce nu e bine ?

O mică paranteză>>> @log_errors... def foobar():... pass

>>> print(foobar)<function ...wrapper at 0x...>

Numele funcției, dat dispărut ...

O mică paranteză>>> @log_errors... def foobar():... unu()>>> def unu():... doi()>>> def doi():... raise Exception("Dezastru ...")

>>> foobar()Traceback (most recent call last):... File "<doctest decoratori.rst[...]>", line ..., in wrapper...Exception: Dezastru ...

Care wrapper ? Numele e prea generic.

Traceback-ul nu contine informatii despre doi si unu (în Python 2)

De fiecare dată când scriu un decorator ...

Mult cod repetitiv

Inevitabil ajungi în colțuri ciudateExistă 2 tipuri de funcții, decise la compilare:

Funcția cea de toate zilele ...

Funcția generator, dracul împielițat:

are yield

poate avea return (gol)

poate avea return valoare (doar în Python 3)

Așadar, funcția generator intoarce un generator.

Dacă excepția este aruncată după ce a început iterarea atunci decoratorul nostru nu o poateprinde.

Trebuie sa consumam generatorul ( for i in ...: yield i )

Funcție generator>>> @log_errors... def broken_generator():... yield 1... raise RuntimeError()

>>> raises(RuntimeError, list, broken_generator()).valueRuntimeError()

Dooh ! Decoratorul nu face nimic ...

La doctor cu decoratorul (refactor :)Otrava prescrisă: condiții și repetiții

>>> from inspect import isgeneratorfunction>>> def log_errors(func):... if isgeneratorfunction(func):... @wraps(func)... def log_errors_wrapper(*args, **kwargs):... try:... for item in func(*args, **kwargs):... yield item... except Exception as exc:... print("Raised %r for %s/%s" % (exc, args, kwargs))... raise... else:... @wraps(func)... def log_errors_wrapper(*args, **kwargs):... try:... return func(*args, **kwargs)... except Exception as exc:... print("Raised %r for %s/%s" % (exc, args, kwargs))... raise... return log_errors_wrapper

Merge ...>>> @log_errors... def broken_generator():... yield 1... raise RuntimeError()

>>> raises(RuntimeError, list, broken_generator())Raised RuntimeError() for ()/{}...

Medicamentul, greu de înghițitTrebuie 2 functii - fiindcă funcția generator (are yield ) nu poate avea return cu valoare

Nu merge cu corutine ...

Corutine ?

Corutine (1/3)Python 3:>>> from inspect import isgeneratorfunction>>> def log_errors(func):... if isgeneratorfunction(func):... @wraps(func)... def log_errors_wrapper(*args, **kwargs):... try:... yield from func(*args, **kwargs)... except Exception as exc:... print("Raised %r for %s/%s" % (exc, args, kwargs))... raise... else:... @wraps(func)... def log_errors_wrapper(*args, **kwargs):... try:... return func(*args, **kwargs)... except Exception as exc:... print("Raised %r for %s/%s" % (exc, args, kwargs))... raise... return log_errors_wrapper

Corutine (2/3)>>> @log_errors... def broken_coroutine():... print((yield 1))... raise RuntimeError()

>>> coro = broken_coroutine()>>> next(coro)1>>> raises(RuntimeError, coro.send, 'mesaj')mesajRaised RuntimeError() for ()/{}...

Corutine (3/3)yield from (PEP-380) în Python 2? O minune (1/2):_i = iter(EXPR) # EXPR ar fi `func(*args, **kwargs)`try: _y = next(_i)except StopIteration as _e: _r = _e.valueelse: while 1: try: _s = yield _y except GeneratorExit as _e: try: _m = _i.close except AttributeError: pass else: _m() raise _e except BaseException as _e: _x = sys.exc_info()

Corutine (3/3) (bis)yield from (PEP-380) în Python 2? O minune (2/2): try: _m = _i.throw except AttributeError: raise _e else: try: _y = _m(*_x) except StopIteration as _e: _r = _e.value break else: try: if _s is None: _y = next(_i) else: _y = _i.send(_s) except StopIteration as _e: _r = _e.value breakRESULT = _r

Alternativă: aspectlib (1/2)>>> from aspectlib import Aspect>>> @Aspect... def log_errors(*args, **kwargs):... try:... yield... except Exception as exc:... print("Raised %r for %s/%s" % (exc, args, kwargs))... raise

>>> @log_errors... def broken_function():... raise RuntimeError()>>> raises(RuntimeError, broken_function)Raised RuntimeError() for ()/{}...

Mai multe detalii: documentație aspectlib.

Alternativă: aspectlib (1/2)Merge corect cu generatori:>>> @log_errors... def broken_generator():... yield 1... raise RuntimeError()>>> raises(RuntimeError, lambda: list(broken_generator()))Raised RuntimeError() for ()/{}...

Și corutine:>>> @log_errors... def broken_coroutine():... print((yield 1))... raise RuntimeError()

>>> coro = broken_coroutine()>>> next(coro)1>>> raises(RuntimeError, coro.send, 'mesaj')mesajRaised RuntimeError() for ()/{}...

Alte colțuri ciudate: Metode>>> def trebuie_mecanic(func):... @wraps(func)... def wrapper_trebuie_mecanic(sofer):... if not sofer.are_bujie_de_rezerva:... raise RuntimeError("N-ai noroc")... return func(sofer)... return wrapper_trebuie_mecanic

>>> class Dacie(object):... @trebuie_mecanic... def porneste(self, sofer):... print("Blană !")

>>> from collections import namedtuple>>> Sofer = namedtuple("Sofer", ["are_bujie_de_rezerva"])

>>> rabla = Dacie()>>> rabla.porneste(Sofer(True))Traceback (most recent call last):...TypeError: wrapper_trebuie_mecanic() takes 1 positional argument but 2 were given

Opaaaaaa ....

Metodele sunt descriptori (1/2)>>> class Metoda(object):... def __init__(self, func, nevasta):... self.func = func... self.nevasta = nevasta... def __call__(self, *args, **kwargs):... return self.func(self.nevasta, *args, **kwargs)... def __repr__(self):... return "<metodă însurată %s cu %s>" % (... self.func.__name__, self.nevasta)>>> class Functie(object):... factory = Metoda... def __init__(self, func):... self.func = func... def __call__(self, *args, **kwargs):... return self.func(*args, **kwargs)... def __repr__(self):... return "<funcție %s>" % (self.func.__name__)... def __get__(self, instanta, clasa):... if instanta is None:... return self... return self.factory(self.func, instanta)

Metodele sunt descriptori (2/2)>>> def haleste(cine):... print(cine, "mânâncă ...")

Nelegată:>>> Functie(haleste)<funcție haleste>

>>> class Gheorghe(object):... manca = Functie(haleste)>>> Gheorghe.manca # nu e 100% corect, ar trebui sa fie "unbound function ..."<funcție haleste>

Legată:>>> gheo = Gheorghe()>>> gheo.manca<metodă însurată haleste cu <__main__.Gheorghe object at ...>>>>> gheo.manca()<__main__.Gheorghe object at ...> mânâncă ...

Decorator care e și descriptor>>> class MixinTrebuieMecanic(object):... def __call__(self, sofer):... if not sofer.are_bujie_de_rezerva:... raise RuntimeError("N-ai noroc")... return super(MixinTrebuieMecanic, self)(sofer)

>>> class MetodaTrebuieMecanic(Metoda, MixinTrebuieMecanic):... pass

>>> class TrebuieMecanic(Functie, MixinTrebuieMecanic):... factory = MetodaTrebuieMecanic

>>> class Dacie(object):... @TrebuieMecanic... def porneste(self, sofer):... print("Blană !")

>>> rabla = Dacie()>>> rabla.porneste(Sofer(True))Blană !

Aceasta nu e soluția perfectă desigur, există altele ...

Soluția simplificată: wraptFară prea mare bataie de cap:>>> import wrapt

>>> @wrapt.decorator... def trebuie_mecanic(func, instanta, args, kwargs):... sofer, = args... if not sofer.are_bujie_de_rezerva:... raise RuntimeError("N-ai noroc")... return func(sofer)

>>> class Dacie(object):... @trebuie_mecanic... def porneste(self, sofer):... print("Blană !")

>>> rabla = Dacie()>>> rabla.porneste(Sofer(True))Blană !

Acoperă toate cazurile. Documentație.

Întrebări și altecelePrezentarea: h�p://bit.ly/decoratori