检查函数是否被称为装饰器

时间:2018-09-05 19:12:33

标签: python python-3.x decorator python-decorators

在下面的最小示例中,decorate被调用了两次。首先使用@decorate,其次通过常规函数调用decorate(bar)

def decorate(func):
    print(func.__name__)
    return func

@decorate
def bar():
    pass

decorate(bar)

是否可以在decorate内查看该调用是通过使用@decorate还是作为常规函数调用来调用的?

4 个答案:

答案 0 :(得分:9)

@decorator语法只是语法糖,因此两个示例的行为相同。这也意味着您在它们之间所做的任何区分可能都没有您想像的那么有意义。

尽管如此,您可以使用inspect来读取脚本,并查看在上一帧中如何调用装饰器。

import inspect

def decorate(func):
    # See explanation below
    lines = inspect.stack(context=2)[1].code_context
    decorated = any(line.startswith('@') for line in lines)

    print(func.__name__, 'was decorated with "@decorate":', decorated)
    return func

请注意,我们必须为context=2函数指定inspect.stackcontext参数指示必须返回当前行周围的多少行代码。在某些特定情况下,例如装饰子类时,当前行位于类声明上,而不是装饰器上。 The exact reason for this behaviour has been explored here.

示例

@decorate
def bar():
    pass

def foo():
    pass
foo = decorate(foo)

@decorate
class MyDict(dict):
    pass

输出

bar was decorated with "@decorate": True
foo was decorated with "@decorate": False
MyDict was decorated with "@decorate": True

注意

仍有一些难以解决的极端情况,例如装饰器和类声明之间的换行符。

# This will fail
@decorate

class MyDict(dict):
    pass

答案 1 :(得分:2)

奥利维尔(Olivier)的回答使我的想法浮现在脑海。但是,由于inspect.stack()是一个特别昂贵的通话,我会考虑选择以下方式使用

frame = inspect.getframeinfo(inspect.currentframe().f_back, context=1)
if frame.code_context[0][0].startswith('@'): 
    print('Used as @decorate: True')
else:
    print("Used as @decorate: False")

答案 2 :(得分:1)

与普遍认为相反,@decoratordecorator(…)并不完全相等。第一个在之前名称绑定运行,第二个在之后名称绑定运行。对于顶级功能的常见用例,这可以廉价地测试哪种情况适用。

import sys

def decoraware(subject):
    """
    Decorator that is aware whether it was applied using `@deco` syntax
    """
    try:
        module_name, qualname = subject.__module__, subject.__qualname__
    except AttributeError:
        raise TypeError(f"subject must define '__module__' and '__qualname__' to find it")
    if '.' in qualname:
        raise ValueError(f"subject must be a top-level function/class")
    # see whether ``subject`` has been bound to its module
    module = sys.modules[module_name]
    if getattr(module, qualname, None) is not subject:
        print('@decorating', qualname)  # @decoraware
    else:
        print('wrapping()', qualname)   # decoraware()
    return subject

此示例将仅打印其应用方式。

>>> @decoraware
... def foo(): ...
...
@decorating foo
>>> decoraware(foo)
wrapping() foo

不过,可以使用相同的方法在每个路径中运行任意代码。

如果应用了多个装饰器,则必须决定要使用顶部还是底部主题。对于顶级功能,该代码未经修改即可工作。对于最底层的主题,在检测之前使用subject = inspect.unwrap(subject)将其展开。


可以在CPython上以更通用的方式使用相同的方法。使用sys._getframe(n).f_locals可以访问应用装饰器的本地名称空间。

def decoraware(subject):
    """Decorator that is aware whether it was applied using `@deco` syntax"""
    modname, topname = subject.__module__, subject.__name__
    if getattr(sys.modules[modname], topname, None) is subject:
        print('wrapping()', topname, '[top-level]')
    else:
        at_frame = sys._getframe(1)
        if at_frame.f_locals.get(topname) is subject:
            print('wrapping()', topname, '[locals]')
        elif at_frame.f_globals.get(topname) is subject:
            print('wrapping()', topname, '[globals]')
        else:
            print('@decorating', topname)
    return subject

请注意,类似于pickle,如果对主题的__qualname__ / __name__进行了篡改或从其定义的命名空间对其进行了del修改,则此方法将失败。 / p>

答案 3 :(得分:0)

在前两个答案的基础上,我编写了一个通用函数,该函数应该在几乎所有实际情况下都能按预期工作。我用Python 3.6、3.7和3.8进行了测试。

在将此函数复制粘贴到您的代码中之前,请确保使用decorator module会更好。

def am_I_called_as_a_decorator(default=False):
    """This function tries to determine how its caller was called.

    The value returned by this function should not be blindly trusted, it can
    sometimes be inaccurate.

    Arguments:
        default (bool): the fallback value to return when we're unable to determine
                        how the function was called

    >>> def f(*args):
    ...     if am_I_called_as_a_decorator():
    ...         print("called as decorator with args {!r}".format(args))
    ...         if len(args) == 1:
    ...             return args[0]
    ...         return f
    ...     else:
    ...         print("called normally with args {!r}".format(args))
    ...
    >>> f()
    called normally with args ()
    >>> @f                              #doctest: +ELLIPSIS
    ... def g(): pass
    ...
    called as decorator with args (<function g at ...>,)
    >>> @f()
    ... class Foobar: pass
    ...
    called as decorator with args ()
    called as decorator with args (<class 'state_chain.Foobar'>,)
    >>> @f(                             #doctest: +ELLIPSIS
    ...     'one long argument',
    ...     'another long argument',
    ... )
    ... def g(): pass
    ...
    called as decorator with args ('one long argument', 'another long argument')
    called as decorator with args (<function g at ...>,)
    >>> @f('one long argument',         #doctest: +ELLIPSIS
    ...    'another long argument')
    ... def g(): pass
    ...
    called as decorator with args ('one long argument', 'another long argument')
    called as decorator with args (<function g at ...>,)
    >>> @f(                             #doctest: +ELLIPSIS
    ...     # A weirdly placed comment
    ...   )
    ... @f
    ... def g(): pass
    ...
    called as decorator with args ()
    called as decorator with args (<function g at ...>,)

    """

    def get_indentation(line):
        for i, c in enumerate(line):
            if not c.isspace():
                break
        return line[:i]

    # First, we try to look at the line where Python says the function call is.
    # Unfortunately, Python doesn't always give us the line we're interested in.
    call_frame = inspect.currentframe().f_back.f_back
    call_info = inspect.getframeinfo(call_frame, context=0)
    source_lines = linecache.getlines(call_info.filename)
    if not source_lines:
        # Reading the source code failed, return the fallback value.
        return default
    try:
        call_line = source_lines[call_info.lineno - 1]
    except IndexError:
        # The source file seems to have been modified.
        return default
    call_line_ls = call_line.lstrip()
    if call_line_ls.startswith('@'):
        # Note: there is a small probability of false positive here, if the
        # function call is on the same line as a decorator call.
        return True
    if call_line_ls.startswith('class ') or call_line_ls.startswith('def '):
        # Note: there is a small probability of false positive here, if the
        # function call is on the same line as a `class` or `def` keyword.
        return True
    # Next, we try to find and examine the line after the function call.
    # If that line doesn't start with a `class` or `def` keyword, then the
    # function isn't being called as a decorator.
    def_lineno = call_info.lineno
    while True:
        try:
            def_line = source_lines[def_lineno]
        except IndexError:
            # We've reached the end of the file.
            return False
        def_line_ls = def_line.lstrip()
        if def_line_ls[:1] in (')', '#', '@', ''):
            def_lineno += 1
            continue
        break
    if not (def_line_ls.startswith('class') or def_line_ls.startswith('def')):
        # Note: there is a small probability of false negative here, as we might
        # be looking at the wrong line.
        return False
    # Finally, we look at the lines above, taking advantage of the fact that a
    # decorator call is at the same level of indentation as the function or
    # class being decorated.
    def_line_indentation = get_indentation(def_line)
    for lineno in range(call_info.lineno - 1, 0, -1):
        line = source_lines[lineno - 1]
        line_indentation = get_indentation(line)
        if line_indentation == def_line_indentation:
            line_ls = line.lstrip()
            if line_ls[:1] in (')', ','):
                continue
            return line_ls.startswith('@')
        elif len(line_indentation) < len(def_line_indentation):
            break
    return default