Last active
June 8, 2020 13:00
-
-
Save tritium21/fca09e7ace6cb8edcc28b6372a88e7e9 to your computer and use it in GitHub Desktop.
This is a demo of one way to write a decorator.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This is not the right way, or wrong way, to write a decorator; | |
its just things you can do, with Python 3.8+, to make decorators | |
more powerful | |
""" | |
import functools | |
import inspect | |
def demo(func=None, /, *, message='Default Message'): | |
# we accept func as a positional only argument, and everything else as keyword only. | |
# If all our arguments are keyword only, then we dont have to use a level of nesting | |
# to have a decorator that takes options. We assume if no arguments are passed | |
# positionally, then we have not been used on a function yet, and return | |
# ourselves partially applied. | |
if func is None: | |
return functools.partial(demo, message=message) | |
# Because decorators can be used on a class definition, we will do so here. in | |
# this implementation, we are just applying our decorator to all the functions defined | |
# in our class, unconditionally. | |
if inspect.isclass(func): | |
cls = func # so the names are less confusing. | |
for name, func in inspect.getmembers(cls, inspect.isfunction): | |
setattr(cls, name, demo(func, message=message)) | |
return cls | |
# We are adding an attribute to our functions to record if we decorated them or | |
# not already, so we don't get multiple applications of the decorator. Multiple | |
# applications of a decorator is not inherently bad, this is just a demo of | |
# setting and looking up attributes on function objects - something decorators | |
# are commonly used for. | |
if getattr(func, '_decorated', False): | |
return func | |
func._decorated = True | |
# We could `return func` here - there is no inherent need to write a wrapper, | |
# but they are such a common use case that we will continue. | |
# Finally we create a wrapper function, copying the metadata from the decorated | |
# function and applying it to the wrapper, so things like help() work on it. | |
@functools.wraps(func) | |
def wrapper(*args, **kwargs): | |
print(f"Calling {func.__qualname__} with {message=}") | |
return func(*args, **kwargs) | |
return wrapper | |
# A demo of our demo. | |
@demo(message='Class Message') | |
class Test: | |
@demo(message="Second Application") | |
@demo(message="First Application") | |
def test1(self, bar): | |
print(f"Test.test1({bar=})") | |
@demo | |
def test2(self, bar): | |
print(f"Test.test2({bar=})") | |
def test3(self, bar): | |
print(f"Test.test3({bar=})") | |
@demo | |
def test4(bar): | |
print(f"test4({bar=})") | |
t = Test() | |
t.test1('Multiple Applications') | |
print() | |
t.test2('No arguments') | |
print() | |
t.test3('No decorator') | |
print() | |
test4('Not in a class') | |
# Output: | |
# Calling Test.test1 with message='First Application' | |
# Test.test1(bar='Multiple Applications') | |
# | |
# Calling Test.test2 with message='Default Message' | |
# Test.test2(bar='No arguments') | |
# | |
# Calling Test.test3 with message='Class Message' | |
# Test.test3(bar='No decorator') | |
# | |
# Calling test4 with message='Default Message' | |
# test4(bar='Not in a class') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment