placeholderDecorators Pattern in Python

Decorators Pattern in Python

Understand Python decorators as a design pattern, including first-class functions, simple wrappers, chained decorators, and common uses.

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.

First-Class Object

  • 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

Ref

What are “first-class” objects? —— StackOverFlow