簡體   English   中英

如何使用給定的裝飾器獲取 Python 類的所有方法?

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

如何獲取用@decorator2的給定類 A 的所有方法?

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

    @decorator1
    def method_b(self, b):
      pass

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

方法一:基本注冊裝飾器

我已經在這里回答了這個問題: Calling functions by array index in Python =)


方法二:源碼解析

如果您無法控制定義,這是您想要假設的一種解釋,這是不可能的(沒有代碼閱讀反射),因為例如裝飾器可能是無操作裝飾器(如在我的鏈接示例中)僅返回未修改的函數。 (盡管如此,如果您允許自己包裝/重新定義裝飾器,請參閱方法 3:將裝飾器轉換為“自我意識” ,那么您會找到一個優雅的解決方案)

這是一個可怕的駭客,但您可以使用inspect模塊來讀取源代碼本身並對其進行解析。 這在交互式解釋器中不起作用,因為檢查模塊將拒絕在交互模式下提供源代碼。 然而,下面是一個概念證明。

#!/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)

有用!:

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

請注意,必須注意解析和 python 語法,例如@deco@deco(...是有效結果,但如果我們僅要求'deco' ,則不應返回@deco2 。我們注意到根據http://docs.python.org/reference/compound_stmts.html裝飾器上的官方 python 語法如下:

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

不必處理像@(deco)這樣的案例,我們松了一口氣。 但請注意,如果您有非常復雜的裝飾器,例如@getDecorator(...) ,這仍然對您沒有幫助,例如

def getDecorator():
    return deco

因此,這種解析代碼的最佳策略無法檢測到這樣的情況。 雖然如果你使用這個方法,你真正想要的是寫在定義中的方法之上的東西,在這種情況下是getDecorator

根據規范,將@foo1.bar2.baz3(...)作為裝飾器也是有效的。 您可以擴展此方法以使用它。 您可能還可以擴展此方法以返回<function object ...>而不是函數的名稱,但需要付出很多努力。 然而,這種方法是駭人聽聞和可怕的。


方法 3:將裝飾器轉換為“自我意識”

如果您無法控制裝飾器定義(這是您想要的另一種解釋),那么所有這些問題都會消失,因為您可以控制裝飾器的應用方式。 因此,您可以通過包裝來修改裝飾器,創建自己的裝飾器,並使用來裝飾您的函數。 讓我再說一遍:你可以制作一個裝飾器來裝飾你無法控制的裝飾器,“啟發”它,在我們的例子中,它可以做它之前做的事情,但也可以.decorator元數據屬性附加到可調用對象它返回,允許您跟蹤“此函數是否已裝飾?讓我們檢查 function.decorator!”。 然后你可以遍歷類的方法,並檢查裝飾器是否具有適當的.decorator屬性! =) 如此處所示:

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

@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

有用!:

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

但是,“注冊的裝飾器”必須是最外層的裝飾器,否則.decorator屬性注解會丟失。 例如在一列火車上

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

您只能看到decoOutermost公開的元數據,除非我們保留對“更多內部”包裝器的引用。

旁注:上述方法還可以構建一個.decorator來跟蹤應用的裝飾器和輸入函數以及裝飾器工廠參數的整個堆棧 =) 例如,如果您考慮注釋掉的行R.original = func ,使用這樣的方法來跟蹤所有包裝層是可行的。 如果我寫了一個裝飾器庫,這就是我個人會做的事情,因為它允許進行深入的自省。

@foo@bar(...)之間也有區別。 雖然它們都是規范中定義的“裝飾器表達式”,但請注意foo是一個裝飾器,而bar(...)返回一個動態創建的裝飾器,然后應用它。 因此,您需要一個單獨的函數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

@decorator(...)的演示:

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

deco2 = makeRegisteringDecoratorFactory(deco2)

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

@deco2()
def f():
    pass

這個生成器工廠包裝器也適用:

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

獎金讓我們甚至嘗試使用方法 #3 進行以下操作:

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

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

    @deco2()
    def method2(self):
        pass

結果:

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

如您所見,與方法 2 不同,@deco 被正確識別,即使它從未在類中明確寫入。 與 method2 不同,如果該方法是在運行時添加(手動,通過元類等)或繼承的,這也將起作用。

請注意,您也可以裝飾一個類,因此如果您“啟發”一個用於裝飾方法和類的裝飾器,然后在您要分析的類的主體中編寫一個類,那么methodsWithDecorator將返回已裝飾的類以及裝飾方法。 可以認為這是一個特性,但是您可以通過檢查裝飾器的參數(即.original )輕松編寫邏輯來忽略這些邏輯,以實現所需的語義。

要擴展 @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

我添加了一個稍微復雜的裝飾方法:

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

結果:

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

如果您確實可以控制裝飾器,則可以使用裝飾器類而不是函數:

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

如果我調用awesome.methods(Robot)它會返回

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

對於我們這些只想要最簡單的情況的人 - 即,我們可以完全控制我們正在使用的類和我們試圖跟蹤的裝飾器的單文件解決方案,我有一個答案. ninjagecko 鏈接到一個解決方案,當您可以控制要跟蹤的裝飾器時,但我個人發現它很復雜並且很難理解,可能是因為我之前從未使用過裝飾器。 因此,我創建了以下示例,目標是盡可能簡單明了。 它是一個裝飾器,一個具有多個裝飾方法的類,以及用於檢索和運行所有應用了特定裝飾器的方法的代碼。

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

運行上面的例子會給我們以下輸出:

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

請注意,此示例中的修飾方法具有不同類型的返回值和不同的簽名,因此能夠全部檢索和運行它們的實用價值有點可疑。 但是,在有許多類似方法的情況下,所有方法都具有相同的簽名和/或返回值類型(例如,如果您正在編寫連接器以從一個數據庫中檢索未規范化的數據,對其進行規范化並將其插入第二個數據庫,標准化數據庫,並且您有一堆類似的方法,例如 15 個 read_and_normalize_table_X 方法),能夠即時檢索(並運行)它們可能會更有用。

我不想添加太多,只是 ninjagecko 方法 2 的一個簡單變體。它可以創造奇跡。

相同的代碼,但使用列表理解而不是生成器,這是我需要的。

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]

也許,如果裝飾器不是太復雜(但我不知道是否有不那么 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

解決此問題的一種簡單方法是將代碼放入裝飾器中,將傳入的每個函數/方法添加到數據集(例如列表)中。

例如

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

現在每個帶有deco 裝飾器的函數都將被添加到函數中。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM