web-dev-qa-db-fra.com

Python Decorator fait en sorte que la fonction oublie qu'elle appartient à une classe

J'essaie d'écrire un décorateur pour faire de la journalisation:

def logger(myFunc):
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__)
        return myFunc(*args, **keyargs)

    return new

class C(object):
    @logger
    def f():
        pass

C().f()

J'aimerais que ceci soit imprimé:

Entering C.f

mais au lieu de cela, je reçois ce message d'erreur:

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

Vraisemblablement, cela a quelque chose à voir avec la portée de «myFunc» dans «enregistreur», mais je ne sais pas quoi.

56
Charles Anderson

La réponse de Claudiu est correcte, mais vous pouvez également tricher en obtenant le nom de classe hors de l'argument self. Cela donnera des instructions de journal trompeuses en cas d'héritage, mais vous indiquera la classe de l'objet dont la méthode est appelée. Par exemple:

from functools import wraps  # use this to preserve function signatures and docstrings
def logger(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print "Entering %s.%s" % (args[0].__class__.__name__, func.__name__)
        return func(*args, **kwargs)
    return with_logging

class C(object):
    @logger
    def f(self):
        pass

C().f()

Comme je l'ai dit, cela ne fonctionnera pas correctement dans les cas où vous avez hérité d'une fonction d'une classe parent; dans ce cas, vous pourriez dire

class B(C):
    pass

b = B()
b.f()

et obtenez le message Entering B.f où vous voulez réellement obtenir le message Entering C.f puisque c'est la bonne classe. D'un autre côté, cela pourrait être acceptable, auquel cas je recommanderais cette approche à la suggestion de Claudiu.

43
Eli Courtwright

Les fonctions ne deviennent des méthodes qu'au moment de l'exécution. Autrement dit, lorsque vous obtenez C.f, vous obtenez une fonction liée (et C.f.im_class is C). Au moment où votre fonction est définie, il s’agit simplement d’une fonction simple, elle n’est liée à aucune classe. Cette fonction non liée et dissociée est ce qui est décoré par l'enregistreur.

self.__class__.__name__ vous donnera le nom de la classe, mais vous pouvez aussi utiliser des descripteurs pour accomplir ceci d'une manière un peu plus générale. Ce modèle est décrit dans un article de blog sur Decorators and Descriptors , et une implémentation de votre décorateur de bûcheron ressemblerait notamment à:

class logger(object):
    def __init__(self, func):
        self.func = func
    def __get__(self, obj, type=None):
        return self.__class__(self.func.__get__(obj, type))
    def __call__(self, *args, **kw):
        print 'Entering %s' % self.func
        return self.func(*args, **kw)

class C(object):
    @logger
    def f(self, x, y):
        return x+y

C().f(1, 2)
# => Entering <bound method C.f of <__main__.C object at 0x...>>

Il est évident que le résultat peut être amélioré (en utilisant, par exemple, getattr(self.func, 'im_class', None)), mais ce modèle général fonctionnera pour les méthodes et les fonctions. Cependant, cela fonctionnera avec not pour les classes de style ancien (mais n'utilisez pas celles-ci;)

25
ianb

Les idées proposées ici sont excellentes, mais présentent quelques inconvénients:

  1. inspect.getouterframes et args[0].__class__.__name__ ne conviennent pas aux fonctions simples et aux méthodes statiques.
  2. __get__ doit être dans une classe, qui est rejetée par @wraps.
  3. @wraps devrait lui-même mieux cacher les traces.

Donc, j'ai combiné certaines idées de cette page, des liens, des documents et ma propre tête,
et a finalement trouvé une solution qui ne présente pas les trois inconvénients ci-dessus.

En conséquence, method_decorator:

  • Connaît la classe à laquelle la méthode décorée est liée.
  • Masque les traces de décorateur en répondant aux attributs système plus correctement que functools.wraps().
  • Est couvert de tests unitaires pour les méthodes-instances, les méthodes-classes, les méthodes-statiques et les fonctions simples non liées.

Usage:

pip install method_decorator
from method_decorator import method_decorator

class my_decorator(method_decorator):
    # ...

Voir tests unitaires complets pour plus de détails sur l'utilisation .

Et voici juste le code de la classe method_decorator:

class method_decorator(object):

    def __init__(self, func, obj=None, cls=None, method_type='function'):
        # These defaults are OK for plain functions
        # and will be changed by __get__() for methods once a method is dot-referenced.
        self.func, self.obj, self.cls, self.method_type = func, obj, cls, method_type

    def __get__(self, obj=None, cls=None):
        # It is executed when decorated func is referenced as a method: cls.func or obj.func.

        if self.obj == obj and self.cls == cls:
            return self # Use the same instance that is already processed by previous call to this __get__().

        method_type = (
            'staticmethod' if isinstance(self.func, staticmethod) else
            'classmethod' if isinstance(self.func, classmethod) else
            'instancemethod'
            # No branch for plain function - correct method_type for it is already set in __init__() defaults.
        )

        return object.__getattribute__(self, '__class__')( # Use specialized method_decorator (or descendant) instance, don't change current instance attributes - it leads to conflicts.
            self.func.__get__(obj, cls), obj, cls, method_type) # Use bound or unbound method with this underlying func.

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

    def __getattribute__(self, attr_name): # Hiding traces of decoration.
        if attr_name in ('__init__', '__get__', '__call__', '__getattribute__', 'func', 'obj', 'cls', 'method_type'): # Our known names. '__class__' is not included because is used only with explicit object.__getattribute__().
            return object.__getattribute__(self, attr_name) # Stopping recursion.
        # All other attr_names, including auto-defined by system in self, are searched in decorated self.func, e.g.: __module__, __class__, __name__, __doc__, im_*, func_*, etc.
        return getattr(self.func, attr_name) # Raises correct AttributeError if name is not found in decorated self.func.

    def __repr__(self): # Special case: __repr__ ignores __getattribute__.
        return self.func.__repr__()
16
Denis Ryzhkov

Il semble que pendant la création de la classe, Python crée des objets de fonction normaux. Ils ne sont ensuite transformés en objets de méthode non liés. Sachant cela, c’est la seule façon pour moi de faire ce que vous voulez:

def logger(myFunc):
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__)
        return myFunc(*args, **keyargs)

    return new

class C(object):
    def f(self):
        pass
C.f = logger(C.f)
C().f()

Ceci produit le résultat souhaité.

Si vous voulez envelopper toutes les méthodes dans une classe, alors vous voulez probablement créer une fonction wrapClass, que vous pourriez ensuite utiliser comme ceci:

C = wrapClass(C)
7
Claudiu

J'ai trouvé une autre solution à un problème très similaire en utilisant la bibliothèque inspect. Lorsque le décorateur est appelé, même si la fonction n'est pas encore liée à la classe, vous pouvez inspecter la pile et découvrir quelle classe appelle le décorateur. Vous pouvez au moins obtenir le nom de chaîne de la classe, si c'est tout ce dont vous avez besoin (vous ne pouvez probablement pas le référencer pour le moment car il est en cours de création). Ensuite, vous n'avez rien à appeler après la création du cours.

import inspect

def logger(myFunc):
    classname = inspect.getouterframes(inspect.currentframe())[1][3]
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (classname, myFunc.__name__)
        return myFunc(*args, **keyargs)
    return new

class C(object):
    @logger
    def f(self):
        pass

C().f()

Bien que ce ne soit pas nécessairement meilleur que les autres, il s’agit du moyen seulement pour découvrir le nom de classe de la future méthode lors de l’appel du décorateur. Notez de ne pas conserver de références aux cadres dans la documentation de la bibliothèque inspect.

6
user398139

Les fonctions de classe doivent toujours prendre self comme premier argument, vous pouvez donc utiliser cela à la place de im_class.

def logger(myFunc):
    def new(self, *args, **keyargs):
        print 'Entering %s.%s' % (self.__class__.__name__, myFunc.__name__)
        return myFunc(self, *args, **keyargs)

    return new 

class C(object):
    @logger
    def f(self):
        pass
C().f()

au début, je voulais utiliser self.__name__ mais cela ne fonctionne pas car l'instance n'a pas de nom. vous devez utiliser self.__class__.__name__ pour obtenir le nom de la classe.

6
Asa Ayers

Vous pouvez également utiliser new.instancemethod() pour créer une méthode d'instance (liée ou non liée) à partir d'une fonction.

0
Andrew Beyer

Au lieu d'injecter du code de décoration au moment de la définition, lorsque la fonction ne sait pas qu'elle est une classe, retardez l'exécution de ce code jusqu'à ce que la fonction soit accédée/appelée. L'objet descripteur facilite l'injection tardive de son propre code, au moment de l'accès/de l'appel:

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

    def __get__(self, obj, type_=None):
        return self.__class__(self.func.__get__(obj, type_), 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)

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

Nous pouvons maintenant inspecter la classe au moment de l'accès (__get__) et au moment de l'appel (__call__). Ce mécanisme fonctionne aussi bien pour les méthodes simples que pour les méthodes statiques:

>>> Foo().foo(1, b=2)
called Foo.foo with args=(1,) kwargs={'b': 2}

Exemple complet sur: https://github.com/aurzenligl/study/blob/master/python-robotwrap/Example4.py

0
aurzenligl

Comme indiqué dans La réponse de Asa Ayers , vous n'avez pas besoin d'accéder à l'objet de classe. Il peut être intéressant de savoir que depuis Python 3.3, vous pouvez également utiliser __qualname__ , qui vous donne le nom complet:

>>> def logger(myFunc):
...     def new(*args, **keyargs):
...         print('Entering %s' % myFunc.__qualname__)
...         return myFunc(*args, **keyargs)
... 
...     return new
... 
>>> class C(object):
...     @logger
...     def f(self):
...         pass
... 
>>> C().f()
Entering C.f

Cela présente l’avantage supplémentaire de fonctionner également dans le cas de classes imbriquées, comme illustré dans cet exemple tiré de PEP 3155 :

>>> class C:
...   def f(): pass
...   class D:
...     def g(): pass
...
>>> C.__qualname__
'C'
>>> C.f.__qualname__
'C.f'
>>> C.D.__qualname__
'C.D'
>>> C.D.g.__qualname__
'C.D.g'

Notez également qu'en Python 3, l'attribut im_class a disparu. Par conséquent, si vous souhaitez vraiment accéder à la classe dans un décorateur, vous avez besoin d'une autre méthode. L'approche que j'utilise actuellement implique object.__set_name__ et est détaillée dans ma réponse à "Un décorateur Python d'une méthode d'instance peut-il accéder à la classe?"

0
tyrion