Как получить все методы класса python с заданным декоратором

Как получить все методы данного класса A, украшенные @decorator2?

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

    @decorator1
    def method_b(self, b):
      pass

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

person kraiz    schedule 06.05.2011    source источник
comment
у вас есть какой-либо контроль над исходным кодом decorator2?   -  person ascobol    schedule 06.05.2011
comment
скажем нет, просто чтобы было интересно. но когда это делает решение намного проще, я тоже заинтересован в этом решении.   -  person kraiz    schedule 06.05.2011
comment
+1: сохраняйте интерес: таким образом вы узнаете больше   -  person Lauritz V. Thaulow    schedule 06.05.2011
comment
@S.Lott: Вы имеете в виду меньше обучения с помощью поиска. Посмотрите на верхний ответ ниже. Разве это не очень хороший вклад в SO, повышающий его ценность как ресурса для программистов? Я утверждаю, что главная причина, почему этот ответ так хорош, заключается в том, что @kraiz хотел, чтобы он был интересным. Ответы на ваш связанный вопрос не содержат десятой информации, содержащейся в ответе ниже, если только вы не посчитаете две ссылки, ведущие сюда.   -  person Lauritz V. Thaulow    schedule 30.05.2011


Ответы (7)


Способ 1: простой регистрационный декоратор

Я уже ответил на этот вопрос здесь: Вызов функций по индексу массива в Python =)


Способ 2: разбор исходного кода

Если у вас нет контроля над определением класса, что является одной из интерпретаций того, что вы хотели бы предположить, это невозможно (без code-reading-reflection), поскольку, например, декоратор может быть декоратором без операций (как в моем связанном примере), который просто возвращает функцию без изменений. (Тем не менее, если вы позволите себе обернуть/переопределить декораторы, см. Метод 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(... являются допустимыми результатами, но @deco2 не должен возвращаться, если мы просто запрашиваем 'deco'. Мы заметили, что в соответствии с официальным синтаксисом Python на http://docs.python.org/reference/compound_stmts.html декораторы:

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

Мы вздыхаем с облегчением, что нам не приходится иметь дело с такими случаями, как @(deco). Но обратите внимание, что это все равно не поможет вам, если у вас действительно очень сложные декораторы, такие как @getDecorator(...), например.

def getDecorator():
    return deco

Таким образом, эта лучшая из возможных стратегий разбора кода не может обнаруживать подобные случаи. Хотя, если вы используете этот метод, вам действительно нужно то, что написано поверх метода в определении, в данном случае это getDecorator.

В соответствии со спецификацией допустимо также использовать @foo1.bar2.baz3(...) в качестве декоратора. Вы можете расширить этот метод для работы с этим. Вы также можете расширить этот метод, чтобы он возвращал <function object ...>, а не имя функции, что требует больших усилий. Однако этот метод является хакерским и ужасным.


Метод 3: Преобразование декораторов в «самосознательных»

Если у вас нет контроля над определением decorator (что является другой интерпретацией того, что вам нужно), то все эти проблемы исчезнут, поскольку вы контролируете, как применяется декоратор. Таким образом, вы можете изменить декоратор, обернув его, чтобы создать свой собственный декоратор и использовать его для украшения ваших функций. Позвольте мне повторить это еще раз: вы можете создать декоратор, который украшает декоратор, над которым вы не можете контролировать, «просветляя» его, что в нашем случае заставляет его делать то, что он делал раньше, но также добавляет .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, для достижения желаемой семантики.

person Community    schedule 06.05.2011
comment
Это такой отличный ответ на проблему с неочевидным решением, что я открыл награду за этот ответ. Извините, у меня недостаточно представителей, чтобы дать вам больше! - person Niall Douglas; 04.03.2012
comment
@NiallDouglas: Спасибо. =) (Я не знал, как после критического количества правок ответ автоматически преобразуется в вики сообщества, поэтому я не получил репутацию за большинство голосов ... так что спасибо!) - person ninjagecko; 04.03.2012
comment
Хм, кажется, это не работает, когда исходный декоратор является свойством (или его модифицированной формой)? Любые идеи? - person StevenMurray; 19.02.2016
comment
Это действительно отличный ответ! Потрясающе @ninjagecko - person Brijesh Lakkad; 27.11.2020

Чтобы расширить превосходный ответ @ninjagecko в Методе 2: анализ исходного кода, вы можете использовать модуль ast, представленный в Python 2.6, для выполнения самопроверки, если модуль проверки имеет доступ к исходному коду.

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())"]}
person Shane Holloway    schedule 06.03.2012
comment
Хорошо, анализ исходного кода выполнен правильно и с надлежащими предостережениями. =) Это будет совместимо с предыдущими версиями, если они когда-нибудь решат улучшить или исправить грамматику Python (например, удалив ограничения выражения для выражения-декоратора, что кажется недосмотром). - person ninjagecko; 12.03.2012
comment
@ninjagecko Я рад, что я не единственный человек, который столкнулся с ограничением выражения декоратора! Чаще всего я сталкиваюсь с этим, когда привязываю декоративное закрытие функции внутри метода. Превращается в глупый двухшаг, чтобы привязать его к переменной... - person Shane Holloway; 12.03.2012
comment
См. также stackoverflow.com/questions/4930414/ - person Tony; 28.02.2019

Если у вас есть контроль над декораторами, вы можете использовать классы декораторов, а не функции:

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>}
person Jason S    schedule 09.10.2019
comment
Это как раз то, что я искал Большое спасибо - person Erick Siordia; 30.03.2021

Может быть, если декораторы не слишком сложны (но я не знаю, есть ли менее хакерский способ).

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
person Community    schedule 06.05.2011
comment
К сожалению, это возвращает только номера строк следующих строк: def new_f(): (первая, строка 4), def new_f(): (вторая, строка 11) и def method_a(self):. Вам будет трудно найти настоящие строки, которые вы хотите, если у вас нет соглашения всегда писать свои декораторы, определяя новую функцию в качестве первой строки, и, кроме того, вы не должны писать строки документации... хотя вы могли бы избежать необходимости не напишите строки документации, используя метод, который проверяет отступы по мере их продвижения вверх по строке, чтобы найти имя настоящего декоратора. - person ninjagecko; 06.05.2011
comment
Даже с изменениями это также не работает, если определенная функция не находится в декораторе. Также бывает, что декоратор также может быть вызываемым объектом, и поэтому этот метод может даже генерировать исключение. - person ninjagecko; 06.05.2011
comment
...если декораторы не слишком сложны... - если номер строки одинаков для двух декорированных методов, они, вероятно, оформлены одинаково. Наверное. (ну и co_filename тоже надо проверять). - person ; 06.05.2011

Простой способ решить эту проблему — поместить в декоратор код, который добавляет каждую переданную функцию/метод в набор данных (например, список).

e.g.

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

теперь каждая функция с декоратором deco будет добавлена ​​в functions.

person Thomas King    schedule 25.01.2019

Я не хочу много добавлять, просто простая вариация Метода 2 от ninjagecko. Он творит чудеса.

Тот же код, но с использованием понимания списка вместо генератора, что мне и было нужно.

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]
person Skovborg Jensen    schedule 15.07.2019

Для тех из нас, кто просто хочет абсолютно простейший возможный случай, а именно решение с одним файлом, в котором у нас есть полный контроль как над классом, с которым мы работаем, так и над декоратором, который мы пытаемся отслеживать, у меня есть ответ . 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), возможность извлекать (и запускать) их все на лету может быть более полезной.

person Nick Muise    schedule 20.07.2021