Um decorador de um método de instância pode acessar a classe?

109

Eu tenho algo parecido com o seguinte. Basicamente, preciso acessar a classe de um método de instância de um decorador usado no método de instância em sua definição.

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

O código como está fornece:

AttributeError: o objeto 'function' não tem atributo 'im_class'

Eu encontrei perguntas / respostas semelhantes - o decorador Python faz a função esquecer que ela pertence a uma classe e Get class no decorador Python - mas elas dependem de uma solução alternativa que captura a instância em tempo de execução ao capturar o primeiro parâmetro. No meu caso, chamarei o método com base nas informações coletadas de sua classe, portanto, mal posso esperar a chegada de uma chamada.

Carl G
fonte

Respostas:

68

Se você estiver usando Python 2.6 ou posterior, poderá usar um decorador de classe, talvez algo assim (aviso: código não testado).

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

O decorador de método marca o método como um que é de interesse, adicionando um atributo "use_class" - funções e métodos também são objetos, portanto, você pode anexar metadados adicionais a eles.

Depois que a classe foi criada, o decorador de classe passa por todos os métodos e faz o que for necessário nos métodos que foram marcados.

Se você quiser que todos os métodos sejam afetados, pode deixar de fora o decorador de método e apenas usar o decorador de classe.

Dave Kirby
fonte
2
Obrigado, acho que este é o caminho a seguir. Apenas uma linha extra de código para qualquer classe que eu queira usar neste decorador. Talvez eu pudesse usar uma metaclasse personalizada e executar essa mesma verificação durante o novo ...?
Carl G
3
Qualquer pessoa que tentar usar isso com método estático ou método de classe vai querer ler este PEP: python.org/dev/peps/pep-0232 Não tenho certeza se é possível porque você não pode definir um atributo em um método de classe / estático e acho que eles engolem até quaisquer atributos de função personalizados quando eles são aplicados a uma função.
Carl G
Exatamente o que eu estava procurando, para meu ORM baseado em DBM ... Obrigado, cara.
Coyote21,
Você deve usar inspect.getmro(cls)para processar todas as classes básicas no decorador de classe para oferecer suporte à herança.
Schlamar de
1
oh, na verdade, parece que inspecto resgate stackoverflow.com/a/1911287/202168
Anentrópico
16

Desde o python 3.6, você pode usar object.__set_name__para fazer isso de uma maneira muito simples. O documento afirma que __set_name__é "chamado no momento em que o proprietário da classe proprietária é criado". Aqui está um exemplo:

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)

Observe que ele é chamado no momento da criação da classe:

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

Se você quiser saber mais sobre como as classes são criadas e, em particular, exatamente quando __set_name__é chamado, você pode consultar a documentação em "Criação do objeto de classe" .

tirião
fonte
1
Como seria usar o decorador com parâmetros? Por exemplo@class_decorator('test', foo='bar')
luckydonald
2
@luckydonald Você pode abordar isso de forma semelhante a decoradores normais que aceitam argumentos . Basta terdef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Matt Eding
Uau, muito obrigado. Não sabia, __set_name__embora eu use o Python 3.6+ por um longo tempo.
kawing-chiu
Há uma desvantagem desse método: o verificador estático não entende isso de forma alguma. Mypy vai pensar que hellonão é um método, mas sim um objeto do tipo class_decorator.
kawing-chiu
@ kawing-chiu Se nada mais funcionar, você pode usar um if TYPE_CHECKINGpara definir class_decoratorcomo um decorador normal retornando o tipo correto.
tirião
15

Como outros apontaram, a classe não foi criada no momento em que o decorador é chamado. No entanto , é possível anotar o objeto de função com os parâmetros do decorador e, em seguida, redecorar a função no __new__método da metaclasse . Você precisará acessar o __dict__atributo da função diretamente, pois pelo menos para mim, func.foo = 1resultou em um AttributeError.

Mark Visser
fonte
6
setattrdeve ser usado em vez de acessar__dict__
schlamar
7

Como sugere Mark:

  1. Qualquer decorador é chamado ANTES da classe ser construída, portanto, é desconhecido para o decorador.
  2. Podemos marcar esses métodos e fazer qualquer pós-processamento necessário posteriormente.
  3. Temos duas opções de pós-processamento: automaticamente no final da definição da classe ou em algum lugar antes de o aplicativo ser executado. Eu prefiro a 1ª opção usando uma classe base, mas você também pode seguir a 2ª abordagem.

Este código mostra como isso pode funcionar usando o pós-processamento 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__)

A saída produz:

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}

Observe que neste exemplo:

  1. Podemos anotar qualquer função com qualquer parâmetro arbitrário.
  2. Cada classe tem seus próprios métodos expostos.
  3. Podemos herdar métodos expostos também.
  4. os métodos podem ser substituídos conforme o recurso de exposição é atualizado.

Espero que isto ajude

Astério Gonzalez
fonte
4

Como as formigas indicaram, você não pode obter uma referência para a classe de dentro da classe. No entanto, se você estiver interessado em distinguir entre classes diferentes (não manipular o objeto de tipo de classe real), você pode passar uma string para cada classe. Você também pode passar quaisquer outros parâmetros que desejar para o decorador usando decoradores de estilo de classe.

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

Impressões:

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

Além disso, consulte a página de Bruce Eckel sobre decoradores.

Ross Rogers
fonte
Obrigado por confirmar minha conclusão deprimente de que isso não é possível. Eu também poderia usar uma string que qualificasse totalmente o módulo / classe ('module.Class'), armazenar a (s) string (s) até que todas as classes tenham sido totalmente carregadas e, em seguida, recuperar as classes eu mesmo com a importação. Essa parece uma forma lamentavelmente não-SECA de realizar minha tarefa.
Carl G,
Você não precisa usar uma classe para esse tipo de decorador: a abordagem idiomática é usar um nível extra de funções aninhadas dentro da função do decorador. No entanto, se você for com classes, pode ser melhor não usar letras maiúsculas no nome da classe para fazer a decoração em si parecer "padrão", ou seja, @decorator('Bar')em vez de @Decorator('Bar').
Erik Kaplun
4

O que o flask-classy faz é criar um cache temporário que ele armazena no método, então ele usa outra coisa (o fato de que o Flask registrará as classes usando um registermétodo de classe) para realmente embrulhar o método.

Você pode reutilizar esse padrão, desta vez usando uma metaclasse para que possa agrupar o método no momento da importação.

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

Na aula real (você pode fazer o mesmo usando uma metaclasse):

@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

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

charlax
fonte
esse é um padrão útil, mas não resolve o problema de um decorador de método ser capaz de se referir à classe pai do método ao qual é aplicado
Anentrópico
Atualizei minha resposta para ser mais explícito como isso pode ser útil para obter acesso à classe no momento da importação (ou seja, usando uma metaclasse + caching o parâmetro do decorador no método).
charlax
3

O problema é que quando o decorador é chamado a classe ainda não existe. Experimente isto:

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 irá gerar:

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 você vê, terá que descobrir uma maneira diferente de fazer o que deseja.

Formigas Aasma
fonte
quando alguém define uma função, a função ainda não existe, mas pode-se chamar recursivamente a função de dentro de si. Acho que esse é um recurso de linguagem específico para funções e não disponível para aulas.
Carl G
DGGenuine: A função só é chamada e, portanto, acessa a si mesma, somente depois de ter sido criada completamente. Nesse caso, a classe não pode ser concluída quando o decorador é chamado, pois a classe deve aguardar o resultado do decorador, que ficará armazenado como um dos atributos da classe.
u0b34a0f6ae
3

Aqui está um exemplo simples:

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"

O resultado é:

>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec
Nicodjimenez
fonte
1

Essa é uma pergunta antiga, mas me deparei com venusiana. http://venusian.readthedocs.org/en/latest/

Parece ter a capacidade de decorar métodos e fornecer acesso à classe e ao método enquanto o faz. Observe que o chamado setattr(ob, wrapped.__name__, decorated)não é a maneira típica de usar venusiano e, de certa forma, vai contra o propósito.

De qualquer forma ... o exemplo abaixo está completo e deve ser executado.

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
fonte
1

A função não sabe se é um método no ponto de definição, quando o código do decorador é executado. Somente quando é acessado por meio do identificador de classe / instância, ele pode saber sua classe / instância. Para superar essa limitação, você pode decorar por objeto descritor para atrasar o código de decoração real até o tempo de acesso / chamada:

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)

Isso permite que você decore métodos individuais (classe estática):

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

Agora você pode usar o código do decorador para introspecção ...

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

... e para mudar o comportamento da função:

>>> 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
fonte
Infelizmente, esta abordagem é funcionalmente equivalente a Will McCutchen 's resposta igualmente inaplicável . Esta e aquela resposta obtêm a classe desejada na hora da chamada do método , em vez da hora da decoração do método , conforme exigido pela pergunta original. O único meio razoável de obter essa classe em um momento suficientemente precoce é introspectar todos os métodos no momento da definição da classe (por exemplo, por meio de um decorador de classe ou metaclasse). </sigh>
Cecil Curry
1

Como outras respostas apontaram, decorador é uma coisa funcional, você não pode acessar a classe à qual esse método pertence, pois a classe ainda não foi criada. No entanto, está totalmente ok usar um decorador para "marcar" a função e, em seguida, usar técnicas de metaclasse para lidar com o método mais tarde, porque no __new__estágio, a classe foi criada por sua metaclasse.

Aqui está um exemplo simples:

Usamos @fieldpara marcar o método como um campo especial e lidar com ele na metaclasse.

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
fonte
Você tem um erro de digitação nesta linha: if inspect.isfunction(v) and getattr(k, "is_field", False):deveria ser getattr(v, "is_field", False).
EvilTosha
0

Você terá acesso à classe do objeto no qual o método está sendo chamado no método decorado que seu decorador deve retornar. Igual a:

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 sua classe ModelA, aqui está o que isso faz:

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>
Will McCutchen
fonte
1
Obrigado, mas esta é exatamente a solução que mencionei na minha pergunta que não funciona para mim. Estou tentando implementar um padrão de observador usando decoradores e nunca serei capaz de chamar o método no contexto correto do meu despachante de observação se não tiver a classe em algum ponto ao adicionar o método ao despachante de observação. Obter a classe na chamada de método não me ajuda a chamar o método corretamente em primeiro lugar.
Carl G
Uau, desculpe pela minha preguiça em não ler sua pergunta inteira.
Will McCutchen,
0

Eu só quero adicionar meu exemplo, pois ele contém todas as coisas que eu poderia pensar para acessar a classe a partir do método decorado. Ele usa um descritor como sugere @tyrion. O decorador pode pegar argumentos e passá-los para o descritor. Ele pode lidar com um método em uma classe ou uma função sem uma classe.

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
fonte