Decorators Pattern in Python
The decorator pattern is a design pattern that lets you add new behavior to an original object by placing it inside a wrapper object. It is an alternative to inheritance: a way to extend a class without subclassing it.
With decorators, you can modify the behavior of a function or method without directly changing its source code, making the code cleaner and more Pythonic.
The fundamentals
functions in python
Functions in Python are first-class objects. They can be assigned to variables, passed as arguments, returned as values, and dynamically created or modified at runtime. Combined with built-in decorators, this makes it easy to implement cross-cutting concerns without an additional AOP library.
- being expressible as an anonymous literal value
- being storable in variables
- being storable in data structures
- having an intrinsic identity (independent of any given name)
- being comparable for equality with other entities
- being passable as a parameter to a procedure/function
- being returnable as the result of a procedure/function
- being constructible at runtime
- being printable
- being readable
- being transmissible among distributed processes
- being storable outside running processes
Here are examples of using these function properties in Python:# 1. Assigning Functions to Variablessay_hello = greetprint(say_hello("Alice")) # Output: Hello, Alice!# 2. Pass function as an argumentdef apply_function(func, value): return func(value)def uppercase(text): return text.upper()result = apply_function(uppercase, "hello")print(result) # Output: HELLO# 3. Return function from another function (Nested Function)def make_multiplier(n): def multiplier(x): return x * n return multiplierdouble = make_multiplier(2)print(double(5)) # Output: 10
Simple Demo
With the properties above, we can implement a simple decorator that measures execution time.
Tips: use functools.wraps to preserve metadata from the original function, such as __name__ and __doc__.from functools import wrapsfrom time import timedef simple_decorator(func): @wraps(func) # This annotation preserves metadata from func def wrapper(*args, **kwargs): print(f"function <{func.__name__}> is called") start_time = time() result = func(*args, **kwargs) print(f"Function execution time: {time() - start_time}") return result return wrapper@simple_decoratordef calculate(): sum = 0 for i in range(1000000): sum += i print(sum)# The code above is equivalent to:# @simple_decorator syntax is equivalent to calculate = simple_decorator(calculate)calculate()[OUTPUT]function <calculate> is called499999500000Function execution time: 0.16027212142944336
Chaining Decorators
Python allows multiple decorators, written as @decorator1 @decorator2 … @decoratorN.from functools import wrapsdef star(func): @wraps(func) def wrapper(*args, **kwargs): print("*" * 30) func(*args, **kwargs) print("*" * 30) return wrapperdef hyphen(func): @wraps(func) def wrapper(*args, **kwargs): print("-" * 30) func(*args, **kwargs) print("-" * 30) return wrapper@star@hyphendef greet(name): print(f"Hello, {name}!")greet("Python")[OUTPUT]******************************------------------------------Hello, Python!------------------------------******************************
Best Practices(?)
- Parameter Validation Decorators
- Method Routing
- Caching and Memoization
- …
@lru_cache
from functools import lru_cache@lru_cache(maxsize=128)def fibonacci(n): if n < 2: return n else: return fibonacci(n-1) + fibonacci(n-2)getter/setter
class Person: def __init__(self, name, age): self._name = name self._age = age @property def name(self): return self._name @name.setter def name(self, value): self._name = value @property def age(self): return self._age @age.setter def age(self, value): self._age = valueperson = Person("Alice", 25)print(person.name) # Output: Aliceperson.name = "Bob"print(person.name) # Output: Bob
