2009-03-25 11 views
98

En Python 2.5, ¿hay alguna manera de crear un decorador que decore una clase? Específicamente, quiero usar un decorador para agregar un miembro a una clase y cambiar el constructor para tomar un valor para ese miembro.Python Class Decorator

Buscando algo como lo siguiente (que tiene un error de sintaxis en la 'clase Foo:':

def getId(self): return self.__id 

class addID(original_class): 
    def __init__(self, id, *args, **kws): 
     self.__id = id 
     self.getId = getId 
     original_class.__init__(self, *args, **kws) 

@addID 
class Foo: 
    def __init__(self, value1): 
     self.value1 = value1 

if __name__ == '__main__': 
    foo1 = Foo(5,1) 
    print foo1.value1, foo1.getId() 
    foo2 = Foo(15,2) 
    print foo2.value1, foo2.getId() 

Gracias, Rob

Editar: Al releer esta pregunta, supongo que lo Estoy realmente después es una forma de hacer algo así como una interfaz C# en Python. Necesito cambiar mi paradigma, supongo.

+34

Si bien es útil, los decoradores de Python son diferentes al patrón de decorador. Un desafortunado nombre, supongo. Aunque puedo terminar yendo de esa manera. Gracias. –

+13

@Robert Gowland: Muy diplomático. –

Respuesta

65

Me gustaría secundar la idea de que es posible que desee considerar una subclase en lugar del enfoque que ha descrito. Sin embargo, sin conocer su escenario específico, YMMV :-)

Lo que está pensando es una metaclase. La función __new__ en una metaclase se pasa a la definición propuesta completa de la clase, que luego puede reescribir antes de que se cree la clase. En ese momento, puede subcontratar al constructor para obtener uno nuevo.

Ejemplo:

def substitute_init(self, id, *args, **kwargs): 
    pass 

class FooMeta(type): 

    def __new__(cls, name, bases, attrs): 
     attrs['__init__'] = substitute_init 
     return super(FooMeta, cls).__new__(cls, name, bases, attrs) 

class Foo(object): 

    __metaclass__ = FooMeta 

    def __init__(self, value1): 
     pass 

Sustitución del constructor es tal vez un poco dramático, pero el lenguaje sí provee soporte para este tipo de profunda introspección y la modificación dinámica.

+0

Gracias, eso es lo que estoy buscando. Una clase que puede modificar cualquier cantidad de otras clases de manera que todas tengan un miembro en particular. Mis razones para no tener las clases heredadas de una clase de ID común es que quiero tener versiones que no sean ID de las clases, así como las versiones de ID. –

+0

Las metaclasses solían ser la forma más fácil de hacer cosas como esta en Python2.5 o una versión anterior, pero hoy en día es muy común que uses decoradores de clase (mira la respuesta de Steven), que son mucho más simples. –

11

Eso no es una buena práctica y no hay mec Hanism para hacer eso debido a eso. La forma correcta de lograr lo que quieres es la herencia.

Eche un vistazo a class documentation.

Un pequeño ejemplo:

class Employee(object): 

    def __init__(self, age, sex, siblings=0): 
     self.age = age 
     self.sex = sex  
     self.siblings = siblings 

    def born_on(self):  
     today = datetime.date.today() 

     return today - datetime.timedelta(days=self.age*365) 


class Boss(Employee):  
    def __init__(self, age, sex, siblings=0, bonus=0): 
     self.bonus = bonus 
     Employee.__init__(self, age, sex, siblings) 

este jefe manera tiene todo Employee tiene, también con su propio método __init__ y propios miembros.

+3

Creo que lo que quería era tener a Boss como un agnóstico de la clase que contiene. Es decir, puede haber docenas de clases diferentes a las que quiera aplicar funciones de Boss. ¿Debo dejar que estas docenas de clases hereden de Boss? –

+4

@Robert Gowland: Es por eso que Python tiene herencia múltiple. Sí, debe heredar varios aspectos de varias clases principales. –

+7

@ S.Lott: En general, la herencia múltiple es una mala idea, incluso demasiados niveles de herencia también son malos. Te recomendaré que te mantengas alejado de la herencia múltiple. – mpeterson

176

Aparte de la cuestión de si los decoradores de clase son la solución adecuada a su problema:

en Python 2.6 y superior, hay decoradores de clase con el @ -Sintaxis, para que pueda escribir:

@addID 
class Foo: 
    pass 

en las versiones anteriores, puede hacerlo de otro modo: sin embargo

class Foo: 
    pass 

Foo = addID(Foo) 

Tenga en cuenta que esto funciona lo mismo que para los decoradores de función, y que el decorador debe r edita la clase nueva (o original modificada), que no es lo que estás haciendo en el ejemplo. El decorador Addid se vería así:

def addID(original_class): 
    orig_init = original_class.__init__ 
    # make copy of original __init__, so we can call it without recursion 

    def __init__(self, id, *args, **kws): 
     self.__id = id 
     self.getId = getId 
     orig_init(self, *args, **kws) # call the original __init__ 

    original_class.__init__ = __init__ # set the class' __init__ to the new one 
    return original_class 

A continuación, puede utilizar la sintaxis apropiada para su versión de Python como se describió anteriormente.

Pero estoy de acuerdo con otros en que la herencia es más adecuada si desea anular __init__.

+2

... aunque la pregunta menciona específicamente Python 2.5 :-) –

+0

Su ejemplo causa un RuntimeError: se excede la profundidad de recursión máxima, porque cuando se crea una instancia de la clase, original_class .__ init__ es la función que se llama, por lo que __init__ se llama a sí mismo. Voy a publicar un ejemplo de trabajo en el próximo comentario. –

+5

Lo siento por el desorden linesep, pero muestras de código no son estrictamente grande en los comentarios ...: def Addid (original_class): original_class .__ orig__init__ = original_class .__ init__ def __init __ (self, * args, ** KWS): de impresión "decorador" self.id = 9 original_class .__ orig__init __ (self, * args, ** KWS) original_class .__ init__ = __init__ retorno original_class @addID clase Foo: def __init __ (self): imprimir "Foo" a = Foo() print a.id –

4

De hecho, hay una muy buena aplicación de un decorador de la clase aquí:

https://github.com/agiliq/Django-parsley/blob/master/parsley/decorators.py

realidad creo que esta es una aplicación bastante interesante. Debido a que subclasifica la clase que decora, se comportará exactamente como esta clase en cosas como cheques isinstance.

Se tiene un beneficio adicional: no es raro que la declaración __init__ en un formulario de Django encargo de realizar modificaciones o adiciones a self.fields por lo que es mejor para los cambios en self.fields sucedan después todos __init__ ha utilizado por la clase de pregunta.

Muy inteligente.

Sin embargo, en su clase realmente desea que la decoración altere el constructor, lo que no creo que sea un buen caso de uso para un decorador de clases.

10

nadie ha explicado que puede definir dinámicamente las clases. lo que puede tener un decorador que define (y vuelve) una subclase:

def addId(cls): 

    class AddId(cls): 

     def __init__(self, id, *args, **kargs): 
      super(AddId, self).__init__(*args, **kargs) 
      self.__id = id 

     def getId(self): 
      return self.__id 

    return AddId 

que se puede utilizar en Python 2 (el comentario de Blckknght que explica por qué usted debe seguir para hacer esto en 2.6+) así:

class Foo: 
    pass 

FooId = addId(Foo) 

y en Python 3 como esto (pero tenga cuidado de usar super() en sus clases):

@addId 
class Foo: 
    pass 

para que pueda tener su pastel y cómelo - herencia y decoradores!

+2

La subclasificación en un decorador es peligrosa en Python 2.6+, ya que rompe llamadas 'super' en la clase original. Si 'Foo' tuviera un método llamado' foo' que se llama 'super (Foo, self) .foo()', se repetiría infinitamente, porque el nombre 'Foo' está vinculado a la subclase devuelta por el decorador, no a la clase original (que no es accesible con ningún nombre). El '' super() 'sin argumentos de Python 3 evita este problema (supongo que a través del mismo compilador mágico que le permite funcionar). También puede solucionar el problema decorando manualmente la clase con diferentes nombres (como lo hizo en el ejemplo de Python 2.5). – Blckknght

+1

huh. gracias, no tenía ni idea (uso Python 3). agregará un comentario. –

0

Estoy de acuerdo en que la herencia es una mejor opción para el problema planteado.

Encontré esta pregunta realmente útil en las clases de decoración, gracias a todos. Aquí hay otro par de ejemplos, sobre la base de otras respuestas, incluyendo cómo afecta la herencia cosas en Python 2.7, (y @wraps, que mantiene cadena de documentación de la función original, etc):

def dec(klass): 
    old_foo = klass.foo 
    @wraps(klass.foo) 
    def decorated_foo(self, *args ,**kwargs): 
     print('@decorator pre %s' % msg) 
     old_foo(self, *args, **kwargs) 
     print('@decorator post %s' % msg) 
    klass.foo = decorated_foo 
    return klass 

@dec # no parentheses 
class Foo... 

frecuencia con que desea añadir parámetros a su decorador:

from functools import wraps 

def dec(msg='default'): 
    def decorator(klass): 
     old_foo = klass.foo 
     @wraps(klass.foo) 
     def decorated_foo(self, *args ,**kwargs): 
      print('@decorator pre %s' % msg) 
      old_foo(self, *args, **kwargs) 
      print('@decorator post %s' % msg) 
     klass.foo = decorated_foo 
     return klass 
    return decorator 

@dec('foo decorator') # you must add parentheses now, even if they're empty 
class Foo(object): 
    def foo(self, *args, **kwargs): 
     print('foo.foo()') 

@dec('subfoo decorator') 
class SubFoo(Foo): 
    def foo(self, *args, **kwargs): 
     print('subfoo.foo() pre') 
     super(SubFoo, self).foo(*args, **kwargs) 
     print('subfoo.foo() post') 

@dec('subsubfoo decorator') 
class SubSubFoo(SubFoo): 
    def foo(self, *args, **kwargs): 
     print('subsubfoo.foo() pre') 
     super(SubSubFoo, self).foo(*args, **kwargs) 
     print('subsubfoo.foo() post') 

SubSubFoo().foo() 

Salidas:

@decorator pre subsubfoo decorator 
subsubfoo.foo() pre 
@decorator pre subfoo decorator 
subfoo.foo() pre 
@decorator pre foo decorator 
foo.foo() 
@decorator post foo decorator 
subfoo.foo() post 
@decorator post subfoo decorator 
subsubfoo.foo() post 
@decorator post subsubfoo decorator 

he utilizado un decorador función, ya que me parece más concisos. Aquí está una clase para decorar una clase:

class Dec(object): 

    def __init__(self, msg): 
     self.msg = msg 

    def __call__(self, klass): 
     old_foo = klass.foo 
     msg = self.msg 
     def decorated_foo(self, *args, **kwargs): 
      print('@decorator pre %s' % msg) 
      old_foo(self, *args, **kwargs) 
      print('@decorator post %s' % msg) 
     klass.foo = decorated_foo 
     return klass 

Una versión más robusta que los controles de los paréntesis, y funciona si no existen los métodos de la clase decorada:

from inspect import isclass 

def decorate_if(condition, decorator): 
    return decorator if condition else lambda x: x 

def dec(msg): 
    # Only use if your decorator's first parameter is never a class 
    assert not isclass(msg) 

    def decorator(klass): 
     old_foo = getattr(klass, 'foo', None) 

     @decorate_if(old_foo, wraps(klass.foo)) 
     def decorated_foo(self, *args ,**kwargs): 
      print('@decorator pre %s' % msg) 
      if callable(old_foo): 
       old_foo(self, *args, **kwargs) 
      print('@decorator post %s' % msg) 

     klass.foo = decorated_foo 
     return klass 

    return decorator 

Los assert cheques que el decorador no se ha usado sin paréntesis.Si lo tiene, la clase que se está decorando se pasa al parámetro msg del decorador, lo que genera un AssertionError.

@decorate_if solo aplica decorator si condition evalúa a True.

El getattr, callable prueba, y @decorate_if se utilizan para que el decorador no se rompe si el método foo() no existe en la clase de ser decorado.

Cuestiones relacionadas