98 votes

Comment obtenir toutes les méthodes d'une classe python avec un décorateur donné ?

Comment obtenir toutes les méthodes d'une classe A donnée qui sont décorées avec le @decorator2 ?

class A():
    def method_a(self):
      pass

    @decorator1
    def method_b(self, b):
      pass

    @decorator2
    def method_c(self, t=5):
      pass

2 votes

Avez-vous un contrôle sur le code source de "decorator2" ?

11 votes

Disons que non, juste pour garder l'intérêt. mais quand cela rend la solution beaucoup plus facile, cette solution m'intéresse aussi.

17 votes

+1 : "garder l'intérêt" : apprendre davantage de cette façon

138voto

ninjagecko Points 25709

Méthode 1 : Enregistrement de base du décorateur

J'ai déjà répondu à cette question ici : Appel de fonctions par index de tableau en Python \=)


Méthode 2 : Analyse du code source

Si vous n'avez pas le contrôle sur le classe définition qui est une interprétation de ce que vous voulez supposer, c'est impossible (sans code-reading-reflection), puisque par exemple le décorateur pourrait être un décorateur no-op (comme dans mon exemple lié) qui renvoie simplement la fonction non modifiée. (Néanmoins, si vous vous autorisez à envelopper/redéfinir les décorateurs, voir Méthode 3 : Convertir les décorateurs pour qu'ils soient "auto-conscients". alors vous trouverez une solution élégante)

C'est un terrible hack, mais vous pourriez utiliser la fonction inspect pour lire le code source lui-même, et l'analyser. Cela ne fonctionnera pas dans un interpréteur interactif, car le module inspect refusera de donner le code source en mode interactif. Cependant, vous trouverez ci-dessous une preuve de concept.

#!/usr/bin/python3

import inspect

def deco(func):
    return func

def deco2():
    def wrapper(func):
        pass
    return wrapper

class Test(object):
    @deco
    def method(self):
        pass

    @deco2()
    def method2(self):
        pass

def methodsWithDecorator(cls, decoratorName):
    sourcelines = inspect.getsourcelines(cls)[0]
    for i,line in enumerate(sourcelines):
        line = line.strip()
        if line.split('(')[0].strip() == '@'+decoratorName: # leaving a bit out
            nextLine = sourcelines[i+1]
            name = nextLine.split('def')[1].split('(')[0].strip()
            yield(name)

Ça marche !

>>> print(list(  methodsWithDecorator(Test, 'deco')  ))
['method']

Notez que vous devez faire attention à l'analyse syntaxique et à la syntaxe python, par ex. @deco y @deco(... sont des résultats valables, mais @deco2 ne devrait pas être renvoyé si nous demandons simplement 'deco' . Nous remarquons que selon la syntaxe officielle de python à http://docs.python.org/reference/compound_stmts.html Les décorateurs sont les suivants :

decorator      ::=  "@" dotted_name ["(" [argument_list [","]] ")"] NEWLINE

Nous poussons un soupir de soulagement en n'ayant pas à traiter des cas tels que @(deco) . Mais notez que cela ne vous aide toujours pas vraiment si vous avez des décorateurs vraiment très compliqués, tels que @getDecorator(...) par exemple

def getDecorator():
    return deco

Ainsi, cette stratégie d'analyse syntaxique du code, qui consiste à faire au mieux, ne peut pas détecter des cas comme celui-ci. Bien que si vous utilisez cette méthode, ce que vous recherchez réellement est ce qui est écrit au-dessus de la méthode dans la définition, ce qui dans ce cas est getDecorator .

Selon la spécification, il est également possible d'avoir @foo1.bar2.baz3(...) comme décorateur. Vous pouvez étendre cette méthode pour l'utiliser. Vous pouvez également étendre cette méthode pour qu'elle renvoie un objet <function object ...> plutôt que le nom de la fonction, avec beaucoup d'efforts. Cependant, cette méthode est bidon et terrible.


Méthode 3 : Convertir les décorateurs pour qu'ils soient "auto-conscients".

Si vous n'avez pas le contrôle sur le décorateur définition (ce qui est une autre interprétation de ce que vous souhaitez), alors tous ces problèmes disparaissent car vous avez le contrôle sur la façon dont le décorateur est appliqué. Ainsi, vous pouvez modifier le décorateur en emballage pour créer votre propre et utiliser le décorateur que pour décorer vos fonctions. Permettez-moi de le répéter : vous pouvez créer un décorateur qui décore le décorateur sur lequel vous n'avez aucun contrôle, en l'"éclairant", ce qui, dans notre cas, lui fait faire ce qu'il faisait auparavant, mais également ajouter un .decorator à l'appelant qu'il renvoie, ce qui vous permet de garder la trace de "cette fonction était-elle décorée ou non ? vérifions function.decorator !". Et puis vous pouvez itérer sur les méthodes de la classe et vérifier que le décorateur possède le code approprié. .decorator propriété ! =) Comme démontré ici :

def makeRegisteringDecorator(foreignDecorator):
    """
        Returns a copy of foreignDecorator, which is identical in every
        way(*), except also appends a .decorator property to the callable it
        spits out.
    """
    def newDecorator(func):
        # Call to newDecorator(method)
        # Exactly like old decorator, but output keeps track of what decorated it
        R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done
        R.decorator = newDecorator # keep track of decorator
        #R.original = func         # might as well keep track of everything!
        return R

    newDecorator.__name__ = foreignDecorator.__name__
    newDecorator.__doc__ = foreignDecorator.__doc__
    # (*)We can be somewhat "hygienic", but newDecorator still isn't signature-preserving, i.e. you will not be able to get a runtime list of parameters. For that, you need hackish libraries...but in this case, the only argument is func, so it's not a big issue

    return newDecorator

Démonstration pour @decorator :

deco = makeRegisteringDecorator(deco)

class Test2(object):
    @deco
    def method(self):
        pass

    @deco2()
    def method2(self):
        pass

def methodsWithDecorator(cls, decorator):
    """ 
        Returns all methods in CLS with DECORATOR as the
        outermost decorator.

        DECORATOR must be a "registering decorator"; one
        can make any decorator "registering" via the
        makeRegisteringDecorator function.
    """
    for maybeDecorated in cls.__dict__.values():
        if hasattr(maybeDecorated, 'decorator'):
            if maybeDecorated.decorator == decorator:
                print(maybeDecorated)
                yield maybeDecorated

Ça marche !

>>> print(list(   methodsWithDecorator(Test2, deco)   ))
[<function method at 0x7d62f8>]

Cependant, un "décorateur enregistré" doit être le décorateur extérieur sinon le .decorator sera perdue. Par exemple, dans un train de

@decoOutermost
@deco
@decoInnermost
def func(): ...

vous ne pouvez voir que les métadonnées qui decoOutermost expose, à moins que nous ne conservions des références à des enveloppes "plus internes".

remarque : la méthode ci-dessus permet également de constituer un .decorator qui garde la trace de la la pile entière des décorateurs appliqués, des fonctions d'entrée et des arguments de la fabrique de décorateurs. . =) Par exemple, si vous considérez la ligne commentée R.original = func il est possible d'utiliser une méthode comme celle-ci pour garder la trace de toutes les couches d'emballage. C'est personnellement ce que je ferais si j'écrivais une bibliothèque de décorateurs, car cela permet une introspection profonde.

Il existe également une différence entre @foo y @bar(...) . Bien qu'il s'agisse tous deux de "décorateurs expressifs" tels que définis dans la spécification, notez que foo est un décorateur, tandis que bar(...) renvoie un décorateur créé dynamiquement, qui est ensuite appliqué. Vous auriez donc besoin d'une fonction distincte makeRegisteringDecoratorFactory c'est un peu comme makeRegisteringDecorator mais encore plus de META :

def makeRegisteringDecoratorFactory(foreignDecoratorFactory):
    def newDecoratorFactory(*args, **kw):
        oldGeneratedDecorator = foreignDecoratorFactory(*args, **kw)
        def newGeneratedDecorator(func):
            modifiedFunc = oldGeneratedDecorator(func)
            modifiedFunc.decorator = newDecoratorFactory # keep track of decorator
            return modifiedFunc
        return newGeneratedDecorator
    newDecoratorFactory.__name__ = foreignDecoratorFactory.__name__
    newDecoratorFactory.__doc__ = foreignDecoratorFactory.__doc__
    return newDecoratorFactory

Démonstration pour @decorator(...) :

def deco2():
    def simpleDeco(func):
        return func
    return simpleDeco

deco2 = makeRegisteringDecoratorFactory(deco2)

print(deco2.__name__)
# RESULT: 'deco2'

@deco2()
def f():
    pass

Cet emballage de générateur-factory fonctionne également :

>>> print(f.decorator)
<function deco2 at 0x6a6408>

bonus Essayons même ce qui suit avec la méthode n° 3 :

def getDecorator(): # let's do some dispatching!
    return deco

class Test3(object):
    @getDecorator()
    def method(self):
        pass

    @deco2()
    def method2(self):
        pass

Résultat :

>>> print(list(   methodsWithDecorator(Test3, deco)   ))
[<function method at 0x7d62f8>]

Comme vous pouvez le voir, contrairement à la méthode2, @deco est correctement reconnue même si elle n'a jamais été explicitement écrite dans la classe. Contrairement à method2, cela fonctionnera également si la méthode est ajoutée au moment de l'exécution (manuellement, via une métaclasse, etc.) ou héritée.

Sachez que vous pouvez également décorer une classe, donc si vous "illuminez" un décorateur qui est utilisé à la fois pour décorer des méthodes et des classes, et que vous écrivez ensuite une classe dans le corps de la classe que vous voulez analyser entonces methodsWithDecorator retournera des classes décorées ainsi que des méthodes décorées. On pourrait considérer cela comme une fonctionnalité, mais vous pouvez facilement écrire une logique pour ignorer ces éléments en examinant l'argument du décorateur, c'est-à-dire .original pour obtenir la sémantique souhaitée.

2 votes

C'est une si bonne réponse à un problème dont la solution n'est pas évidente que j'ai ouvert une prime pour cette réponse. Désolé, je n'ai pas assez de représentants pour vous donner plus !

2 votes

@NiallDouglas : Merci. =) (Je ne savais pas qu'après un nombre critique d'éditions, une réponse est automatiquement convertie en "community-wiki", donc je n'ai pas été récompensé pour le plus grand nombre de votes positifs... donc merci !)

0 votes

Hmmm, cela ne semble pas fonctionner lorsque le décorateur original est une propriété (ou une forme modifiée de celle-ci) ? Avez-vous une idée ?

19voto

Shane Holloway Points 2021

Pour développer l'excellente réponse de @ninjagecko dans la Méthode 2 : Analyse du code source, vous pouvez utiliser la fonction ast introduit dans Python 2.6 pour effectuer une auto-inspection tant que le module inspect a accès au code source.

def findDecorators(target):
    import ast, inspect
    res = {}
    def visit_FunctionDef(node):
        res[node.name] = [ast.dump(e) for e in node.decorator_list]

    V = ast.NodeVisitor()
    V.visit_FunctionDef = visit_FunctionDef
    V.visit(compile(inspect.getsource(target), '?', 'exec', ast.PyCF_ONLY_AST))
    return res

J'ai ajouté une méthode décorée légèrement plus compliquée :

@x.y.decorator2
def method_d(self, t=5): pass

Résultats :

> findDecorators(A)
{'method_a': [],
 'method_b': ["Name(id='decorator1', ctx=Load())"],
 'method_c': ["Name(id='decorator2', ctx=Load())"],
 'method_d': ["Attribute(value=Attribute(value=Name(id='x', ctx=Load()), attr='y', ctx=Load()), attr='decorator2', ctx=Load())"]}

1 votes

Sympa, l'analyse syntaxique des sources bien faite, et avec les avertissements appropriés. =) Ce sera compatible avec l'avenir si jamais ils décident d'améliorer ou de corriger la grammaire python (par exemple en supprimant les restrictions d'expression sur une expression-décorateur, ce qui semble être un oubli).

0 votes

@ninjagecko Je suis content de ne pas être la seule personne à avoir rencontré la limitation de l'expression du décorateur ! Le plus souvent, je la rencontre lorsque je lie une fermeture de fonction décorée à l'intérieur d'une méthode. Cela se transforme en deux étapes stupides pour la lier à une variable...

0 votes

17voto

Jason S Points 58434

Si vous avez le contrôle sur les décorateurs, vous pouvez utiliser des classes de décorateurs plutôt que des fonctions :

class awesome(object):
    def __init__(self, method):
        self._method = method
    def __call__(self, obj, *args, **kwargs):
        return self._method(obj, *args, **kwargs)
    @classmethod
    def methods(cls, subject):
        def g():
            for name in dir(subject):
                method = getattr(subject, name)
                if isinstance(method, awesome):
                    yield name, method
        return {name: method for name,method in g()}

class Robot(object):
   @awesome
   def think(self):
      return 0

   @awesome
   def walk(self):
      return 0

   def irritate(self, other):
      return 0

et si j'appelle awesome.methods(Robot) il retourne

{'think': <mymodule.awesome object at 0x000000000782EAC8>, 'walk': <mymodulel.awesome object at 0x000000000782EB00>}

5voto

Nick Muise Points 71

Pour ceux d'entre nous qui veulent juste le cas le plus simple possible - à savoir, une solution à fichier unique où nous avons un contrôle total à la fois sur la classe avec laquelle nous travaillons et sur le décorateur que nous essayons de suivre, j'ai une réponse. ninjagecko a fait un lien vers une solution pour quand vous avez le contrôle sur le décorateur que vous voulez suivre, mais j'ai personnellement trouvé qu'elle était compliquée et vraiment difficile à comprendre, peut-être parce que je n'ai jamais travaillé avec des décorateurs jusqu'à présent. J'ai donc créé l'exemple suivant, dans le but d'être aussi direct et simple que possible. Il s'agit d'un décorateur, d'une classe avec plusieurs méthodes décorées, et du code pour récupérer+exécuter toutes les méthodes qui ont un décorateur spécifique appliqué à elles.

# our decorator
def cool(func, *args, **kwargs):
    def decorated_func(*args, **kwargs):
        print("cool pre-function decorator tasks here.")
        return_value = func(*args, **kwargs)
        print("cool post-function decorator tasks here.")
        return return_value
    # add is_cool property to function so that we can check for its existence later
    decorated_func.is_cool = True
    return decorated_func

# our class, in which we will use the decorator
class MyClass:
    def __init__(self, name):
        self.name = name

    # this method isn't decorated with the cool decorator, so it won't show up 
    # when we retrieve all the cool methods
    def do_something_boring(self, task):
        print(f"{self.name} does {task}")

    @cool
    # thanks to *args and **kwargs, the decorator properly passes method parameters
    def say_catchphrase(self, *args, catchphrase="I'm so cool you could cook an egg on me.", **kwargs):
        print(f"{self.name} says \"{catchphrase}\"")

    @cool
    # the decorator also properly handles methods with return values
    def explode(self, *args, **kwargs):
        print(f"{self.name} explodes.")
        return 4

    def get_all_cool_methods(self):
        """Get all methods decorated with the "cool" decorator.
        """
        cool_methods =  {name: getattr(self, name)
                            # get all attributes, including methods, properties, and builtins
                            for name in dir(self)
                                # but we only want methods
                                if callable(getattr(self, name))
                                # and we don't need builtins
                                and not name.startswith("__")
                                # and we only want the cool methods
                                and hasattr(getattr(self, name), "is_cool")
        }
        return cool_methods

if __name__ == "__main__":
    jeff = MyClass(name="Jeff")
    cool_methods = jeff.get_all_cool_methods()    
    for method_name, cool_method in cool_methods.items():
        print(f"{method_name}: {cool_method} ...")
        # you can call the decorated methods you retrieved, just like normal,
        # but you don't need to reference the actual instance to do so
        return_value = cool_method()
        print(f"return value = {return_value}\n")

L'exécution de l'exemple ci-dessus nous donne la sortie suivante :

explode: <bound method cool.<locals>.decorated_func of <__main__.MyClass object at 0x00000220B3ACD430>> ...
cool pre-function decorator tasks here.
Jeff explodes.
cool post-function decorator tasks here.
return value = 4

say_catchphrase: <bound method cool.<locals>.decorated_func of <__main__.MyClass object at 0x00000220B3ACD430>> ...
cool pre-function decorator tasks here.
Jeff says "I'm so cool you could cook an egg on me."
cool post-function decorator tasks here.
return value = None

Notez que les méthodes décorées dans cet exemple ont différents types de valeurs de retour et différentes signatures, donc la valeur pratique de pouvoir les récupérer et les exécuter toutes est un peu douteuse. Cependant, dans les cas où il y a beaucoup de méthodes similaires, toutes avec la même signature et/ou le même type de valeur de retour (comme si vous écrivez un connecteur pour récupérer des données non normalisées d'une base de données, les normaliser, et les insérer dans une seconde base de données normalisée, et que vous avez un tas de méthodes similaires, par exemple 15 méthodes read_and_normalize_table_X), être capable de les récupérer (et de les exécuter) toutes à la volée pourrait être plus utile.

1voto

thouis Points 1700

Peut-être, si les décorateurs ne sont pas trop complexes (mais je ne sais pas s'il existe un moyen moins compliqué).

def decorator1(f):
    def new_f():
        print "Entering decorator1", f.__name__
        f()
    new_f.__name__ = f.__name__
    return new_f

def decorator2(f):
    def new_f():
        print "Entering decorator2", f.__name__
        f()
    new_f.__name__ = f.__name__
    return new_f

class A():
    def method_a(self):
      pass

    @decorator1
    def method_b(self, b):
      pass

    @decorator2
    def method_c(self, t=5):
      pass

print A.method_a.im_func.func_code.co_firstlineno
print A.method_b.im_func.func_code.co_firstlineno
print A.method_c.im_func.func_code.co_firstlineno

0 votes

Malheureusement, cela ne renvoie que les numéros de ligne des lignes suivantes : def new_f(): (le premier, ligne 4), def new_f(): (la deuxième, ligne 11), et def method_a(self): . Vous aurez du mal à trouver les vraies lignes que vous voulez, à moins que vous ayez une convention pour toujours écrire vos décorateurs en définissant une nouvelle fonction comme première ligne, et de plus vous ne devez pas écrire de docstrings... bien que vous puissiez éviter de ne pas écrire de docstrings en ayant une méthode qui vérifie l'indentation en remontant ligne par ligne pour trouver le nom du vrai décorateur.

0 votes

Même avec des modifications, cela ne fonctionne pas non plus si la fonction définie n'est pas dans le décorateur. Il se peut également qu'un décorateur soit un objet appelable et donc que cette méthode lève une exception.

0 votes

"...si les décorateurs ne sont pas trop complexes..." - si le numéro de ligne est le même pour deux méthodes décorées, elles sont probablement décorées de la même façon. Probablement. (enfin, le co_filename devrait être vérifié aussi).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X