¿Cómo decorar una clase?

En Python 2.5, ¿hay una 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.

Buscando algo como lo siguiente (que tiene un error de syntax en ‘class 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() 

Supongo que lo que realmente busco es una manera de hacer algo como una interfaz C # en Python. Necesito cambiar mi paradigma, supongo.

Me gustaría secundar la idea de que tal vez desee considerar una subclase en lugar del enfoque que ha descrito. Sin embargo, al no conocer su escenario específico, YMMV 🙂

Lo que estás pensando es una metaclase. La función __new__ en una metaclase recibe la definición propuesta completa de la clase, que luego puede volver a escribir antes de que se cree la clase. Usted puede, en ese momento, eliminar al constructor por 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 

Reemplazar al constructor es quizás un poco dramático, pero el lenguaje proporciona soporte para este tipo de introspección profunda y modificación dinámica.

Aparte de la pregunta de si los decoradores de clase son la solución adecuada para su problema:

En Python 2.6 y versiones posteriores, hay decoradores de clase con @ -syntax, por lo que puedes escribir:

 @addID class Foo: pass 

En versiones anteriores, puedes hacerlo de otra manera:

 class Foo: pass Foo = addID(Foo) 

Sin embargo, tenga en cuenta que esto funciona igual que para los decoradores de funciones, y que el decorador debería devolver la clase nueva (o original modificada), que no es lo que está 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 

Luego puede usar la syntax apropiada para su versión de Python como se describe anteriormente.

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

Nadie ha explicado que puedes definir clases dinámicamente. Así que puedes tener un decorador que define (y devuelve) 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 usar en Python 2 (el comentario de Blckknght que explica por qué debes continuar haciendo esto en 2.6+) de esta manera:

 class Foo: pass FooId = addId(Foo) 

Y en Python 3 como este (pero ten cuidado de usar super() en tus clases):

 @addId class Foo: pass 

¡Así que puedes tener tu pastel y comerlo – herencia y decoradores!

Esa no es una buena práctica y no hay un mecanismo para hacerlo debido a eso. La forma correcta de lograr lo que quieres es la herencia.

Echa un vistazo a la documentación de la clase .

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) 

De esta manera, Boss tiene todo lo que el Employee tiene, también con su propio método __init__ y sus propios miembros.

En realidad hay una implementación bastante buena de un decorador de clase aquí:

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

En realidad creo que esta es una implementación bastante interesante. Debido a que asigna una subclase a la clase que decora, se comportará exactamente como esta clase en cosas como isinstance verificaciones de isinstance .

Tiene un beneficio adicional: no es infrecuente que la statement __init__ en un formulario django personalizado realice modificaciones o adiciones a self.fields por lo que es mejor que self.fields cambios en self.fields después de que __init__ haya ejecutado para la clase en cuestión.

Muy inteligente.

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

Estoy de acuerdo en que la herencia es un mejor ajuste para el problema planteado.

Me pareció muy útil esta pregunta en las clases de decoración, gracias a todos.

Aquí hay otro par de ejemplos, basados ​​en otras respuestas, incluyendo cómo la herencia afecta las cosas en Python 2.7, (y @wraps , que mantiene la cadena de documentos 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... 

A menudo desea agregar 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 de funciones, ya que los encuentro más concisos. Aquí hay 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 busca esos paréntesis y funciona si los métodos no existen en 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 

La assert comprueba que el decorador no se ha utilizado sin paréntesis. Si es así, entonces la clase que se está decorando pasa al parámetro msg del decorador, que genera un AssertionError .

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

La prueba @decorate_if , @decorate_if y @decorate_if se usan para que el decorador no se rompa si el método foo() no existe en la clase que se está decorando.