web-dev-qa-db-fra.com

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

Comment obtenir toutes les méthodes d'une classe donnée A 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
66
kraiz

Méthode 1: décorateur d'enregistrement de base

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 la classe definition, qui est une interprétation de ce que vous voulez supposer, ceci est impossible (sans code lecture-réflexion ), car par exemple, le décorateur pourrait être un décorateur sans problème (comme dans mon exemple associé) qui renvoie simplement la fonction sans modification. (Néanmoins, si vous vous permettez d'envelopper/redéfinir les décorateurs, voir Méthode 3: Convertir les décorateurs pour qu'ils soient "conscients", alors vous trouverez une solution élégante.)

C'est un hack terrible, mais vous pouvez utiliser le module inspect pour lire le code source lui-même et l'analyser. Cela ne fonctionnera pas dans un interpréteur interactif, car le module inspecter refusera de donner du code source en mode interactif. Cependant, ci-dessous est 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 qu'il faut prêter attention à l'analyse syntaxique et à la syntaxe python, par ex. @deco et @deco(... sont des résultats valides, mais @deco2 ne doit 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 decorators sont les suivants:

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

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

def getDecorator():
    return deco

Ainsi, cette stratégie d’analyse de code «best-you-can-do» ne peut pas détecter de tels cas. Cependant, si vous utilisez cette méthode, ce que vous recherchez réellement est ce qui est écrit en haut de la méthode dans la définition, qui dans ce cas est getDecorator.

Selon la spécification, il est également valide d'avoir @foo1.bar2.baz3(...) en tant que décorateur. Vous pouvez étendre cette méthode pour travailler avec cela. Vous pourriez également être en mesure d'étendre cette méthode pour renvoyer un <function object ...> plutôt que le nom de la fonction, avec beaucoup d'effort. Cette méthode est cependant bidon et terrible.


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

Si vous ne contrôlez pas la décorateur definition (ce qui est une autre interprétation de ce que vous souhaitez), toutes ces questions disparaissent, car vous avez le contrôle sur l'application du décorateur. Ainsi, vous pouvez modifier le décorateur en l'enveloppant pour créer votre décorateur propre et utiliser that pour décorer vos fonctions. Permettez-moi de le dire encore une fois: vous pouvez créer un décorateur qui décore celui-ci sur lequel vous n'avez aucun contrôle, en "l'éclairant", ce qui dans notre cas le fait faire ce qu'il faisait auparavant mais aussi ajoute une propriété de métadonnées .decorator à l'appelable, il revient, vous permettant de garder une trace de "cette fonction a-t-elle été décorée ou non? vérifions function.decorator!". Et alors vous pouvez parcourir les méthodes de la classe et vérifier si le décorateur a la propriété .decorator appropriée! =) 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.__= 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 le plus à l'extérieur, sinon l'annotation de l'attribut .decorator sera perdue. Par exemple dans un train de

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

vous ne pouvez voir que les métadonnées que decoOutermost expose, sauf si nous conservons des références à des wrappers "plus internes".

note de bas de page: la méthode ci-dessus peut également créer un .decorator qui garde la trace de la pile entière de décorateurs appliqués, de fonctions d'entrée et d'arguments de 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 une trace de toutes les couches de l’enveloppe. Personnellement, c’est ce que je ferais si j’écrivais une bibliothèque de décorateurs, car elle permet une introspection profonde.

Il existe également une différence entre @foo et @bar(...). Bien qu'ils soient tous deux des "expressions de décorateur" tels que définis dans la spécification, notez que foo est un décorateur, alors que bar(...) renvoie un décorateur créé de manière dynamique, qui est ensuite appliqué. Ainsi, vous auriez besoin d’une fonction distincte makeRegisteringDecoratorFactory, qui ressemble un peu à makeRegisteringDecorator mais encore PLUS 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.__= 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

Ce wrapper générateur-usine 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 constater, contrairement à method2, @deco est correctement reconnu même s'il n'a jamais été explicitement écrit dans la classe. Contrairement à method2, cela fonctionnera également si la méthode est ajoutée à l'exécution (manuellement, via une métaclasse, etc.) ou héritée.

Sachez que vous pouvez également décorer une classe. Ainsi, si vous "éclairez" un décorateur utilisé pour décorer des méthodes et des classes, puis écrivez une classe dans le corps de la classe que vous souhaitez analyser, alors methodsWithDecorator retournera les classes décorées ainsi que les méthodes décorées. On pourrait considérer cela comme une fonctionnalité, mais vous pouvez facilement écrire une logique pour ignorer celles-ci en examinant l'argument adressé au décorateur, c'est-à-dire .original, pour obtenir la sémantique souhaitée.

99
ninjagecko

Pour développer l'excellente réponse de @ ninjagecko à la Méthode 2: Analyse du code source, vous pouvez utiliser le module ast introduit dans Python 2.6 pour effectuer l'auto-inspection tant que le module inspecte 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 de décor 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())"]}
14
Shane Holloway

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

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

def decorator2(f):
    def new_f():
        print "Entering decorator2", f.__name__
        f()
    new_f.__= 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
user227667

Je ne veux pas ajouter grand chose, juste une simple variation de la Méthode 2 de ninjagecko. Cela fonctionne à merveille.

Même code, mais en utilisant la compréhension de liste au lieu d’un générateur, c’est ce dont j'avais besoin.

def methodsWithDecorator(cls, decoratorName):

    sourcelines = inspect.getsourcelines(cls)[0]
    return [ sourcelines[i+1].split('def')[1].split('(')[0].strip()
                    for i, line in enumerate(sourcelines)
                    if line.split('(')[0].strip() == '@'+decoratorName]
0
Skovborg Jensen

Un moyen simple de résoudre ce problème consiste à insérer dans le décorateur un code qui ajoute chaque fonction/méthode transmise à un ensemble de données (par exemple une liste).

par exemple.

def deco(foo):
    functions.append(foo)
    return foo

maintenant chaque fonction avec deco decorator sera ajoutée à functions .

0
Thomas King