Як отримати всі методи класу python із заданим декоратором


79

Як отримати всі методи даного класу A, які прикрашені @ decorator2?

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

    @decorator1
    def method_b(self, b):
      pass

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

2
Ви контролюєте вихідний код "decorator2"?
аскобол

8
скажемо ні, лише щоб було цікаво. але коли це робить рішення набагато простішим, мене також цікавить це рішення.
країз

16
+1: "тримай цікаво": дізнайся більше цим
Лауріц В. Таулов,

10
@ S.Lott: Ви маєте на увазі менше вчитися за допомогою пошуку. Подивіться на верхню відповідь нижче. Це не дуже хороший внесок у SO, що збільшує його цінність як ресурсу для програмістів? Я стверджую , що головна причина , чому ця відповідь настільки добре, що @kraiz хотів «тримати його цікавим». Відповіді на ваше пов'язане запитання не містять десятої частини інформації, що міститься у відповіді нижче, якщо ви не порахуєте два посилання, які ведуть сюди.
Lauritz V. Thaulow

Відповіді:


116

Спосіб 1: Основний декоратор реєстрації

Я вже відповів на це запитання тут: Виклик функцій за допомогою індексу масиву в Python =)


Спосіб 2: Розбір вихідного коду

Якщо у вас немає контролю над визначенням класу , що є однією з інтерпретацій того, що ви хотіли б припустити, це неможливо (без читання-роздуму коду), оскільки, наприклад, декоратор може бути декоратором без операції (наприклад у моєму зв’язаному прикладі), яка просто повертає функцію незміненою. (Тим не менше, якщо ви дозволите собі обернути / перевизначити декоратори, див. Метод 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властивість метаданих до викликається, який він повертає , що дозволяє вам відстежувати "чи була ця функція оформлена чи ні? давайте перевіримо 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розкривають, якщо ми не зберігаємо посилання на "більш внутрішні" обгортки.

sidenote: вищезазначений метод може також створити такий, .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>]

Як бачите, на відміну від method2, @deco правильно розпізнається, хоча він ніколи не писався явно в класі. На відміну від method2, це також буде працювати, якщо метод додається під час виконання (вручну, через метаклас тощо) або успадковується.

Майте на увазі, що ви також можете прикрасити клас, тому, якщо ви "просвітлите" декоратор, який використовується як для декорування методів, так і для класів, а потім напишете клас у тілі класу, який ви хочете проаналізувати , тоді methodsWithDecoratorповернете оформлені класи як а також декоровані методи. Можна вважати це особливістю, але ви можете легко написати логіку, щоб ігнорувати їх, досліджуючи аргумент до декоратора, тобто .originalдля досягнення бажаної семантики.


1
Це така чудова відповідь на проблему з неочевидним рішенням, що я відкрив нагороду за цю відповідь. Вибачте, у мене недостатньо представників, щоб дати вам більше!
Найл Дуглас

2
@NiallDouglas: Дякую. =) (Я не знав, як після критичної кількості редагувань відповідь автоматично перетворюється на "wiki-спільноту", тому я не отримав репутацію для більшості голосів "за", тож дякую!)
ninjagecko

Хм, це, здається, не працює, коли оригінальний декоратор є властивістю (або модифікованою формою)? Будь-які ідеї?
StevenMurray

Це справді чудова відповідь! Високий @ninjagecko
Brijesh Lakkad

15

Щоб розширити чудову відповідь @ ninjagecko у Методі 2: Синтаксичний аналіз вихідного коду, ви можете використовувати astмодуль, представлений в Python 2.6, для самостійної перевірки, доки модуль inspect має доступ до вихідного коду.

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

1
Хороший, синтаксичний аналіз джерела зроблений правильно і з відповідними застереженнями. =) Це буде сумісно вперед, якщо вони коли-небудь вирішать вдосконалити або виправити граматику python (наприклад, видаливши обмеження виразу для декоратора-виразу, що здається недоглядом).
ninjagecko

@ninjagecko Я радий, що я не єдина людина, яка натрапила на обмеження виразу декоратора! Найчастіше я стикаюся з цим, коли прив'язую оформлене закриття функції всередині методу. Перетворюється на безглуздий двоетап, щоб прив’язати його до змінної ...
Шейн Холлоуей


0

Можливо, якщо декоратори не надто складні (але я не знаю, чи є менш вдалий спосіб).

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 new_f():(перший, рядок 4), def new_f():(другий, рядок 11) та def method_a(self):. Вам буде важко знайти реальні рядки, які ви хочете, якщо у вас немає домовленості, щоб завжди писати свої декоратори, визначаючи нову функцію як перший рядок, і, крім того, ви не повинні писати жодних рядків ... хоча ви можете уникнути необхідності не запишіть рядки документації, маючи метод, який перевіряє відступ, коли він рухається вгору за рядком, щоб знайти ім'я справжнього декоратора.
ninjagecko

Навіть із модифікаціями це також не спрацьовує, якщо визначена функція відсутня в декораторі. Також буває так, що декоратор також може бути об’єктом, що викликається, і таким чином цей метод може навіть створити виняток.
ninjagecko

"... якщо декоратори не надто складні ..." - якщо номер рядка однаковий для двох декорованих способів, вони, ймовірно, оформляються однаково. Мабуть. (ну, co_filename теж слід перевірити).

0

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

напр

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

Тепер кожна функція з Деко декоратором буде додана функцій .


0

Я не хочу додавати багато, просто просту варіацію методу 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]

0

Якщо ви все-таки контролюєте декоратори, ви можете використовувати класи декораторів, а не функції:

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>}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.