Master Python Decorators: From Basics to Advanced Techniques
This article provides a comprehensive, code‑rich guide to Python decorators, covering why they are needed, how to write simple and parameterized decorators, class‑based decorators, built‑in decorators like @property and @classmethod, common pitfalls, and best‑practice optimizations using third‑party tools.
Decorators are a very important Python concept, possibly a major hurdle for advanced learners. This article provides a comprehensive introduction to decorators with detailed code examples, recommended reading.
Python decorators are a gate you must pass to enter Python; whether you cross it or not, they are always there.
Why decorators are needed
Assume your program implements say_hello() and say_goodbye() functions.
def say_hello():
print("hello!")
def say_goodbye():
print("hello!") # bug here
if __name__ == '__main__':
say_hello()
say_goodbye()When actually called, the program prints two "hello" messages because say_goodbye() fails. The boss requires logging the entry time and name of each function before it runs.
[DEBUG] 2016-10-27 11:11-Enter say_hello()
Hello!
[DEBUG] 2016-10-27 11:11-Enter say_goodbye()
Goodbye!Graduate A implements it like this:
def say_hello():
print("[DEBUG]: enter say_hello()")
print("hello!")
def say_goodbye():
print("[DEBUG]: enter say_goodbye()")
print("hello!")
if __name__ == '__main__':
say_hello()
say_goodbye()Graduate B improves the approach:
def debug():
import inspect
caller_name = inspect.stack()[1][3]
print("[DEBUG]: enter {}()".format(caller_name))
def say_hello():
debug()
print("hello!")
def say_goodbye():
debug()
print("goodbye!")
if __name__ == '__main__':
say_hello()
say_goodbye()Calling debug() in every business function is cumbersome, especially when only certain functions need it. This is where decorators come in.
In essence, a decorator is a Python function that can add extra functionality to another function without modifying its code; the decorator returns a function object. It is often used for cross‑cutting concerns such as logging, performance testing, transaction handling, caching, and permission checks.
Decorators add extra functionality to existing functions or objects.
How to write a decorator
Before Python 2.4 (pre‑2004), adding extra functionality to a function was done like this:
def debug(func):
def wrapper():
print("[DEBUG]: enter {}()".format(func.__name__))
return func()
return wrapper
def say_hello():
print("hello!")
say_hello = debug(say_hello) # add functionality while keeping original nameUsing the @ syntax makes it cleaner:
def debug(func):
def wrapper():
print("[DEBUG]: enter {}()".format(func.__name__))
return func()
return wrapper
@debug
def say_hello():
print("hello!")The simple decorator fails when the decorated function requires arguments because the wrapper does not accept them. You can modify the wrapper to accept the same parameters:
def debug(func):
def wrapper(something):
print("[DEBUG]: enter {}()".format(func.__name__))
return func(something)
return wrapper
@debug
def say(something):
print("hello {}!".format(something))To handle any target function, use variable arguments *args and **kwargs:
def debug(func):
def wrapper(*args, **kwargs):
print("[DEBUG]: enter {}()".format(func.__name__))
print('Prepare and say...')
return func(*args, **kwargs)
return wrapper
@debug
def say(something):
print("hello {}!".format(something))Now you have mastered basic decorator writing.
Advanced decorators
Parameterized decorators and class decorators are more advanced. Before understanding them, you should be familiar with closures and the decorator interface (see the Python closure article).
Parameterized decorators
Suppose you want the decorator to log at different levels:
def logging(level):
def wrapper(func):
def inner_wrapper(*args, **kwargs):
print("[{level}]: enter function {func}()".format(level=level, func=func.__name__))
return func(*args, **kwargs)
return inner_wrapper
return wrapper
@logging(level='INFO')
def say(something):
print("say {}!".format(something))
@logging(level='DEBUG')
def do(something):
print("do {}...".format(something))
if __name__ == '__main__':
say('hello')
do('my work')When a parameterized decorator is applied, the outer function runs immediately, returning a decorator that wraps the target function.
Class‑based decorators
A decorator must accept a callable and return a callable. Any object that implements __call__() is callable, so a class can be used:
class Test:
def __call__(self):
print('call me!')
t = Test()
t() # call meFor a class‑based decorator that takes a function in its constructor and implements __call__ to add behavior:
class logging(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("[DEBUG]: enter function {func}()".format(func=self.func.__name__))
return self.func(*args, **kwargs)
@logging
def say(something):
print("say {}!".format(something))For a parameterized class decorator, store the parameters in __init__ and return a wrapper function in __call__:
class logging(object):
def __init__(self, level='INFO'):
self.level = level
def __call__(self, func):
def wrapper(*args, **kwargs):
print("[{level}]: enter function {func}()".format(level=self.level, func=func.__name__))
func(*args, **kwargs)
return wrapper
@logging(level='INFO')
def say(something):
print("say {}!".format(something))Built‑in decorators
Built‑in decorators work the same way but return class objects.
@property
Before using @property, a property can be created manually:
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx, "I am doc for x property")Using @property simplifies it:
@property
def x(self):
...There are three related decorators: setter, getter, and deleter, all built on property(). After decoration, the function becomes a property object.
@classmethod and @staticmethod
Both return class objects; @classmethod returns a classmethod object, @staticmethod returns a staticmethod object.
class Foo(object):
@staticmethod
def bar():
pass # equivalent to bar = staticmethod(bar)Pitfalls in decorators
Code placed in the wrong location
Example showing prints in outer and inner functions to illustrate execution order:
def html_tags(tag_name):
print('begin outer function.')
def wrapper_(func):
print('begin of inner wrapper function.')
def wrapper(*args, **kwargs):
content = func(*args, **kwargs)
print(f"<{tag_name}>{content}</{tag_name}>")
print('end of inner wrapper function.')
return wrapper
print('end of outer function')
return wrapper_
@html_tags('b')
def hello(name='Toby'):
return f'Hello {name}!'
hello()
hello()The output order demonstrates that code outside the wrapper runs at decoration time, not at call time.
begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
<b> Hello Toby! </b>
<b> Hello Toby! </b>Incorrect function signature and documentation
When a decorator replaces a function, attributes like __name__ and __doc__ refer to the wrapper:
def logging(func):
def wrapper(*args, **kwargs):
"""print log before a function."""
print("[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__))
return func(*args, **kwargs)
return wrapper
@logging
def say(something):
"""say something"""
print("say {}!".format(something))
print(say.__name__) # wrapper
print(say.__doc__) # NoneUsing functools.wraps preserves name and docstring:
from functools import wraps
def logging(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""print log before a function."""
print("[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__))
return func(*args, **kwargs)
return wrapper
@logging
def say(something):
"""say something"""
print("say {}!".format(something))
print(say.__name__) # say
print(say.__doc__) # say somethingHowever, the original signature and source code are still lost; third‑party packages like wrapt can solve this.
Cannot decorate @staticmethod or @classmethod
Applying a decorator before @staticmethod raises an AttributeError because functools.wraps expects attributes that staticmethod lacks.
class Car(object):
@logging # fails
@staticmethod
def check_model_for(obj):
...The fix is to place the custom decorator after @staticmethod (or before @classmethod), so the function is wrapped first and then converted to a staticmethod.
class Car(object):
@staticmethod
@logging
def check_model_for(obj):
passOptimizing your decorators
Nested wrapper functions can be hard to read. The decorator library simplifies this.
decorator.py
from decorator import decorate
def wrapper(func, *args, **kwargs):
"""print log before a function."""
print("[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__))
return func(*args, **kwargs)
def logging(func):
return decorate(func, wrapper) # use wrapper to decorate funcYou can also use the @decorator decorator provided by the library:
from decorator import decorator
@decorator
def logging(func, *args, **kwargs):
print("[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__))
return func(*args, **kwargs)The library preserves the original function's name, docstring, and arguments, though inspect.getsource(func) still returns the wrapper source; you need to use func.__wrapped__ for the original source.
wrapt
The wrapt package provides a robust solution that handles all the above issues.
import wrapt
@wrapt.decorator
def logging(wrapped, instance, args, kwargs):
print("[DEBUG]: enter {}()".format(wrapped.__name__))
return wrapped(*args, **kwargs)
@logging
def say(something):
passFor parameterized decorators with wrapt:
def logging(level):
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
print("[{level}]: enter {}()".format(level, wrapped.__name__))
return wrapped(*args, **kwargs)
return wrapper
@logging(level='INFO')
def do(work):
passRefer to the official documentation for more details.
http://wrapt.readthedocs.io/en/latest/quick-start.html
Conclusion
Python decorators differ from Java annotations or C# attributes; they wrap functions or objects to add behavior, while annotations and attributes merely attach metadata that can be inspected via reflection.
Decorators are useful for logging, caching, and other cross‑cutting concerns, whereas annotations are used to classify functions for runtime processing.
This article covered the fundamentals of decorators, advanced patterns, common pitfalls, and optimization techniques. Topics like class decorators will be explored later.
Source code: https://github.com/tobyqin/python_decorator
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
