In the following minimal example decorate
is called two times. First using @decorate
, second by normal function call decorate(bar)
.
def decorate(func):
print(func.__name__)
return func
@decorate
def bar():
pass
decorate(bar)
Is it possible to see inside of decorate
if the call was invoked by using @decorate
or as a normal function call?
You can log a message when the function is called using: Debug. Log("Function called!"); You can store a bool that starts as false and set it to true when you enter the function. You can then check this bool elsewhere in code to tell whether your function has been called.
By definition, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.
has_been_called = True return func(*args) wrapper. has_been_called = False return wrapper @calltracker def doubler(number): return number * 2 if __name__ == '__main__': if not doubler. has_been_called: print "You haven't called this function yet" doubler(2) if doubler. has_been_called: print 'doubler has been called!'
A decorator in Python is a function that takes another function as its argument, and returns yet another function . Decorators can be extremely useful as they allow the extension of an existing function, without any modification to the original function source code.
The @decorator
syntax is just syntactic sugar, thus both examples have identical behaviour. This also means whatever distinction you are doing between them might not be as meaningful as you thought.
Although, you can use inspect
to read your script and see how the decorator was called in the above frame.
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
Note that we had to specify context=2
to the inspect.stack
function. The context
argument indicates how many lines of code around the current line must be returned. In some specific cases, such as when decorating a subclass, the current line was on the class declaration instead of the decorator. 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
There are still some corner cases that we can hardly overcome such as linebreaks between the decorator and a class declaration.
# This will fail
@decorate
class MyDict(dict):
pass
Olivier's answer took the thoughts right out of my head. However, as inspect.stack()
is a particularly expensive call, I would consider opting to use something along the lines of:
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")
Contrary to popular believe, @decorator
and decorator(…)
are not exactly equivalent. The first runs before name binding, the latter after name binding. For the common use-case of top-level functions, this allows to cheaply test which case applies.
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
This example will merely print how it was applied.
>>> @decoraware
... def foo(): ...
...
@decorating foo
>>> decoraware(foo)
wrapping() foo
The same means can be used to run arbitrary code in each path, though.
In case that multiple decorators are applied, you must decide whether you want the top or bottom subject. For the top-function, the code works unmodified. For the bottom subject, unwrap it using subject = inspect.unwrap(subject)
before detection.
The same approach can be used in a more general way on CPython. Using sys._getframe(n).f_locals
gives access to the local namespace in which the decorator was applied.
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
Note that similar to pickle
, this approach will fail if the subject's __qualname__
/__name__
is tampered with or it is del
'ed from its defining namespace.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With