在下面的最小示例中,decorate
被调用了两次。首先使用@decorate
,其次通过常规函数调用decorate(bar)
。
def decorate(func):
print(func.__name__)
return func
@decorate
def bar():
pass
decorate(bar)
是否可以在decorate
内查看该调用是通过使用@decorate
还是作为常规函数调用来调用的?
答案 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.stack
。 context
参数指示必须返回当前行周围的多少行代码。在某些特定情况下,例如装饰子类时,当前行位于类声明上,而不是装饰器上。 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)
与普遍认为相反,@decorator
和decorator(…)
并不完全相等。第一个在之前名称绑定运行,第二个在之后名称绑定运行。对于顶级功能的常见用例,这可以廉价地测试哪种情况适用。
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