web-dev-qa-db-fra.com

Un décorateur Python d'une méthode d'instance peut-il accéder à la classe?

Salut, j'ai quelque chose comme ce qui suit. Fondamentalement, je dois accéder à la classe d'une méthode d'instance à partir d'un décorateur utilisé sur la méthode d'instance dans sa définition.

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

Le code tel quel donne

AttributeError: 'function' object has no attribute 'im_class'

J'ai trouvé des questions/réponses similaires - Le décorateur Python fait en sorte que la fonction oublie qu'elle appartient à une classe et Obtenir une classe dans Python decorator - premier paramètre. Dans mon cas, j'appellerai la méthode en fonction des informations glanées dans sa classe. Je ne peux donc pas attendre l'arrivée d'un appel.

Je vous remercie.

87
Carl G

Si vous utilisez Python 2.6 ou une version ultérieure, vous pouvez utiliser un décorateur de classe, ce qui pourrait ressembler à ceci (avertissement: code non testé).

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

Le décorateur de méthode marque la méthode comme étant intéressante en ajoutant un attribut "use_class" - les fonctions et les méthodes sont également des objets, ce qui vous permet d'y attacher des métadonnées supplémentaires.

Une fois la classe créée, le décorateur de la classe passe en revue toutes les méthodes et fait le nécessaire sur les méthodes marquées.

Si vous souhaitez que toutes les méthodes soient affectées, vous pouvez laisser le décorateur de méthode en place et utiliser simplement le décorateur de classe.

58
Dave Kirby

Comme d'autres l'ont fait remarquer, la classe n'a pas été créée au moment où le décorateur est appelé. Cependant , il est possible d'annoter l'objet fonction avec les paramètres de décorateur, puis de re-décorer la fonction dans la méthode __new__ de la métaclasse. Vous devrez accéder directement à l'attribut __dict__ de la fonction, car au moins pour moi, func.foo = 1 a provoqué une AttributeError.

13
Mark Visser

Comme indiqué par Ants, vous ne pouvez pas obtenir une référence à la classe à partir de la classe. Toutefois, si vous souhaitez faire la distinction entre différentes classes (sans manipuler l'objet de type de classe réel), vous pouvez transmettre une chaîne pour chaque classe. Vous pouvez également transmettre les autres paramètres de votre choix au décorateur à l'aide de décorateurs de style classique.

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

Impressions:

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

Aussi, voir la page de Bruce Eckel sur les décorateurs.

4
Ross Rogers

Voici un exemple simple: 

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 sortie est: 

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

Ce que flask-classy crée, c’est un cache temporaire qu’il stocke sur la méthode, puis utilise autre chose (le fait que Flask enregistre les classes en utilisant une méthode de classe register) pour envelopper la méthode.

Vous pouvez réutiliser ce modèle en utilisant cette fois une métaclasse pour envelopper la méthode au moment de l'importation.

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.__in f._rule_cache:
            f._rule_cache[f.__name__] = [(rule, options)]
        else:
            f._rule_cache[f.__name__].append((rule, options))

        return f

    return decorator

Sur la classe actuelle (vous pouvez faire la même chose en utilisant une métaclasse):

@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

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

3
charlax

Le problème est que lorsque le décorateur s'appelle, la classe n'existe pas encore. Essaye ça:

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

Ce programme produira:

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

Comme vous le voyez, vous allez devoir trouver une façon différente de faire ce que vous voulez.

3
Ants Aasma

Comme Mark le suggère:

  1. Tout décorateur s'appelle AVANT que la classe soit construite, de sorte que le décorateur l'ignore.
  2. Nous pouvons taguer ces méthodes et effectuer ultérieurement le post-traitement nécessaire.
  3. Nous avons deux options pour le post-traitement: automatiquement à la fin de la définition de la classe ou quelque part avant l’exécution de l’application. Je préfère la 1ère option en utilisant une classe de base, mais vous pouvez également suivre la 2ème approche.

Ce code montre comment cela peut fonctionner en utilisant le post-traitement automatique:

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 production donne:

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}

Notez que dans cet exemple:

  1. Nous pouvons annoter toute fonction avec des paramètres quelconques.
  2. Chaque classe a ses propres méthodes exposées.
  3. Nous pouvons également hériter des méthodes exposées.
  4. les méthodes peuvent être prioritaires lorsque la fonction d'exposition est mise à jour.

J'espère que cela t'aides

3
asterio gonzalez

C'est une vieille question, mais je suis tombé sur Vénusien. http://venusian.readthedocs.org/en/latest/

Il semble avoir la capacité de décorer les méthodes et de vous donner accès à la fois à la classe et à la méthode en même temps ... Remarquez que l'appel de setattr(ob, wrapped.__name__, decorated) n'est pas la méthode habituelle d'utilisation de la méthode vénusienne et qu'il en va quelque peu à l'encontre du but recherché.

Quoi qu'il en soit ... l'exemple ci-dessous est complet et devrait être exécuté.

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 __== '__main__':
    t = Foo()
    t.bar()
1
eric.frederich

Function ne sait pas si c'est une méthode au point de définition, quand le code de décorateur est exécuté. Ce n'est que lorsqu'il est accédé via l'identifiant de classe/instance qu'il peut connaître sa classe/instance. Pour surmonter cette limitation, vous pouvez décorer à l'aide d'un objet descripteur afin de retarder le code de décoration réel jusqu'à l'accès/l'heure de l'appel:

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)

Cela vous permet de décorer des méthodes individuelles (statique | classe):

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

Vous pouvez maintenant utiliser le code de décorateur pour l'introspection ...

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

... et pour changer le comportement de la fonction:

>>> 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'}
0
aurzenligl

Vous aurez accès à la classe de l'objet sur lequel la méthode est appelée dans la méthode décorée que votre décorateur doit renvoyer. Ainsi:

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

En utilisant votre classe ModelA, voici ce que cela fait:

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>
0
Will McCutchen

Depuis Python 3.6, vous pouvez utiliser object.__set_name__ pour accomplir ceci d’une manière très simple. La doc indique que __set_name__ est "appelé au moment de la création de la classe propriétaire propriétaire" . Voici un exemple:

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)

Notez qu'il est appelé au moment de la création de la 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

Si vous souhaitez en savoir plus sur la création de classes et en particulier sur l'appel de __set_name__, vous pouvez vous reporter à la documentation sur "Création de l'objet de classe" .

0
tyrion