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


109

J'ai quelque chose à peu près comme ce qui suit. Fondamentalement, j'ai besoin d'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: l'objet 'function' n'a pas d'attribut 'im_class'

J'ai trouvé des questions / réponses similaires - le décorateur Python fait oublier à la fonction qu'il appartient à une classe et la classe Get dans le décorateur Python - mais ceux-ci reposent sur une solution de contournement qui saisit l'instance au moment de l'exécution en saisissant le premier paramètre. Dans mon cas, j'appellerai la méthode en fonction des informations glanées dans sa classe, donc j'ai hâte qu'un appel arrive.

Réponses:


68

Si vous utilisez Python 2.6 ou une version ultérieure, vous pouvez utiliser un décorateur de classe, peut-être quelque chose comme ça (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 méthodes sont également des objets, vous pouvez donc leur attacher des métadonnées supplémentaires.

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

Si vous voulez que toutes les méthodes soient affectées, vous pouvez omettre le décorateur de méthode et utiliser simplement le décorateur de classe.


2
Merci, je pense que c'est la voie à suivre. Juste une ligne de code supplémentaire pour toute classe que je voudrais utiliser ce décorateur. Peut-être que je pourrais utiliser une métaclasse personnalisée et effectuer cette même vérification lors d'un nouveau ...?
Carl G

3
Quiconque essaie de l'utiliser avec une méthode statique ou une méthode de classe voudra lire ce PEP: python.org/dev/peps/pep-0232 Pas sûr que ce soit possible car vous ne pouvez pas définir un attribut sur une méthode de classe / statique et je pense qu'ils gobent tous les attributs de fonction personnalisés lorsqu'ils sont appliqués à une fonction.
Carl G

Juste ce que je cherchais, pour mon ORM basé sur DBM ... Merci, mec.
Coyote21

Vous devez utiliser inspect.getmro(cls)pour traiter toutes les classes de base dans le décorateur de classe pour prendre en charge l'héritage.
schlamar

1
oh, en fait, cela ressemble inspectà la rescousse stackoverflow.com/a/1911287/202168
Anentropic

16

Depuis python 3.6, vous pouvez utiliser object.__set_name__pour accomplir cela de manière très simple. Le document indique qu'il __set_name__est "appelé au moment où le propriétaire de la classe propriétaire est créé". 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 des classes et en particulier sur le moment exact de leur __set_name__appel, vous pouvez vous référer à la documentation sur "Création de l'objet de classe" .


1
À quoi cela ressemblerait-il pour utiliser le décorateur avec des paramètres? Par exemple@class_decorator('test', foo='bar')
luckydonald

2
@luckydonald Vous pouvez l'approcher de la même manière que les décorateurs normaux qui acceptent des arguments . Just havedef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Matt Eding

Wow, merci beaucoup. Je ne savais pas __set_name__bien que j'utilise Python 3.6+ depuis longtemps.
kawing-chiu le

Il y a un inconvénient à cette méthode: le vérificateur statique ne comprend pas du tout cela. Mypy pensera que ce hellon'est pas une méthode, mais plutôt un objet de type class_decorator.
kawing-chiu

@ kawing-chiu Si rien d'autre ne fonctionne, vous pouvez utiliser un if TYPE_CHECKINGpour définir class_decoratorcomme un décorateur normal retournant le type correct.
tyrion

15

Comme d'autres l'ont souligné, 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 du décorateur, puis de re-décorer la fonction dans la __new__méthode de la métaclasse . Vous devrez accéder __dict__directement à l'attribut de la fonction , car au moins pour moi, cela a func.foo = 1abouti à une AttributeError.


6
setattrdevrait être utilisé au lieu d'accéder__dict__
schlamar

7

Comme Mark le suggère:

  1. Tout décorateur est appelé AVANT que la classe ne soit construite, il est donc inconnu du décorateur.
  2. Nous pouvons étiqueter ces méthodes et effectuer ultérieurement tout post-traitement nécessaire.
  3. Nous avons deux options pour le post-traitement: automatiquement à la fin de la définition de 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 sortie 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 n'importe quelle fonction avec n'importe quel paramètre arbitraire.
  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 remplacées lorsque la fonction d'exposition est mise à jour.

J'espère que cela t'aides


4

Comme Ants l'a indiqué, vous ne pouvez pas obtenir de référence à la classe à partir de la classe. Cependant, 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 en utilisant des décorateurs de style 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()

Impressions:

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

Voir également la page de Bruce Eckel sur les décorateurs.


Merci d'avoir confirmé ma conclusion déprimante que ce n'est pas possible. Je pourrais également utiliser une chaîne qui qualifie pleinement le module / classe ('module.Class'), stocker la ou les chaînes jusqu'à ce que toutes les classes soient complètement chargées, puis récupérer les classes moi-même avec l'importation. Cela semble être une façon terriblement peu sèche d'accomplir ma tâche.
Carl G

Vous n'avez pas besoin d'utiliser une classe pour ce type de décorateur: l'approche idiomatique consiste à utiliser un niveau supplémentaire de fonctions imbriquées dans la fonction de décorateur. Cependant, si vous optez pour les classes, il serait peut-être préférable de ne pas utiliser de majuscules dans le nom de la classe pour donner à la décoration elle-même un aspect "standard", c'est- @decorator('Bar')à- dire par opposition à @Decorator('Bar').
Erik Kaplun

4

Ce que fait flask-classy est de créer un cache temporaire qu'il stocke sur la méthode, puis il utilise autre chose (le fait que Flask enregistrera les classes en utilisant une registerméthode de classe) pour encapsuler réellement la méthode.

Vous pouvez réutiliser ce modèle, cette fois en utilisant une métaclasse afin de pouvoir encapsuler 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.__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

Sur la classe réelle (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


c'est un modèle utile, mais cela ne résout pas le problème d'un décorateur de méthode pouvant faire référence à la classe parente de la méthode à laquelle il est appliqué
Anentropic

J'ai mis à jour ma réponse pour être plus explicite en quoi cela peut être utile pour accéder à la classe au moment de l'importation (c'est-à-dire en utilisant une métaclasse + mise en cache du paramètre décorateur sur la méthode).
charlax

3

Le problème est que lorsque le décorateur est appelé, 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 affichera:

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 manière différente de faire ce que vous voulez.


quand on définit une fonction, la fonction n'existe pas encore, mais on est capable d'appeler récursivement la fonction de l'intérieur d'elle-même. Je suppose que c'est une fonctionnalité de langage spécifique aux fonctions et non disponible pour les classes.
Carl G

DGG Genuine: La fonction n'est appelée et la fonction accède donc à elle-même qu'après avoir été complètement créée. Dans ce cas, la classe ne peut pas être complète lorsque le décorateur est appelé, car la classe doit attendre le résultat du décorateur, qui sera stocké comme l'un des attributs de la classe.
u0b34a0f6ae

3

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

1

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

Il semble avoir la capacité de décorer des méthodes et de vous donner accès à la fois à la classe et à la méthode. Notez que l'appel setattr(ob, wrapped.__name__, decorated)n'est pas la manière typique d'utiliser vénusien et va quelque peu à l'encontre du but.

Quoi qu'il en soit ... l'exemple ci-dessous est complet et devrait fonctionner.

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

1

Function ne sait pas s'il s'agit d'une méthode au point de définition, lorsque le code du décorateur s'exécute. Ce n'est qu'en cas d'accès via l'identifiant de classe / instance qu'il peut connaître sa classe / instance. Pour surmonter cette limitation, vous pouvez décorer par objet descripteur pour retarder le code de décoration réel jusqu'au temps d'accès / d'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 (statiques | 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 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 des fonctions:

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

Malheureusement, cette approche est fonctionnellement équivalent à Will McCutchen de réponse tout aussi inapplicable . Cette réponse et cette réponse obtiennent la classe souhaitée au moment de l' appel de la méthode plutôt qu'au moment de la décoration de la méthode , comme l'exige la question d'origine. Le seul moyen raisonnable d'obtenir cette classe à un moment suffisamment précoce est d'introspecter toutes les méthodes au moment de la définition de la classe (par exemple, via un décorateur de classe ou une métaclasse). </sigh>
Cecil Curry

1

Comme d'autres réponses l'ont souligné, le décorateur est une chose très fonctionnelle, vous ne pouvez pas accéder à la classe à laquelle appartient cette méthode car la classe n'a pas encore été créée. Cependant, il est tout à fait correct d'utiliser un décorateur pour "marquer" la fonction et ensuite utiliser des techniques de métaclasse pour traiter la méthode plus tard, car à l' __new__étape, la classe a été créée par sa métaclasse.

Voici un exemple simple:

Nous utilisons @fieldpour marquer la méthode comme un champ spécial et la traiter dans une métaclasse.

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": ...}

Vous avez une faute de frappe dans cette ligne: if inspect.isfunction(v) and getattr(k, "is_field", False):il devrait l'être à la getattr(v, "is_field", False)place.
EvilTosha

0

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

1
Merci mais c'est exactement la solution à laquelle j'ai fait référence dans ma question qui ne fonctionne pas pour moi. J'essaie d'implémenter un modèle d'observateur à l'aide de décorateurs et je ne pourrai jamais appeler la méthode dans le bon contexte à partir de mon répartiteur d'observation si je n'ai pas la classe à un moment donné lors de l'ajout de la méthode au répartiteur d'observation. Obtenir la classe lors de l'appel de méthode ne m'aide pas à appeler correctement la méthode en premier lieu.
Carl G

Whoa, désolé pour ma paresse de ne pas avoir lu votre question en entier.
Will McCutchen

0

Je veux juste ajouter mon exemple car il a toutes les choses auxquelles je pourrais penser pour accéder à la classe à partir de la méthode décorée. Il utilise un descripteur comme le suggère @tyrion. Le décorateur peut prendre des arguments et les transmettre au descripteur. Il peut traiter à la fois une méthode dans une classe ou une fonction sans 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")
    
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.