2012-04-24 9 views
33

Como he entendido, hay dos maneras de hacer un decorador de Python, ya sea usar el __call__ de una clase o 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?Mejor práctica del decorador de Python, usando una clase frente a una función

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() 
+5

Una cosa importante: sus funciones de contenedor reales llaman la 'función F' original, pero no devuelven su valor devuelto al destinatario de la llamada: esto muy probablemente conduciría a un comportamiento incorrecto. – jsbueno

+0

posible duplicado de [Diferencia entre clases de decorador y decorador] (http://stackoverflow.com/questions/4650333/difference-between-decorator-classes-and-decorator-functions) –

Respuesta

29

es más bien subjetiva decir si hay "ventajas" para cada método.

Sin embargo, una buena comprensión de lo que sucede bajo el capó lo haría natural para 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 su diseño bastante interesante que permite a crear otros tipos de objetos invocables, además de funciones, y se puede usar para crear código más fácil de mantener o más corto en ocasiones.

decoradores se añadieron de nuevo en Python 2.3 como un "atajo sintáctico" para

def a(x): 
    ... 

a = my_decorator(a) 

Además de eso, que se suele llamar decoradores algunos "callables" que prefiere ser "fábricas decorador" - cuando utilizamos este tipo :

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

la llamada se realiza a "my_decorator" con param1 y param2 - a continuación, devuelve un objeto que será llamado de nuevo, esta vez teniendo "mi_func" como un parámetro. Entonces, en este caso, técnicamente el "decorador" es lo que devuelve el "my_decorator", lo que lo convierte en una "fábrica decoradora" .

Ahora, cualquiera de los decoradores o "fábricas decoradoras" como se describe normalmente 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.

Por lo tanto, para la mayoría de los casos es cuestión de legibilidad si prefiere un enfoque u otro: para decoradores cortos y simples, el enfoque funcional es a menudo más legible que uno escrito como clase - mientras que a veces una elaborada, especialmente una "fábrica decoradora" aprovechará al máximo el consejo "plano es mejor que anidado" para la codificación Python.

Considere:

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 de 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: Missing formas "pura clase" de decoradores

Ahora, tenga en cuenta el "híbrido" método toma lo mejor de ambos mundos tratando de mantener el código más corto y más fácil de leer. Una "fábrica decorador" completa definida exclusivamente con las clases sería o bien tendrá dos clases, o un atributo "modo" para saber si ha sido llamado a registrar la función decorado o llamar realmente 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, por último un puro, decorador de "colar blanco" se define con dos clases - tal vez mantener las cosas más separados, pero el aumento de la redundancia a un punto que no se puede decir que es 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(): 
    ... 
+0

Perdón por haber tardado tanto en responder, estaba fuera. De todos modos, esta fue una buena respuesta, gracias :) – olofom

+0

@jiamo - lo siento, su edición fue correcta - no hay forma de que pueda volver a ella ya que presioné 'rechazar' por error. El error que descubrió está solucionado ahora. – jsbueno

+0

Respuesta impresionante, gracias. Para evitar el "modo propio", también puede devolver una función dentro de la __call__. p.ej. def __call __ (self, func): def wrapper (* args, ** kwargs): return func (* args, ** kwargs) return wrapper – Josejulio

1

Hay dos implementaciones diferentes decorador. 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 un montón de trabajo, entonces se puede utilizar 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() 
+0

Sí, pero todavía me pregunto sobre las ventajas/desventajas para las dos formas de hacerlo y cuándo usar qué. Ok, el decorador de clases puede ser un poco más avanzado, ¿no? Cualquier desventaja? – olofom

+0

OK, el decorador es una función o clase que toma la función o clase en la entrada. Y ahora puede pensar en qué casos es conveniente usar la función o cuándo desea usar la clase. – Denis

+1

Esta respuesta es conceptualmente incorrecta: este no es un "decorador de clases": un "decorador de clases" decora una clase, y no se correlaciona con su implementación, es una clase o una función. – jsbueno

9

sobre todo estoy de acuerdo con jsbueno: no hay nadie en el camino correcto. 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__ de 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 usted va con la clase o función, se debe utilizar functools.wraps, que a su vez está destinado a ser utilizado como un decorador (que debe ser más profunda!), 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 decorated parezca check_email por ejemplo cambiando su atributo func_name.

De todos modos, esto es generalmente lo que hago y lo que veo a otras personas a mi alrededor, a menos que quiera una fábrica de decoradores. En ese caso, acabo de añadir 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, yo también estar en guardia contra el uso excesivo de decoradores, ya que pueden hacer que sea muy difícil seguir seguimientos de 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 extrema para mantener su pila traza cuerdo es para el decorador para devolver el decoratee no modificada, así:

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 es llamada dentro de un marco que sabe sobre el decorador 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) 
+0

Lo que llamo "enfoque híbrido" es simplemente hacer que el método '__call__' actúe como un decorador en sí mismo - obteniendo la función de ser decorado como un parámetro. ¿Podría dar un ejemplo de un decorador de clase que no lo haría? (Para tal, el '__call__' tendría que alimentar el objeto todavía en otro objeto, probablemente de otra clase, estrechamente unido con el primero - o - mantener un estado para" saber "si ya se había vinculado a una función o no .-- Ambas cosas me resultan bastante más confusas que tener '__call__' envolver la función decorada en sí. – jsbueno

+0

Excelente respuesta. –

Cuestiones relacionadas