with how decorators decoradores create python python-2.7 decorator syntactic-sugar

how - python decorator with arguments



La mejor práctica del decorador de Python, usando una clase frente a una función (3)

Como lo he entendido, hay dos maneras de hacer un decorador de Python, ya sea para usar el __call__ de una clase o para definir y llamar a una función como el decorador. ¿Cuáles son las ventajas / desventajas de estos métodos? ¿Hay un método preferido?

Ejemplo 1

class dec1(object): def __init__(self, f): self.f = f def __call__(self): print "Decorating", self.f.__name__ self.f() @dec1 def func1(): print "inside func1()" func1() # Decorating func1 # inside func1()

Ejemplo 2

def dec2(f): def new_f(): print "Decorating", f.__name__ f() return new_f @dec2 def func2(): print "inside func2()" func2() # Decorating func2 # inside func2()


En general estoy de acuerdo con jsbueno: no hay una sola manera correcta. Depende de la situación. Pero creo que la def es probablemente mejor en la mayoría de los casos, porque si vas con clase, la mayor parte del trabajo "real" se realizará en __call__ todos modos. Además, los callables que no son funciones son bastante raros (con la notable excepción de crear instancias de una clase), y la gente en general no espera eso. Además, las variables locales suelen ser más sencillas para que las personas realicen un seguimiento de las variables de instancia, simplemente porque tienen un alcance más limitado, aunque en este caso, las variables de instancia probablemente solo se utilicen en __call__ (con __init__ simplemente copiándolas de los argumentos).

Sin embargo, tengo que estar en desacuerdo con su enfoque híbrido. Es un diseño interesante, pero creo que probablemente te va a confundir a ti o a alguien más que lo vea unos meses después.

Tangente: Independientemente de si va con la clase o la función, debe usar functools.wraps , que a su vez está destinado a ser utilizado como decorador (¡debemos ir más profundo!) Así:

import functools def require_authorization(f): @functools.wraps(f) def decorated(user, *args, **kwargs): if not is_authorized(user): raise UserIsNotAuthorized return f(user, *args, **kwargs) return decorated @require_authorization def check_email(user, etc): # etc.

Esto hace que decorated parezca check_email por ejemplo, cambiando su atributo func_name .

De todos modos, esto es generalmente lo que hago y lo que veo que hacen otras personas a mi alrededor, a menos que quiera una fábrica de decoradores. En ese caso, solo agrego otro nivel de def:

def require_authorization(action): def decorate(f): @functools.wraps(f): def decorated(user, *args, **kwargs): if not is_allowed_to(user, action): raise UserIsNotAuthorized(action, user) return f(user, *args, **kwargs) return decorated return decorate

Por cierto, también estaría en guardia contra el uso excesivo de decoradores, ya que pueden hacer que sea muy difícil seguir los rastros de la pila.

Un enfoque para gestionar rastros de pila horribles es tener una política de no cambiar sustancialmente el comportamiento del decorado. P.ej

def log_call(f): @functools.wraps(f) def decorated(*args, **kwargs): logging.debug(''call being made: %s(*%r, **%r)'', f.func_name, args, kwargs) return f(*args, **kwargs) return decorated

Un enfoque más extremo para mantener sus rastros de pila en buen estado es que el decorador devuelva el decorado sin modificaciones, de la siguiente manera:

import threading DEPRECATED_LOCK = threading.Lock() DEPRECATED = set() def deprecated(f): with DEPRECATED_LOCK: DEPRECATED.add(f) return f @deprecated def old_hack(): # etc.

Esto es útil si la función se llama dentro de un marco que conoce acerca del decorador en deprecated . P.ej

class MyLamerFramework(object): def register_handler(self, maybe_deprecated): if not self.allow_deprecated and is_deprecated(f): raise ValueError( ''Attempted to register deprecated function %s as a handler.'' % f.func_name) self._handlers.add(maybe_deprecated)


Es bastante subjetivo decir si hay "ventajas" para cada método.

Sin embargo, una buena comprensión de lo que sucede bajo el capó haría que sea natural elegir la mejor opción para cada ocasión.

Un decorador (hablando de decoradores de función), es simplemente un objeto invocable que toma una función como su parámetro de entrada. Python tiene un diseño bastante interesante que le permite a uno crear otros tipos de objetos invocables, además de funciones, y uno puede usarlo para crear código más fácil de mantener o más corto en ocasiones.

Los decoradores se agregaron en Python 2.3 como un "atajo sintáctico" para

def a(x): ... a = my_decorator(a)

Además de eso, solemos llamar a los decoradores algunos "callables" que prefieren ser "fábricas de decoradores", cuando usamos este tipo:

@my_decorator(param1, param2) def my_func(...): ...

la llamada se realiza a "my_decorator" con param1 y param2 - luego devuelve un objeto que se llamará de nuevo, esta vez con "my_func" como parámetro. Entonces, en este caso, técnicamente el "decorador" es lo que devuelve el "my_decorator", convirtiéndolo en una "fábrica decoradora".

Ahora bien, los decoradores o las "fábricas decoradoras" tal como se describen generalmente tienen que mantener algún estado interno. En el primer caso, lo único que conserva es una referencia a la función original (la variable llamada f en sus ejemplos). Una "fábrica decoradora" puede querer registrar variables de estado adicionales ("param1" y "param2" en el ejemplo anterior).

Este estado adicional, en el caso de decoradores escritos como funciones, se mantiene en variables dentro de las funciones adjuntas, y se accede como variables "no locales" por la función de envoltura real. Si se escribe una clase adecuada, se pueden mantener como variables de instancia en la función de decorador (que se verá como un "objeto invocable", no como una "función"), y el acceso a ellos es más explícito y más legible.

Entonces, para la mayoría de los casos, es cuestión de legibilidad si prefieres un enfoque u otro: para decoradores cortos y simples, el enfoque funcional es a menudo más legible que uno escrito como clase, aunque a veces sea más elaborado, especialmente uno. La "fábrica de decoradores" aprovechará al máximo el asesoramiento "plano es mejor que anidado" para la codificación de Python.

Considerar:

def my_dec_factory(param1, param2): ... ... def real_decorator(func): ... def wraper_func(*args, **kwargs): ... #use param1 result = func(*args, **kwargs) #use param2 return result return wraper_func return real_decorator

contra esta solución "híbrida":

class MyDecorator(object): """Decorator example mixing class and function definitions.""" def __init__(self, func, param1, param2): self.func = func self.param1, self.param2 = param1, param2 def __call__(self, *args, **kwargs): ... #use self.param1 result = self.func(*args, **kwargs) #use self.param2 return result def my_dec_factory(param1, param2): def decorator(func): return MyDecorator(func, param1, param2) return decorator

actualización : formas de decoración de "clase pura" que faltan

Ahora, tenga en cuenta que el método "híbrido" toma lo "mejor de ambos mundos" tratando de mantener el código más corto y más legible. Una "fábrica de decoradores" completa definida exclusivamente con clases necesitaría dos clases, o un atributo de "modo" para saber si se llamó para registrar la función decorada o para llamar realmente a la función final:

class MyDecorator(object): """Decorator example defined entirely as class.""" def __init__(self, p1, p2): self.p1 = p1 ... self.mode = "decorating" def __call__(self, *args, **kw): if self.mode == "decorating": self.func = args[0] self.mode = "calling" return self # code to run prior to function call result = self.func(*args, **kw) # code to run after function call return result @MyDecorator(p1, ...) def myfunc(): ...

Y, finalmente, un decorador puro de "colar blanco" definido con dos clases, tal vez manteniendo las cosas más separadas, pero aumentando la redundancia hasta un punto, no se puede decir que sea más fácil de mantener:

class Stage2Decorator(object): def __init__(self, func, p1, p2, ...): self.func = func self.p1 = p1 ... def __call__(self, *args, **kw): # code to run prior to function call ... result = self.func(*args, **kw) # code to run after function call ... return result class Stage1Decorator(object): """Decorator example defined as two classes. No "hacks" on the object model, most bureacratic. """ def __init__(self, p1, p2): self.p1 = p1 ... self.mode = "decorating" def __call__(self, func): return Stage2Decorator(func, self.p1, self.p2, ...) @Stage1Decorator(p1, p2, ...) def myfunc(): ...


Hay dos implementaciones de decorador diferentes. Uno de ellos usa una clase como decorador y el otro usa una función como decorador. Debe elegir la implementación preferida para sus necesidades.

Por ejemplo, si su decorador hace mucho trabajo, entonces puede usar la clase como decorador, así:

import logging import time import pymongo import hashlib import random DEBUG_MODE = True class logger(object): def __new__(cls, *args, **kwargs): if DEBUG_MODE: return object.__new__(cls, *args, **kwargs) else: return args[0] def __init__(self, foo): self.foo = foo logging.basicConfig(filename=''exceptions.log'', format=''%(levelname)s % (asctime)s: %(message)s'') self.log = logging.getLogger(__name__) def __call__(self, *args, **kwargs): def _log(): try: t = time.time() func_hash = self._make_hash(t) col = self._make_db_connection() log_record = {''func_name'':self.foo.__name__, ''start_time'':t, ''func_hash'':func_hash} col.insert(log_record) res = self.foo(*args, **kwargs) log_record = {''func_name'':self.foo.__name__, ''exc_time'':round(time.time() - t,4), ''end_time'':time.time(),''func_hash'':func_hash} col.insert(log_record) return res except Exception as e: self.log.error(e) return _log() def _make_db_connection(self): connection = pymongo.Connection() db = connection.logger collection = db.log return collection def _make_hash(self, t): m = hashlib.md5() m.update(str(t)+str(random.randrange(1,10))) return m.hexdigest()