Understanding Python Decorators: Concepts, Usage, and Advanced Examples
This article explains the fundamentals of Python decorators, illustrating their purpose, underlying mechanics, and practical applications through numerous code examples ranging from simple hello‑world wrappers to class‑based, parameterized, and asynchronous decorators, while also discussing side‑effects and best‑practice solutions.
Python decorators are a syntactic feature that allows a function to be wrapped by another function, enabling additional behavior without modifying the original function's code. Unlike the Decorator design pattern in object‑oriented programming, Python decorators rely on higher‑order functions and first‑class functions.
Hello World Example
A minimal decorator that prints messages before and after calling the wrapped function:
def hello(fn):
def wrapper():
print("hello, %s" % fn.__name__)
fn()
print("goodby, %s" % fn.__name__)
return wrapper
@hello
def foo():
print("i am foo")
foo()Running this script outputs:
hello, foo
i am foo
goodby, fooThe interpreter rewrites the @decorator syntax as func = decorator(func) , meaning the original function is replaced by the wrapper returned by the decorator.
Essence of a Decorator
A decorator receives a function object, returns a new function (often called wrapper ) that may call the original function, and the assignment func = decorator(func) binds the name to the wrapper.
Multiple and Parameterized Decorators
Multiple decorators stack from the bottom up:
@decorator_one
@decorator_two
def func():
passwhich is equivalent to func = decorator_one(decorator_two(func)) . A decorator with arguments must return a real decorator:
@decorator(arg1, arg2)
def func():
passis interpreted as func = decorator(arg1, arg2)(func) .
Class‑Based Decorators
Decorators can be implemented as callable classes that store the target function in __init__ and execute it in __call__ :
class myDecorator(object):
def __init__(self, fn):
print("inside myDecorator.__init__()")
self.fn = fn
def __call__(self):
self.fn()
print("inside myDecorator.__call__()")
@myDecorator
def aFunction():
print("inside aFunction()")
print("Finished decorating aFunction()")
aFunction()Using Decorators to Inject Call Parameters
Three common patterns are shown:
Injecting a keyword argument via **kwargs :
def decorate_A(function):
def wrap_function(*args, **kwargs):
kwargs['str'] = 'Hello!'
return function(*args, **kwargs)
return wrap_function
@decorate_A
def print_message_A(*args, **kwargs):
print(kwargs['str'])
print_message_A()Injecting a positional argument directly:
def decorate_B(function):
def wrap_function(*args, **kwargs):
str = 'Hello!'
return function(str, *args, **kwargs)
return wrap_function
@decorate_B
def print_message_B(str, *args, **kwargs):
print(str)
print_message_B()Injecting via *args manipulation:
def decorate_C(function):
def wrap_function(*args, **kwargs):
str = 'Hello!'
args = args + (str,)
return function(*args, **kwargs)
return wrap_function
class Printer:
@decorate_C
def print_message(self, str, *args, **kwargs):
print(str)
p = Printer()
p.print_message()Side Effects and functools.wraps
Decorated functions lose their original metadata (e.g., __name__ becomes wrapper ). Using functools.wraps preserves the original name and docstring:
from functools import wraps
def hello(fn):
@wraps(fn)
def wrapper():
print("hello, %s" % fn.__name__)
fn()
print("goodby, %s" % fn.__name__)
return wrapper
@hello
def foo():
"""foo help doc"""
print("i am foo")
foo()
print(foo.__name__) # prints foo
print(foo.__doc__) # prints foo help docEven with wraps , introspection tools like inspect.getargspec may still miss arguments; a custom get_true_argspec using closure inspection can recover them.
Various Practical Decorator Examples
Memoization (caching) for a recursive Fibonacci function:
from functools import wraps
def memo(fn):
cache = {}
miss = object()
@wraps(fn)
def wrapper(*args):
result = cache.get(args, miss)
if result is miss:
result = fn(*args)
cache[args] = result
return result
return wrapper
@memo
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)Profiler decorator using cProfile and pstats :
import cProfile, pstats, StringIO
def profiler(func):
def wrapper(*args, **kwargs):
datafn = func.__name__ + ".profile"
prof = cProfile.Profile()
retval = prof.runcall(func, *args, **kwargs)
s = StringIO.StringIO()
ps = pstats.Stats(prof, stream=s).sort_stats('cumulative')
ps.print_stats()
print(s.getvalue())
return retval
return wrapperCallback registration via a class instance:
class MyApp():
def __init__(self):
self.func_map = {}
def register(self, name):
def func_wrapper(func):
self.func_map[name] = func
return func
return func_wrapper
def call_method(self, name=None):
func = self.func_map.get(name)
if func is None:
raise Exception("No function registered against - " + str(name))
return func()
app = MyApp()
@app.register('/')
def main_page_func():
return "This is the main page."
@app.register('/next_page')
def next_page_func():
return "This is the next page."
print(app.call_method('/'))
print(app.call_method('/next_page'))Logging decorator that prints function name, arguments, return value, execution time, and optionally the caller line number:
import time, inspect
def logger(loglevel):
def log_decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
ts = time.time()
result = fn(*args, **kwargs)
te = time.time()
print("function = " + fn.__name__)
print(" arguments = {} {}".format(args, kwargs))
print(" return = {}".format(result))
if loglevel == 'debug':
line = inspect.currentframe().f_back.f_back.f_lineno
print(" called_from_line : {}".format(line))
print(" time = %.6f sec" % (te - ts))
return result
return wrapper
return log_decoratorMySQL query decorator (simplified) that injects query results into the wrapped function's keyword arguments:
import umysql
from functools import wraps
class Configuraion:
def __init__(self, env):
if env == "Prod":
self.host = "coolshell.cn"
self.port = 3306
self.db = "coolshell"
self.user = "coolshell"
self.passwd = "fuckgfw"
elif env == "Test":
self.host = 'localhost'
self.port = 3300
self.user = 'coolshell'
self.db = 'coolshell'
self.passwd = 'fuckgfw'
def mysql(sql):
_conf = Configuraion(env="Prod")
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
mysqlconn = umysql.Connection()
mysqlconn.settimeout(5)
mysqlconn.connect(_conf.host, _conf.port, _conf.user, _conf.passwd, _conf.db, True, 'utf8')
try:
rs = mysqlconn.query(sql, {})
except umysql.Error as e:
print(e)
sys.exit(-1)
data = handle_sql_result(rs)
kwargs["data"] = data
result = fn(*args, **kwargs)
mysqlconn.close()
return result
return wrapper
return decorator
@mysql(sql = "select * from coolshell")
def get_coolshell(data):
...Simple asynchronous decorator using threading.Thread :
from threading import Thread
from functools import wraps
def async(func):
@wraps(func)
def async_func(*args, **kwargs):
t = Thread(target=func, args=args, kwargs=kwargs)
t.start()
return t
return async_func
@async
def print_somedata():
print('starting print_somedata')
time.sleep(2)
print('print_somedata: 2 sec passed')
# ...The article concludes with references to the Python Decorator Library and various proposals for extending decorator capabilities.
Python Programming Learning Circle
A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.
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.