¿Cómo decorar una clase?

129

En Python 2.5, ¿hay alguna forma 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 sintaxis 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 forma de hacer algo como una interfaz C # en Python. Necesito cambiar mi paradigma, supongo.

Robert Gowland
fuente

Respuestas:

80

En segundo lugar, quisiera considerar una subclase en lugar del enfoque que describió. Sin embargo, sin conocer su escenario específico, YMMV :-)

En lo que estás pensando es en una metaclase. los__new__ función en una metaclase pasa la definición completa propuesta de la clase, que luego puede reescribir antes de crear la clase. En ese momento, puede subcontratar el 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 el 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.

Jarret Hardie
fuente
Gracias, eso es lo que estoy buscando. Una clase que puede modificar cualquier número de otras clases de modo 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 sin ID de las clases, así como versiones de ID.
Robert Gowland el
Las metaclases solían ser la forma de hacer cosas como esta en Python2.5 o versiones anteriores, pero hoy en día puedes usar decoradores de clase (ver la respuesta de Steven), que son mucho más simples.
Jonathan Hartley
203

Además de la pregunta de si los decoradores de clase son la solución correcta para su problema:

En Python 2.6 y superior, hay decoradores de clase con la sintaxis @, por lo que puede escribir:

@addID
class Foo:
    pass

En versiones anteriores, puede 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 debe 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

A continuación, puede usar la sintaxis adecuada 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__.

Steven
fuente
2
... a pesar de que la pregunta menciona específicamente Python 2.5 :-)
Jarret Hardie
55
Perdón por el desorden de líneas, pero las muestras de código no son estrictamente excelentes en los comentarios ...: def addID (original_class): original_class .__ orig__init__ = original_class .__ init__ def init __ (self, * args, ** kws): imprime "decorador" self.id = 9 original_class .__ orig__init __ (self, * args, ** kws) original_class .__ init = init return original_class @addID class Foo: def __init __ (self): print "Foo" a = Foo () print a.id
Gerald Senarclens de Grancy
2
@Steven, @Gerald tiene razón, si pudiera actualizar su respuesta, sería mucho más fácil leer su código;)
Día
3
@Día: gracias por el recordatorio, no había notado los comentarios antes. Sin embargo: también obtengo la máxima profundidad de recursión con el código Geralds. Trataré de hacer una versión que funcione ;-)
Steven
1
@Day y @Gerald: lo siento, el problema no fue con el código de Gerald, me equivoqué por el código destrozado en el comentario ;-)
Steven
20

Nadie ha explicado que puede definir dinámicamente clases. Entonces puede tener un decorador que defina (y devuelva) 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é debería continuar haciendo esto en 2.6+) de esta manera:

class Foo:
    pass

FooId = addId(Foo)

Y en Python 3 como este (pero tenga cuidado de usar super()en sus clases):

@addId
class Foo:
    pass

Para que pueda tener su pastel y comerlo, ¡herencia y decoradores!

Andrew Cooke
fuente
44
Subclasificar en un decorador es peligroso en Python 2.6+, porque interrumpe las superllamadas en la clase original. Si Footuviera un método foollamado super(Foo, self).foo()así, se repetiría infinitamente, porque el nombre Fooestá vinculado a la subclase devuelta por el decorador, no a la clase original (a la que ningún nombre puede acceder). Python 3 sin argumentos super()evita este problema (supongo que a través de la misma magia del compilador que le permite funcionar). También puede solucionar el problema decorando manualmente la clase con un nombre diferente (como lo hizo en el ejemplo de Python 2.5).
Blckknght
1
eh gracias, no tenía idea (uso python 3). agregará un comentario
Andrew Cooke
13

Esa no es una buena práctica y no existe 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 Employeetiene, también con su propio __init__método y sus propios miembros.

mpeterson
fuente
3
Supongo que lo que quería era tener un jefe agnóstico de la clase que contiene. Es decir, puede haber docenas de clases diferentes a las que quiero aplicar las características de Boss. ¿Me queda que estas docenas de clases hereden de Boss?
Robert Gowland el
55
@Robert Gowland: Es por eso que Python tiene herencia múltiple. Sí, debe heredar varios aspectos de varias clases primarias.
S.Lott
77
@ S.Lott: En general, la herencia múltiple es una mala idea, incluso demasiados niveles de herencia también son malos. Le recomendaré que se mantenga alejado de la herencia múltiple.
mpeterson
55
mpeterson: ¿La herencia múltiple en python es peor que este enfoque? ¿Qué tiene de malo la herencia múltiple de Python?
Arafangion el
2
@Arafangion: la herencia múltiple generalmente se considera una señal de advertencia de problemas. Crea jerarquías complejas y relaciones difíciles de seguir. Si su dominio problemático se presta bien a la herencia múltiple (¿se puede modelar jerárquicamente?) Es una opción natural para muchos. Esto se aplica a todos los idiomas que permiten la herencia múltiple.
Morten Jensen
6

Estoy de acuerdo en que la herencia se ajusta mejor al problema planteado.

Encontré esta pregunta realmente útil en las clases de decoración, gracias a todos.

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

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 usado un decorador de funciones, ya que me parece más conciso. 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 comprueba 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

Los assertcheques que el decorador no se ha utilizado sin paréntesis. Si es así, la clase que se está decorando se pasa al msgparámetro del decorador, que genera unAssertionError .

@decorate_ifsolo aplica el decoratorsi conditionevalúa aTrue .

El getattr, callableprueba, y @decorate_ifse utiliza para que el decorador no se rompe si el foo()método no existe en la clase de ser decorado.

Chris
fuente
4

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

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

De hecho, creo que esta es una implementación bastante interesante. Debido a que subclasifica la clase que decora, se comportará exactamente como esta clase en cosas como los isinstancecheques.

Tiene un beneficio adicional: no es raro que la __init__declaración en un formulario django personalizado realice modificaciones o adiciones, por self.fieldslo que es mejor self.fieldsque ocurran cambios después de que todo se __init__haya ejecutado para la clase en cuestión.

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 clase.

Jordan Reiter
fuente
0

Aquí hay un ejemplo que responde a la pregunta de devolver los parámetros de una clase. Además, aún respeta la cadena de herencia, es decir, solo se devuelven los parámetros de la clase misma. La función get_paramsse agrega como un ejemplo simple, pero se pueden agregar otras funcionalidades gracias al módulo de inspección.

import inspect 

class Parent:
    @classmethod
    def get_params(my_class):
        return list(inspect.signature(my_class).parameters.keys())

class OtherParent:
    def __init__(self, a, b, c):
        pass

class Child(Parent, OtherParent):
    def __init__(self, x, y, z):
        pass

print(Child.get_params())
>>['x', 'y', 'z']
Patricio Astudillo
fuente