¿Puede un decorador de un método de instancia acceder a la clase?

109

Tengo algo parecido a lo siguiente. Básicamente, necesito acceder a la clase de un método de instancia desde un decorador utilizado en el método de instancia en su definición.

def decorator(view):
    # do something that requires view's class
    print view.im_class
    return view

class ModelA(object):
    @decorator
    def a_method(self):
        # do some stuff
        pass

El código tal como está da:

AttributeError: el objeto 'función' no tiene atributo 'im_class'

Encontré preguntas / respuestas similares: el decorador de Python hace que la función olvide que pertenece a una clase y Obtener clase en el decorador de Python , pero se basan en una solución alternativa que captura la instancia en tiempo de ejecución al arrebatar el primer parámetro. En mi caso, llamaré al método en función de la información obtenida de su clase, por lo que no puedo esperar a que entre una llamada.

Carl G
fuente

Respuestas:

68

Si está utilizando Python 2.6 o posterior, podría usar un decorador de clases, quizás algo como esto (advertencia: código no probado).

def class_decorator(cls):
   for name, method in cls.__dict__.iteritems():
        if hasattr(method, "use_class"):
            # do something with the method and class
            print name, cls
   return cls

def method_decorator(view):
    # mark the method as something that requires view's class
    view.use_class = True
    return view

@class_decorator
class ModelA(object):
    @method_decorator
    def a_method(self):
        # do some stuff
        pass

El decorador de métodos marca el método como uno de interés agregando un atributo "use_class"; las funciones y los métodos también son objetos, por lo que puede adjuntarles metadatos adicionales.

Una vez que se ha creado la clase, el decorador de clases pasa por todos los métodos y hace lo que sea necesario en los métodos que se han marcado.

Si desea que todos los métodos se vean afectados, puede omitir el decorador de métodos y simplemente usar el decorador de clases.

Dave Kirby
fuente
2
Gracias creo que esta es la ruta por la que ir. Solo una línea adicional de código para cualquier clase que quisiera usar con este decorador. ¿Quizás podría usar una metaclase personalizada y realizar esta misma verificación durante la nueva ...?
Carl G
3
Cualquiera que intente usar esto con método estático o método de clase querrá leer este PEP: python.org/dev/peps/pep-0232 No estoy seguro de que sea posible porque no puede establecer un atributo en una clase / método estático y creo que engullen hasta los atributos de función personalizados cuando se aplican a una función.
Carl G
Justo lo que estaba buscando, para mi ORM basado en DBM ... Gracias, amigo.
Coyote
Debe usar inspect.getmro(cls)para procesar todas las clases base en el decorador de clases para admitir la herencia.
Schlamar
1
oh, en realidad parece inspectque el rescate stackoverflow.com/a/1911287/202168
Anentropic
16

Desde python 3.6 puede usar object.__set_name__para lograr esto de una manera muy simple. El documento establece que __set_name__"se llama en el momento en que se crea el propietario de la clase propietaria ". Aquí hay un ejemplo:

class class_decorator:
    def __init__(self, fn):
        self.fn = fn

    def __set_name__(self, owner, name):
        # do something with owner, i.e.
        print(f"decorating {self.fn} and using {owner}")
        self.fn.class_name = owner.__name__

        # then replace ourself with the original method
        setattr(owner, name, self.fn)

Tenga en cuenta que se llama en el momento de la creación de la clase:

>>> class A:
...     @class_decorator
...     def hello(self, x=42):
...         return x
...
decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
>>> A.hello
<function __main__.A.hello(self, x=42)>
>>> A.hello.class_name
'A'
>>> a = A()
>>> a.hello()
42

Si desea saber más sobre cómo se crean las clases y, en particular, exactamente cuándo __set_name__se llama, puede consultar la documentación sobre "Creación del objeto de clase" .

tyrion
fuente
1
¿Cómo se vería eso para usar el decorador con parámetros? Por ejemplo@class_decorator('test', foo='bar')
luckydonald
2
@luckydonald Puede abordarlo de manera similar a los decoradores normales que toman argumentos . Just havedef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Matt Eding
Wow, muchas gracias. No lo sabía, __set_name__aunque he estado usando Python 3.6+ durante mucho tiempo.
kawing-chiu
Hay un inconveniente de este método: el verificador estático no entiende esto en absoluto. Mypy pensará que hellono es un método, sino un objeto de tipo class_decorator.
kawing-chiu
@ kawing-chiu Si nada más funciona, puede usar an if TYPE_CHECKINGpara definir class_decoratorcomo un decorador normal que devuelve el tipo correcto.
tyrion
15

Como han señalado otros, la clase no se ha creado en el momento en que se llama al decorador. Sin embargo , es posible anotar el objeto de función con los parámetros del decorador y luego volver a decorar la función en el __new__método de la metaclase . Deberá acceder al __dict__atributo de la función directamente, ya que al menos para mí, func.foo = 1resultó en un AttributeError.

Mark Visser
fuente
6
setattrdebe usarse en lugar de acceder__dict__
schlamar
7

Como sugiere Mark:

  1. Cualquier decorador se llama ANTES de que se construya la clase, por lo que es desconocido para el decorador.
  2. Podemos etiquetar estos métodos y realizar los posprocesos necesarios más adelante.
  3. Tenemos dos opciones para el posprocesamiento: automáticamente al final de la definición de la clase o en algún lugar antes de que se ejecute la aplicación. Prefiero la primera opción usando una clase base, pero también puede seguir el segundo enfoque.

Este código muestra cómo puede funcionar esto usando el posprocesamiento automático:

def expose(**kw):
    "Note that using **kw you can tag the function with any parameters"
    def wrap(func):
        name = func.func_name
        assert not name.startswith('_'), "Only public methods can be exposed"

        meta = func.__meta__ = kw
        meta['exposed'] = True
        return func

    return wrap

class Exposable(object):
    "Base class to expose instance methods"
    _exposable_ = None  # Not necessary, just for pylint

    class __metaclass__(type):
        def __new__(cls, name, bases, state):
            methods = state['_exposed_'] = dict()

            # inherit bases exposed methods
            for base in bases:
                methods.update(getattr(base, '_exposed_', {}))

            for name, member in state.items():
                meta = getattr(member, '__meta__', None)
                if meta is not None:
                    print "Found", name, meta
                    methods[name] = member
            return type.__new__(cls, name, bases, state)

class Foo(Exposable):
    @expose(any='parameter will go', inside='__meta__ func attribute')
    def foo(self):
        pass

class Bar(Exposable):
    @expose(hide=True, help='the great bar function')
    def bar(self):
        pass

class Buzz(Bar):
    @expose(hello=False, msg='overriding bar function')
    def bar(self):
        pass

class Fizz(Foo):
    @expose(msg='adding a bar function')
    def bar(self):
        pass

print('-' * 20)
print("showing exposed methods")
print("Foo: %s" % Foo._exposed_)
print("Bar: %s" % Bar._exposed_)
print("Buzz: %s" % Buzz._exposed_)
print("Fizz: %s" % Fizz._exposed_)

print('-' * 20)
print('examine bar functions')
print("Bar.bar: %s" % Bar.bar.__meta__)
print("Buzz.bar: %s" % Buzz.bar.__meta__)
print("Fizz.bar: %s" % Fizz.bar.__meta__)

La salida rinde:

Found foo {'inside': '__meta__ func attribute', 'any': 'parameter will go', 'exposed': True}
Found bar {'hide': True, 'help': 'the great bar function', 'exposed': True}
Found bar {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Found bar {'msg': 'adding a bar function', 'exposed': True}
--------------------
showing exposed methods
Foo: {'foo': <function foo at 0x7f7da3abb398>}
Bar: {'bar': <function bar at 0x7f7da3abb140>}
Buzz: {'bar': <function bar at 0x7f7da3abb0c8>}
Fizz: {'foo': <function foo at 0x7f7da3abb398>, 'bar': <function bar at 0x7f7da3abb488>}
--------------------
examine bar functions
Bar.bar: {'hide': True, 'help': 'the great bar function', 'exposed': True}
Buzz.bar: {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Fizz.bar: {'msg': 'adding a bar function', 'exposed': True}

Tenga en cuenta que en este ejemplo:

  1. Podemos anotar cualquier función con cualquier parámetro arbitrario.
  2. Cada clase tiene sus propios métodos expuestos.
  3. También podemos heredar métodos expuestos.
  4. Los métodos pueden anular a medida que se actualiza la función de exposición.

Espero que esto ayude

asterio gonzalez
fuente
4

Como indicó Ants, no puede obtener una referencia a la clase desde dentro de la clase. Sin embargo, si está interesado en distinguir entre diferentes clases (sin manipular el objeto de tipo de clase real), puede pasar una cadena para cada clase. También puede pasar cualquier otro parámetro que desee al decorador utilizando decoradores de estilo de clase.

class Decorator(object):
    def __init__(self,decoratee_enclosing_class):
        self.decoratee_enclosing_class = decoratee_enclosing_class
    def __call__(self,original_func):
        def new_function(*args,**kwargs):
            print 'decorating function in ',self.decoratee_enclosing_class
            original_func(*args,**kwargs)
        return new_function


class Bar(object):
    @Decorator('Bar')
    def foo(self):
        print 'in foo'

class Baz(object):
    @Decorator('Baz')
    def foo(self):
        print 'in foo'

print 'before instantiating Bar()'
b = Bar()
print 'calling b.foo()'
b.foo()

Huellas dactilares:

before instantiating Bar()
calling b.foo()
decorating function in  Bar
in foo

Además, vea la página de Bruce Eckel sobre decoradores.

Ross Rogers
fuente
Gracias por confirmar mi deprimente conclusión de que esto no es posible. También podría usar una cadena que califique completamente el módulo / clase ('módulo.Clase'), almacenar la (s) cadena (s) hasta que las clases se hayan cargado por completo, luego recuperar las clases yo mismo con la importación. Esa parece una forma lamentablemente no SECA de realizar mi tarea.
Carl G
No necesita usar una clase para este tipo de decorador: el enfoque idiomático es usar un nivel adicional de funciones anidadas dentro de la función decoradora. Sin embargo, si opta por clases, sería mejor no usar mayúsculas en el nombre de la clase para que la decoración en sí parezca "estándar", es decir @decorator('Bar'), a diferencia de @Decorator('Bar').
Erik Kaplun
4

Lo que hace flask-classy es crear un caché temporal que almacena en el método, luego usa algo más (el hecho de que Flask registrará las clases usando un registermétodo de clase) para realmente envolver el método.

Puede reutilizar este patrón, esta vez usando una metaclase para que pueda ajustar el método en el momento de la importación.

def route(rule, **options):
    """A decorator that is used to define custom routes for methods in
    FlaskView subclasses. The format is exactly the same as Flask's
    `@app.route` decorator.
    """

    def decorator(f):
        # Put the rule cache on the method itself instead of globally
        if not hasattr(f, '_rule_cache') or f._rule_cache is None:
            f._rule_cache = {f.__name__: [(rule, options)]}
        elif not f.__name__ in f._rule_cache:
            f._rule_cache[f.__name__] = [(rule, options)]
        else:
            f._rule_cache[f.__name__].append((rule, options))

        return f

    return decorator

En la clase real (puede hacer lo mismo usando una metaclase):

@classmethod
def register(cls, app, route_base=None, subdomain=None, route_prefix=None,
             trailing_slash=None):

    for name, value in members:
        proxy = cls.make_proxy_method(name)
        route_name = cls.build_route_name(name)
        try:
            if hasattr(value, "_rule_cache") and name in value._rule_cache:
                for idx, cached_rule in enumerate(value._rule_cache[name]):
                    # wrap the method here

Fuente: https://github.com/apiguy/flask-classy/blob/master/flask_classy.py

charlax
fuente
ese es un patrón útil, pero esto no resuelve el problema de que un decorador de métodos pueda hacer referencia a la clase principal del método al que se aplica
Anentropic
Actualicé mi respuesta para ser más explícito sobre cómo esto puede ser útil para obtener acceso a la clase en el momento de la importación (es decir, usando una metaclase + almacenamiento en caché del parámetro decorador en el método).
charlax
3

El problema es que cuando se llama al decorador, la clase aún no existe. Prueba esto:

def loud_decorator(func):
    print("Now decorating %s" % func)
    def decorated(*args, **kwargs):
        print("Now calling %s with %s,%s" % (func, args, kwargs))
        return func(*args, **kwargs)
    return decorated

class Foo(object):
    class __metaclass__(type):
        def __new__(cls, name, bases, dict_):
            print("Creating class %s%s with attributes %s" % (name, bases, dict_))
            return type.__new__(cls, name, bases, dict_)

    @loud_decorator
    def hello(self, msg):
        print("Hello %s" % msg)

Foo().hello()

Este programa generará:

Now decorating <function hello at 0xb74d35dc>
Creating class Foo(<type 'object'>,) with attributes {'__module__': '__main__', '__metaclass__': <class '__main__.__metaclass__'>, 'hello': <function decorated at 0xb74d356c>}
Now calling <function hello at 0xb74d35dc> with (<__main__.Foo object at 0xb74ea1ac>, 'World'),{}
Hello World

Como puede ver, tendrá que encontrar una forma diferente de hacer lo que quiera.

Hormigas Aasma
fuente
cuando uno define una función, la función no existe todavía, pero uno puede llamar recursivamente a la función desde dentro de sí mismo. Supongo que esta es una característica del lenguaje específica de las funciones y no está disponible para las clases.
Carl G
DGGenuine: La función solo se llama y, por lo tanto, la función accede a sí misma, solo después de que se haya creado por completo. En este caso, la clase no puede estar completa cuando se llama al decorador, ya que la clase debe esperar el resultado del decorador, que será almacenado como uno de los atributos de la clase.
u0b34a0f6ae
3

He aquí un ejemplo sencillo:

def mod_bar(cls):
    # returns modified class

    def decorate(fcn):
        # returns decorated function

        def new_fcn(self):
            print self.start_str
            print fcn(self)
            print self.end_str

        return new_fcn

    cls.bar = decorate(cls.bar)
    return cls

@mod_bar
class Test(object):
    def __init__(self):
        self.start_str = "starting dec"
        self.end_str = "ending dec" 

    def bar(self):
        return "bar"

La salida es:

>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec
nicodjimenez
fuente
1

Esta es una vieja pregunta pero se encontró con venusiana. http://venusian.readthedocs.org/en/latest/

Parece tener la capacidad de decorar métodos y darle acceso tanto a la clase como al método mientras lo hace. Tenga en cuenta que llamar setattr(ob, wrapped.__name__, decorated)no es la forma típica de usar venusian y en cierto modo frustra el propósito.

De cualquier manera ... el siguiente ejemplo está completo y debería ejecutarse.

import sys
from functools import wraps
import venusian

def logged(wrapped):
    def callback(scanner, name, ob):
        @wraps(wrapped)
        def decorated(self, *args, **kwargs):
            print 'you called method', wrapped.__name__, 'on class', ob.__name__
            return wrapped(self, *args, **kwargs)
        print 'decorating', '%s.%s' % (ob.__name__, wrapped.__name__)
        setattr(ob, wrapped.__name__, decorated)
    venusian.attach(wrapped, callback)
    return wrapped

class Foo(object):
    @logged
    def bar(self):
        print 'bar'

scanner = venusian.Scanner()
scanner.scan(sys.modules[__name__])

if __name__ == '__main__':
    t = Foo()
    t.bar()
eric.frederich
fuente
1

La función no sabe si es un método en el punto de definición, cuando se ejecuta el código del decorador. Solo cuando se accede a través del identificador de clase / instancia, puede conocer su clase / instancia. Para superar esta limitación, puede decorar por objeto descriptor para retrasar el código de decoración real hasta la hora de acceso / llamada:

class decorated(object):
    def __init__(self, func, type_=None):
        self.func = func
        self.type = type_

    def __get__(self, obj, type_=None):
        func = self.func.__get__(obj, type_)
        print('accessed %s.%s' % (type_.__name__, func.__name__))
        return self.__class__(func, type_)

    def __call__(self, *args, **kwargs):
        name = '%s.%s' % (self.type.__name__, self.func.__name__)
        print('called %s with args=%s kwargs=%s' % (name, args, kwargs))
        return self.func(*args, **kwargs)

Esto le permite decorar métodos individuales (estáticos | clase):

class Foo(object):
    @decorated
    def foo(self, a, b):
        pass

    @decorated
    @staticmethod
    def bar(a, b):
        pass

    @decorated
    @classmethod
    def baz(cls, a, b):
        pass

class Bar(Foo):
    pass

Ahora puedes usar el código del decorador para la introspección ...

>>> Foo.foo
accessed Foo.foo
>>> Foo.bar
accessed Foo.bar
>>> Foo.baz
accessed Foo.baz
>>> Bar.foo
accessed Bar.foo
>>> Bar.bar
accessed Bar.bar
>>> Bar.baz
accessed Bar.baz

... y para cambiar el comportamiento de la función:

>>> Foo().foo(1, 2)
accessed Foo.foo
called Foo.foo with args=(1, 2) kwargs={}
>>> Foo.bar(1, b='bcd')
accessed Foo.bar
called Foo.bar with args=(1,) kwargs={'b': 'bcd'}
>>> Bar.baz(a='abc', b='bcd')
accessed Bar.baz
called Bar.baz with args=() kwargs={'a': 'abc', 'b': 'bcd'}
aurzenligl
fuente
Lamentablemente, este enfoque es funcionalmente equivalente a la igualmente inaplicable respuesta de Will McCutchen . Tanto esta como aquella respuesta obtienen la clase deseada en el momento de la llamada al método en lugar de en el momento de la decoración del método , como lo requiere la pregunta original. El único medio razonable de obtener esta clase en un momento lo suficientemente temprano es hacer una introspección sobre todos los métodos en el momento de la definición de la clase (por ejemplo, a través de un decorador de clases o metaclase). </sigh>
Cecil Curry
1

Como han señalado otras respuestas, el decorador es algo funcional, no puede acceder a la clase a la que pertenece este método ya que la clase aún no se ha creado. Sin embargo, está totalmente bien usar un decorador para "marcar" la función y luego usar técnicas de metaclase para tratar el método más tarde, porque en la __new__etapa, la clase ha sido creada por su metaclase.

He aquí un ejemplo sencillo:

Usamos @fieldpara marcar el método como un campo especial y tratarlo en la metaclase.

def field(fn):
    """Mark the method as an extra field"""
    fn.is_field = True
    return fn

class MetaEndpoint(type):
    def __new__(cls, name, bases, attrs):
        fields = {}
        for k, v in attrs.items():
            if inspect.isfunction(v) and getattr(k, "is_field", False):
                fields[k] = v
        for base in bases:
            if hasattr(base, "_fields"):
                fields.update(base._fields)
        attrs["_fields"] = fields

        return type.__new__(cls, name, bases, attrs)

class EndPoint(metaclass=MetaEndpoint):
    pass


# Usage

class MyEndPoint(EndPoint):
    @field
    def foo(self):
        return "bar"

e = MyEndPoint()
e._fields  # {"foo": ...}
ospider
fuente
Tiene un error tipográfico en esta línea: if inspect.isfunction(v) and getattr(k, "is_field", False):debería ser getattr(v, "is_field", False)en su lugar.
EvilTosha
0

Tendrá acceso a la clase del objeto en el que se llama al método en el método decorado que debe devolver su decorador. Al igual que:

def decorator(method):
    # do something that requires view's class
    def decorated(self, *args, **kwargs):
        print 'My class is %s' % self.__class__
        method(self, *args, **kwargs)
    return decorated

Usando su clase ModelA, esto es lo que hace:

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>
Will McCutchen
fuente
1
Gracias, pero esta es exactamente la solución a la que hice referencia en mi pregunta que no me funciona. Estoy tratando de implementar un patrón de observador usando decoradores y nunca podré llamar al método en el contexto correcto desde mi despachador de observación si no tengo la clase en algún momento mientras agrego el método al despachador de observación. Obtener la clase en la llamada al método no me ayuda a llamar correctamente al método en primer lugar.
Carl G
Vaya, perdón por mi pereza al no leer toda tu pregunta.
Will McCutchen
0

Solo quiero agregar mi ejemplo, ya que tiene todas las cosas que se me ocurren para acceder a la clase desde el método decorado. Utiliza un descriptor como sugiere @tyrion. El decorador puede tomar argumentos y pasarlos al descriptor. Puede tratar tanto con un método en una clase como con una función sin una clase.

import datetime as dt
import functools

def dec(arg1):
    class Timed(object):
        local_arg = arg1
        def __init__(self, f):
            functools.update_wrapper(self, f)
            self.func = f

        def __set_name__(self, owner, name):
            # doing something fancy with owner and name
            print('owner type', owner.my_type())
            print('my arg', self.local_arg)

        def __call__(self, *args, **kwargs):
            start = dt.datetime.now()
            ret = self.func(*args, **kwargs)
            time = dt.datetime.now() - start
            ret["time"] = time
            return ret
        
        def __get__(self, instance, owner):
            from functools import partial
            return partial(self.__call__, instance)
    return Timed

class Test(object):
    def __init__(self):
        super(Test, self).__init__()

    @classmethod
    def my_type(cls):
        return 'owner'

    @dec(arg1='a')
    def decorated(self, *args, **kwargs):
        print(self)
        print(args)
        print(kwargs)
        return dict()

    def call_deco(self):
        self.decorated("Hello", world="World")

@dec(arg1='a function')
def another(*args, **kwargs):
    print(args)
    print(kwargs)
    return dict()

if __name__ == "__main__":
    t = Test()
    ret = t.call_deco()
    another('Ni hao', world="shi jie")
    
Djiao
fuente