简体   繁体   English

如何使用给定的装饰器获取 Python 类的所有方法?

[英]How to get all methods of a Python class with given decorator?

How to get all methods of a given class A that are decorated with the @decorator2 ?如何获取用@decorator2的给定类 A 的所有方法?

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

    @decorator1
    def method_b(self, b):
      pass

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

Method 1: Basic registering decorator方法一:基本注册装饰器

I already answered this question here: Calling functions by array index in Python =)我已经在这里回答了这个问题: Calling functions by array index in Python =)


Method 2: Sourcecode parsing方法二:源码解析

If you do not have control over the class definition , which is one interpretation of what you'd like to suppose, this is impossible (without code-reading-reflection), since for example the decorator could be a no-op decorator (like in my linked example) that merely returns the function unmodified.如果您无法控制定义,这是您想要假设的一种解释,这是不可能的(没有代码阅读反射),因为例如装饰器可能是无操作装饰器(如在我的链接示例中)仅返回未修改的函数。 (Nevertheless if you allow yourself to wrap/redefine the decorators, see Method 3: Converting decorators to be "self-aware" , then you will find an elegant solution) (尽管如此,如果您允许自己包装/重新定义装饰器,请参阅方法 3:将装饰器转换为“自我意识” ,那么您会找到一个优雅的解决方案)

It is a terrible terrible hack, but you could use the inspect module to read the sourcecode itself, and parse it.这是一个可怕的骇客,但您可以使用inspect模块来读取源代码本身并对其进行解析。 This will not work in an interactive interpreter, because the inspect module will refuse to give sourcecode in interactive mode.这在交互式解释器中不起作用,因为检查模块将拒绝在交互模式下提供源代码。 However, below is a proof of 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)

It works!:有用!:

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

Note that one has to pay attention to parsing and the python syntax, eg @deco and @deco(... are valid results, but @deco2 should not be returned if we merely ask for 'deco' . We notice that according to the official python syntax at http://docs.python.org/reference/compound_stmts.html decorators are as follows:请注意,必须注意解析和 python 语法,例如@deco@deco(...是有效结果,但如果我们仅要求'deco' ,则不应返回@deco2 。我们注意到根据http://docs.python.org/reference/compound_stmts.html装饰器上的官方 python 语法如下:

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

We breathe a sigh of relief at not having to deal with cases like @(deco) .不必处理像@(deco)这样的案例,我们松了一口气。 But note that this still doesn't really help you if you have really really complicated decorators, such as @getDecorator(...) , eg但请注意,如果您有非常复杂的装饰器,例如@getDecorator(...) ,这仍然对您没有帮助,例如

def getDecorator():
    return deco

Thus, this best-that-you-can-do strategy of parsing code cannot detect cases like this.因此,这种解析代码的最佳策略无法检测到这样的情况。 Though if you are using this method, what you're really after is what is written on top of the method in the definition, which in this case is getDecorator .虽然如果你使用这个方法,你真正想要的是写在定义中的方法之上的东西,在这种情况下是getDecorator

According to the spec, it is also valid to have @foo1.bar2.baz3(...) as a decorator.根据规范,将@foo1.bar2.baz3(...)作为装饰器也是有效的。 You can extend this method to work with that.您可以扩展此方法以使用它。 You might also be able to extend this method to return a <function object ...> rather than the function's name, with lots of effort.您可能还可以扩展此方法以返回<function object ...>而不是函数的名称,但需要付出很多努力。 This method however is hackish and terrible.然而,这种方法是骇人听闻和可怕的。


Method 3: Converting decorators to be "self-aware"方法 3:将装饰器转换为“自我意识”

If you do not have control over the decorator definition (which is another interpretation of what you'd like), then all these issues go away because you have control over how the decorator is applied.如果您无法控制装饰器定义(这是您想要的另一种解释),那么所有这些问题都会消失,因为您可以控制装饰器的应用方式。 Thus, you can modify the decorator by wrapping it, to create your own decorator, and use that to decorate your functions.因此,您可以通过包装来修改装饰器,创建自己的装饰器,并使用来装饰您的函数。 Let me say that yet again: you can make a decorator that decorates the decorator you have no control over, "enlightening" it, which in our case makes it do what it was doing before but also append a .decorator metadata property to the callable it returns, allowing you to keep track of "was this function decorated or not? let's check function.decorator!".让我再说一遍:你可以制作一个装饰器来装饰你无法控制的装饰器,“启发”它,在我们的例子中,它可以做它之前做的事情,但也可以.decorator元数据属性附加到可调用对象它返回,允许您跟踪“此函数是否已装饰?让我们检查 function.decorator!”。 And then you can iterate over the methods of the class, and just check to see if the decorator has the appropriate .decorator property!然后你可以遍历类的方法,并检查装饰器是否具有适当的.decorator属性! =) As demonstrated here: =) 如此处所示:

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

Demonstration for @decorator : @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

It works!:有用!:

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

However, a "registered decorator" must be the outermost decorator , otherwise the .decorator attribute annotation will be lost.但是,“注册的装饰器”必须是最外层的装饰器,否则.decorator属性注解会丢失。 For example in a train of例如在一列火车上

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

you can only see metadata that decoOutermost exposes, unless we keep references to "more-inner" wrappers.您只能看到decoOutermost公开的元数据,除非我们保留对“更多内部”包装器的引用。

sidenote: the above method can also build up a .decorator that keeps track of the entire stack of applied decorators and input functions and decorator-factory arguments .旁注:上述方法还可以构建一个.decorator来跟踪应用的装饰器和输入函数以及装饰器工厂参数的整个堆栈 =) For example if you consider the commented-out line R.original = func , it is feasible to use a method like this to keep track of all wrapper layers. =) 例如,如果您考虑注释掉的行R.original = func ,使用这样的方法来跟踪所有包装层是可行的。 This is personally what I'd do if I wrote a decorator library, because it allows for deep introspection.如果我写了一个装饰器库,这就是我个人会做的事情,因为它允许进行深入的自省。

There is also a difference between @foo and @bar(...) . @foo@bar(...)之间也有区别。 While they are both "decorator expressons" as defined in the spec, note that foo is a decorator, while bar(...) returns a dynamically-created decorator, which is then applied.虽然它们都是规范中定义的“装饰器表达式”,但请注意foo是一个装饰器,而bar(...)返回一个动态创建的装饰器,然后应用它。 Thus you'd need a separate function makeRegisteringDecoratorFactory , that is somewhat like makeRegisteringDecorator but even MORE META:因此,您需要一个单独的函数makeRegisteringDecoratorFactory ,这有点像makeRegisteringDecorator但甚至更多元:

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

Demonstration for @decorator(...) : @decorator(...)的演示:

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

deco2 = makeRegisteringDecoratorFactory(deco2)

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

@deco2()
def f():
    pass

This generator-factory wrapper also works:这个生成器工厂包装器也适用:

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

bonus Let's even try the following with Method #3:奖金让我们甚至尝试使用方法 #3 进行以下操作:

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

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

    @deco2()
    def method2(self):
        pass

Result:结果:

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

As you can see, unlike method2, @deco is correctly recognized even though it was never explicitly written in the class.如您所见,与方法 2 不同,@deco 被正确识别,即使它从未在类中明确写入。 Unlike method2, this will also work if the method is added at runtime (manually, via a metaclass, etc.) or inherited.与 method2 不同,如果该方法是在运行时添加(手动,通过元类等)或继承的,这也将起作用。

Be aware that you can also decorate a class, so if you "enlighten" a decorator that is used to both decorate methods and classes, and then write a class within the body of the class you want to analyze , then methodsWithDecorator will return decorated classes as well as decorated methods.请注意,您也可以装饰一个类,因此如果您“启发”一个用于装饰方法和类的装饰器,然后在您要分析的类的主体中编写一个类,那么methodsWithDecorator将返回已装饰的类以及装饰方法。 One could consider this a feature, but you can easily write logic to ignore those by examining the argument to the decorator, ie .original , to achieve the desired semantics.可以认为这是一个特性,但是您可以通过检查装饰器的参数(即.original )轻松编写逻辑来忽略这些逻辑,以实现所需的语义。

To expand upon @ninjagecko's excellent answer in Method 2: Source code parsing, you can use the ast module introduced in Python 2.6 to perform self-inspection as long as the inspect module has access to the source code.要扩展 @ninjagecko 在方法 2:源代码解析中的出色答案,只要检查模块有权访问源代码,您就可以使用 Python 2.6 中引入的ast模块执行自检。

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

I added a slightly more complicated decorated method:我添加了一个稍微复杂的装饰方法:

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

Results:结果:

> 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())"]}

If you do have control over the decorators, you can use decorator classes rather than functions:如果您确实可以控制装饰器,则可以使用装饰器类而不是函数:

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

and if I call awesome.methods(Robot) it returns如果我调用awesome.methods(Robot)它会返回

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

For those of us who just want the absolute simplest possible case - namely, a single-file solution where we have total control over both the class we're working with and the decorator we're trying to track, I've got an answer.对于我们这些只想要最简单的情况的人 - 即,我们可以完全控制我们正在使用的类和我们试图跟踪的装饰器的单文件解决方案,我有一个答案. ninjagecko linked to a solution for when you have control over the decorator you want to track, but I personally found it to be complicated and really hard to understand, possibly because I've never worked with decorators until now. ninjagecko 链接到一个解决方案,当您可以控制要跟踪的装饰器时,但我个人发现它很复杂并且很难理解,可能是因为我之前从未使用过装饰器。 So, I've created the following example, with the goal of being as straightforward and simple as possible.因此,我创建了以下示例,目标是尽可能简单明了。 It's a decorator, a class with several decorated methods, and code to retrieve+run all methods that have a specific decorator applied to them.它是一个装饰器,一个具有多个装饰方法的类,以及用于检索和运行所有应用了特定装饰器的方法的代码。

# 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")

Running the above example gives us the following output:运行上面的例子会给我们以下输出:

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

Note that the decorated methods in this example have different types of return values and different signatures, so the practical value of being able to retrieve and run them all is a bit dubious.请注意,此示例中的修饰方法具有不同类型的返回值和不同的签名,因此能够全部检索和运行它们的实用价值有点可疑。 However, in cases where there are many similar methods, all with the same signature and/or type of return value (like if you're writing a connector to retrieve unnormalized data from one database, normalize it, and insert it into a second, normalized database, and you have a bunch similar methods, eg 15 read_and_normalize_table_X methods), being able to retrieve (and run) them all on the fly could be more useful.但是,在有许多类似方法的情况下,所有方法都具有相同的签名和/或返回值类型(例如,如果您正在编写连接器以从一个数据库中检索未规范化的数据,对其进行规范化并将其插入第二个数据库,标准化数据库,并且您有一堆类似的方法,例如 15 个 read_and_normalize_table_X 方法),能够即时检索(并运行)它们可能会更有用。

I don't want to add much, just a simple variation of ninjagecko's Method 2. It works wonders.我不想添加太多,只是 ninjagecko 方法 2 的一个简单变体。它可以创造奇迹。

Same code, but using list comprehension instead of a generator, which is what I needed.相同的代码,但使用列表理解而不是生成器,这是我需要的。

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]

Maybe, if the decorators are not too complex (but I don't know if there is a less hacky way).也许,如果装饰器不是太复杂(但我不知道是否有不那么 hacky 的方式)。

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

A simple way to solve this problem is to put code in the decorator that adds each function/method, that is passed in, to a data set (for example a list).解决此问题的一种简单方法是将代码放入装饰器中,将传入的每个函数/方法添加到数据集(例如列表)中。

eg例如

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

now every function with the deco decorator will be added to functions .现在每个带有deco 装饰器的函数都将被添加到函数中。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM