Table of Contents
- 2.1 Introduction and prerequisites
- 2.2 First-Class Objects
- 2.3 Returning internal functions from Higher-Order Functions
- 2.4 Preserve non-local state with Closures
- 2.5 Decorate functions
- 2.6 Module 2 Summary
- 3.1 Deep decorators
- 3.2 Decorating functions with arguments
- 3.3 Preserve original function metadata
- 3.4 Commonly used decorators in Python
- 3.5 Implement your own decorators
- 3.6 Module 3 Summary
- 4.1 Handling arguments with Decorator Factories
- 4.2 Implement decorators with arguments
- 4.3 Stack multiple decorators on a single function
- 4.4 Custom decorators in Flask
- 4.5 Module 4 Summary
- 5.1 Use classes as decorators
- 5.2
propertyis a Class Decorator - 5.3 Decorating classes
- 5.4 Standard Library Decorators
- 5.5 Module 5 Summary
1. Course Overview
This course covers Python decorators: what they are and how to use them to decorate functions.
The course covers in order:
- higher-order functions, closures and non-local variables, in order to understand how decorator functions are implemented in practice.
- Implementing custom decorators.
- Decorators with arguments and how to apply multiple decorators on a single function.
- Using classes as decorators and how to decorate classes (instead of functions).
Major topics covered
- Implementing decorators
- Preserving metadata of a function (
functools.wraps) - Decorators with arguments
- Application of several decorators (stacking)
- Class decorators
Final objective
By the end of this course, you will know what decorators are, how they work, and how to create your own decorators to extend the functionality of a function.
Prerequisites
Be familiar with the fundamentals of the Python language, including functions. Experience in object-oriented programming (OOP) is an asset.
2. Working with Higher Order Functions and Closures
2.1 Introduction and prerequisites
This course was created with a specific version of Python. The information applies to the versions of Python shown in the course.
This is an advanced level course. You should already have intermediate level knowledge of Python. If you are a beginner, it is recommended that you review other introductory Pluralsight courses before returning to this one.
Essential prerequisites:
- Mastery of Python functions (this is the fundamental building block for understanding decorators).
- Basic knowledge of object-oriented programming.
What you will learn in this course:
- What decorators are, how they work, and how to create them.
- How to stack them on top of each other.
- How to define decorators with arguments.
- How to use classes as decorators.
Note: Decorators are often created by library or framework developers, because they allow additional abstractions to be offered. Even if you don’t create many of your own decorators, it’s important to understand how they work because you’ll use many of them in your applications.
2.2 Functions as first-class objects (First-Class Objects)
In Python, everything is an object — including functions. We say that the functions are first-class citizens or first-class objects.
This means that everything you can do with objects like str or int, you can also do with a function object:
- Assign them to a variable
- Store them in a collection (list, dictionary, etc.)
- Pass them as arguments to other functions
Assign a function to a variable
# first_class_objects.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
plus = add # On assigne l'objet fonction (sans parenthèses)
print(plus(2, 3)) # Appel via la nouvelle référence
print(add) # Affiche l'adresse mémoire de l'objet add
print(plus) # Affiche la même adresse — c'est le même objet
operations = [add, subtract]
print(operations[1](2, 3)) # Appel de subtract via la liste
Important: Note that
plus = addassigns the function object itself, not the result of its call. There are no parentheses.
The variable plus is a new reference to the same function object in memory. Both identifiers add and plus point to the same object (same memory address).
Pass a function as an argument: Higher-Order Functions
A function that takes another function as an argument or returns a function is called a higher-order function.
# hof.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
def calculator(operation, a, b):
return operation(a, b)
print(calculator(add, 2, 3)) # 5
print(calculator(subtract, 2, 3)) # -1
Here, calculator is a higher-order function because it takes a function (operation) as an argument.
2.3 Returning internal functions from Higher-Order Functions
Local scope reminder
Each function call creates its own local scope. Parameters and variables defined inside a function are only accessible within that scope.
# local_scope.py
def add(a, b):
print(a)
return a + b
print(add(2, 3))
print(add(5, 5))
# print(a) # NameError: name 'a' is not defined
Trying to access a from global scope causes a NameError. Additionally, once the function has completed its evaluation, the values of its local variables are freed from memory by the garbage collector.
Inner Functions
It is entirely possible to define a function inside the body of another function. This function is called an inner function.
# inner_functions.py
def outer():
def inner():
print("Inner function")
return inner
func = outer()
func()
print(func)
What is happening:
- Setting
innercreates a new function object in memory, in the local scope ofouter. outerreturns this function object (without calling it).func = outer()creates a global reference to this inner function object.- This global reference keeps the
innerobject alive, even afterouterhas finished executing.
Key rule: An inner function can be made available outside the outer function, provided you return it from the higher-order function and create a new reference to this location in memory.
2.4 Preserve non-local state with Closures
Free Nonlocal Variables
Let’s look at this more interesting example:
# closures.py
def power(exponent):
def inner(base):
return base ** exponent
return inner
power_of_two = power(2)
power_of_three = power(3)
print(power_of_two(5)) # 25
print(power_of_three(5)) # 125
Analysis:
poweris a higher-order function that takes anexponentas an argument and returnsinner.innerusesexponent, but it is not a local variable ofinner(neither in its body nor as a parameter).- Python looks for
exponentin the enclosing scope — which is the local scope ofpower.
The theoretical problem:
When power(2) is called, the local scope of power is created with exponent = 2. The inner object is defined, then returned. Once power has finished, its local scope should normally be destroyed by the garbage collector. But then, how can inner still access exponent?
The answer: Closures
Python implements a mechanism called closure. When an inner function references variables in the enclosing scope, Python captures them in a special object attached to the inner function. These captured variables are called *free nonlocal variables.
- They are not local to the function itself.
- They are local to the enclosing scope.
A closure is therefore a function which:
- Uses non-local variables.
- Encloses (encloses) the function itself as well as the extended scope of these non-local variables.
Thanks to closures, even after the scope of power is destroyed, the value of exponent remains accessible via the closure of inner.
2.5 Decorate functions
Now that we understand closures and higher-order functions, we can understand how decorators are implemented.
Defining a decorator
A decorator is a higher-order function that takes another function as an argument and returns a closure.
Practical example
Let’s imagine that we need to send emails to interns, and that each email must follow a label: start with “Dear interns,” and end with “Best regards, your new boss”. Rather than repeating this code in each message function, we create a decorator.
# decorators.py
def email_decorator(func):
def wrapper():
print("Dear interns,")
func()
print("Best regards,")
print("your new boss")
return wrapper
@email_decorator
def greeting_message():
print("Welcome to your new job!")
# Ce que fait réellement la syntaxe @email_decorator :
# greeting_message = email_decorator(greeting_message)
greeting_message()
What the @email_decorator syntax does:
- Python passes the original function object (
greeting_message) to the decorator. - Python reassigns the name
greeting_messageto the closure returned by the decorator (herewrapper).
Advantages of this approach:
- separation of concerns: Email label logic is isolated in the decorator.
- Maintainability: if you change the way you write emails, you only change one place.
The
wrapperclosure is what is actually called when writinggreeting_message(). It then calls the original function via the non-local variablefunc.
2.6 Module 2 Summary
| Concept | Description |
|---|---|
| First-class objects | Functions can be assigned to variables, stored in collections, and passed as arguments. |
| Higher-order functions | Functions that take another function as an argument or that return a function. |
| Inner functions | Functions defined inside the body of another function. |
| Free nonlocal variables | Variables used in an inner function that come from the enclosing scope. |
| Closures | Functions that use non-local variables — they “close” the function with the extended scope of those variables. |
| Decorators | Higher-order functions which take a function, create an internal closure adding additional behavior, and return this closure. |
The usual steps for applying a decorator are:
- Assign the new closure to the identifier of the original function:
func = decorator(func) - Or use the
@decoratorsyntax directly on the function definition.
3. Implement function decorators
3.1 Going deeper into decorators
The call stack and frames
When Python encounters a decorator, it handles function calls using the call stack. The call stack keeps track of all active subroutines or functions in a program, along with their return addresses, local variables, and parameters.
- A function call pushes a new frame onto the call stack.
- A frame contains all the information needed to evaluate a function.
Closures and __closure__
The closure returned by a decorator has a special __closure__ attribute which contains cell objects — each cell contains the value of a captured non-local variable.
# 03/demos/1. Digging Deeper into Decorators/script.py
def email_decorator(func):
def wrapper():
print("Dear interns,")
func()
print("Best regards,")
print("your new boss")
return wrapper
@email_decorator
def greeting_message():
print("Welcome to your new job!")
print(greeting_message.__closure__)
print(greeting_message.__closure__[0])
print(greeting_message.__closure__[0].cell_contents)
# L'adresse mémoire de la fonction wrapper
print(f"0x{id(greeting_message):x}")
greeting_message.__closure__returns a tuple ofcellobjects..cell_contentsprovides access to the captured value (here, the originalgreeting_messagefunction).- The
greeting_messageidentifier now points to thewrapperclosure.
3.2 Decorating functions with arguments
The problem
If the decorated function accepts arguments, but the decorator’s wrapper does not accept any, we get a TypeError.
@email_decorator
def greeting_message(x):
print(f"Welcome to your new {x}!")
greeting_message("desk")
# TypeError: wrapper() takes 0 positional arguments but 1 was given
The solution: *args and **kwargs
To make decorators reusable with any function, we use the special parameters *args and **kwargs:
*args: captures all positional arguments in atuple.**kwargs: captures all named arguments in adict.
# 03/demos/2. Decorating Functions with Arguments/script.py
def email_decorator(func):
def wrapper(*args, **kwargs):
# args = ("desk",)
# kwargs = {}
print("Dear interns,")
func(*args, **kwargs)
# func("desk") ← équivalent après unpacking
print("Best regards,")
print("your new boss")
return wrapper
@email_decorator
def greeting_message(x):
print(f"Welcome to your new {x}!")
greeting_message("desk")
Behavior of *args and **kwargs:
- In the definition (parameters): they collect the arguments.
- In the call (arguments): they unpack the values.
Thus, func(*args, **kwargs) passes all arguments received by wrapper to the original function, regardless of their number.
3.3 Keep original function metadata
Introspection
Introspection is the ability of an object to analyze its own attributes at runtime. Many tools and libraries use this functionality to work with object metadata.
The metadata of a function includes:
__name__: the name of the function.__doc__: the docstring of the function.
The problem of metadata loss
When decorating a function, the name of the original function now points to the wrapper closure. So if we inspect __name__ or __doc__, we get the metadata of wrapper, not that of the original function.
# Sans functools.wraps — problème de métadonnées
def email_decorator(func):
def wrapper(*args, **kwargs):
print("Dear interns,")
func(*args, **kwargs)
print("Best regards,")
print("your new boss")
return wrapper
@email_decorator
def greeting_message(x):
"""Function displays a greeting message"""
print(f"Welcome to your new {x}!")
print(greeting_message.__name__) # wrapper ← incorrect !
print(greeting_message.__doc__) # None ← incorrect !
The original function metadata is still intact. The problem is that the greeting_message id now points to wrapper, whose __name__ is “wrapper” and which has no docstring.
The solution: functools.wraps
The functools module provides the @wraps decorator, which copies the metadata from the original function to the closure wrapper.
# 03/demos/3. Retaining Function's Original Metadata/script.py
from functools import wraps
def email_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Dear interns,")
func(*args, **kwargs)
print("Best regards,")
print("your new boss")
return wrapper
@email_decorator
def greeting_message(x):
"""Function displays a greeting message"""
print(f"Welcome to your new {x}!")
print(greeting_message.__name__) # greeting_message ← correct !
print(greeting_message.__doc__) # Function displays a greeting message ← correct !
Best practice: Always use
@wraps(func)in thewrapperof your decorators to preserve transparency and allow correct introspection.
3.4 Commonly used decorators in Python
Besides custom decorators, the most common ones come from the Python standard library or external frameworks.
@classmethod
By default, any method defined in a class is an instance method which receives the instance (self) as its first parameter. The @classmethod decorator transforms a method into a class method, automatically passing it the class itself (cls) as the first parameter.
# 03/demos/4. Commonly Used Decorators in Python/classmethod.py
class MyClass:
@classmethod
def my_class_method(cls):
print("This is a class method.")
MyClass.my_class_method() # Appel direct depuis la classe, sans instance
There is also @staticmethod, similar in functionality but less used.
@property
The @property decorator is the Python alternative to traditional getters and setters. Rather than exposing instance attributes directly, we create property methods.
# 03/demos/4. Commonly Used Decorators in Python/property.py
class MyClass:
def __init__(self, x):
self._x = x
@property
def x(self):
return self._x
@x.setter
def x(self, value):
if value < 5:
self._x = value
c = MyClass(3)
c.x = 2 # Appelle implicitement le setter — vérifie value < 5
print(c.x) # Appelle implicitement le getter — retourne self._x
From the outside, accessing c.x looks like ordinary attribute access, but Python invokes the decorated methods behind the scenes. This allows you to implement transparent validation logic.
@lru_cache (Least Recently Used Cache)
The @lru_cache decorator of the functools module caches the results of a function. When the function is called back with the same arguments, the cached result is returned directly without recalculation.
# 03/demos/4. Commonly Used Decorators in Python/lru_cache.py
from functools import lru_cache
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(100))
Without @lru_cache, calculating fib(100) via recursion would be exponentially slow. With caching, each intermediate value is calculated only once.
maxsize=None: the cache has no size limit.maxsize=128(default): only keeps the 128 most recent calls.
3.5 Implement your own decorators
Logging decorator
The standard logging library allows you to create a simple logging decorator, without external dependencies.
# 03/demos/5. Implementing Your Own Decorators/log.py
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO)
def log_function_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Calling function '{func.__name__}' with args: {args} and kwargs: {kwargs}")
return func(*args, **kwargs)
return wrapper
@log_function_call
def add(a, b):
return a + b
print(add(2, 3))
Important points:
logging.basicConfig(level=logging.INFO)globally configures the logging level.- The wrapper returns the result of
func(*args, **kwargs).
Best practice: Always return the value of the original function call in the wrapper, even if it is
None. This ensures that decorated functions can return values, even if they don’t return any yet.
Performance measurement decorator (timing)
# 03/demos/5. Implementing Your Own Decorators/timing.py
import time
from functools import wraps
def time_function(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
elapsed_time = time.perf_counter() - start_time
print(f"Function '{func.__name__}' took {elapsed_time:.6f} seconds to run.")
return result
return wrapper
@time_function
def slow_function():
time.sleep(1)
slow_function()
time.perf_counter()is the high resolution counter recommended for measuring short durations.- The function result is stored in
resultand then returned after displaying the elapsed time.
3.6 Module 3 Summary
| Concept | Description |
|---|---|
| Call stack / Frames | Structure that manages all active function calls and their contexts. |
*args / **kwargs | Allow decorators to work with functions having any number of arguments. |
functools.wraps | Copies the original function’s metadata to the closure, preserving __name__, __doc__, etc. |
@classmethod | Transforms a method into a class method, receives cls instead of self. |
@property | Alternative to getters/setters, allows transparent validation of attributes. |
@lru_cache | Caches function results to avoid redundant recalculations. |
| Logging decorator | Uses logging.info to log function calls. |
| Timing Decorator | Uses time.perf_counter to measure execution time. |
4. Use advanced decorator workflows
4.1 Handling arguments with Decorator Factories
Reminder: decorator without arguments
@decorator
def my_function():
pass
# Équivalent à :
# my_function = decorator(my_function)
Decorator with argument
When you want to pass an argument to the decorator itself (and not to the decorated function), you need to add an extra layer above the decorator.
@decorator_factory(some_arg)
def my_function():
pass
# Équivalent à :
# my_function = decorator_factory(some_arg)(my_function)
The resolution is done in two steps:
decorator_factory(some_arg)is called and returns a decorator.- This decorator is then applied to
my_function.
This is why we call this type of function a decorator factory: the external function receives the argument and manufactures a decorator that it returns.
Generic structure:
def decorator_factory(arg):
def decorator(func): # Le vrai décorateur
def wrapper(*args, **kwargs):
# ... logique supplémentaire utilisant `arg` ...
return func(*args, **kwargs)
return wrapper
return decorator # La factory retourne le décorateur
Note: “decorator factory” is not an official Python documentation term, but it is technically correct and commonly used.
4.2 Implement decorators with arguments
Here is the example of the email decorator modified to accept a fromWho argument:
# 04/demos/2. Implementing Decorators with Arguments/script.py
from functools import wraps
def email_decorator(fromWho):
def _email_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Dear interns,")
func(*args, **kwargs)
print("Best regards,")
print(f"your new {fromWho}")
return wrapper
return _email_decorator
@email_decorator(fromWho="team leader")
def greeting_message(x):
"""Function displays a greeting message"""
print(f"Welcome to your new {x}!")
greeting_message("job")
Output:
Dear interns,
Welcome to your new job!
Best regards,
your new team leader
Execution flow:
email_decorator(fromWho="team leader")is called → returns_email_decorator._email_decorator(greeting_message)is called → returnswrapper.greeting_messageis now linked towrapper.
The non-local variable fromWho is captured in the wrapper closure via two levels of enclosing scope.
4.3 Stack multiple decorators on a single function
It is possible to apply several decorators to the same function by *stacking them on top of each other.
Syntax
@decorator1
@decorator2
def my_function():
pass
# Équivalent à :
# my_function = decorator1(decorator2(my_function))
Evaluation order
The order is bottom to top when applying, but top to bottom when executing:
decorator2is applied first tomy_function.decorator1receives the closure returned bydecorator2.- When called,
decorator1executes first.
Important: The order of the decorators is crucial. The highest decorator is always evaluated first at runtime.
Example with company_info and email_decorator
# 04/demos/3. Stacking Multiple Decorators on a Single Function/script.py
from functools import wraps
def company_info(func):
@wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs)
print("CompanyName (LLC) Street 22 company@mail.com")
return wrapper
def email_decorator(fromWho):
def _email_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Dear interns,")
func(*args, **kwargs)
print("Best regards,")
print(f"your new {fromWho}")
return wrapper
return _email_decorator
@company_info
@email_decorator(fromWho="team leader")
def greeting_message(x):
"""Function displays a greeting message"""
print(f"Welcome to your new {x}!")
greeting_message("job")
Output:
Dear interns,
Welcome to your new job!
Best regards,
your new team leader
CompanyName (LLC) Street 22 company@mail.com
Execution flow:
email_decorator(fromWho="team leader"))(greeting_message)→ produceswrapper_email.company_info(wrapper_email)→ produceswrapper_company.greeting_messagenow points towrapper_company.- When calling:
wrapper_companyexecutes, callswrapper_email, which calls the original function.
4.4 Custom decorators in Flask
Web frameworks like Flask massively exploit decorators to offer a simple and elegant programming interface.
The @route decorator
@app.route("/")
def index():
return "<h1>Welcome to my website!</h1>"
@app.route("/") is a decorator with argument (the URL path). When someone visits the home page (/), Flask calls the index function. This is a great example of frameworks using decorators to provide a simple programming interface.
Custom decorators for authentication and authorization
In a Flask application, you can create your own decorators to manage security:
@login_required — authentication decorator:
@app.route("/logout")
@login_required
def logout():
# Logique de déconnexion
pass
Order of decorators here:
@app.routeis at the top because it is the one that receives HTTP requests from the client.@login_requiredis below: after receiving the request, we first check if the user is authenticated.
@permission_required("admin") — permission decorator (with argument):
@app.route("/admin")
@login_required
@permission_required("admin")
def admin_panel():
# Page d'administration
pass
This example illustrates a real decorator chain where order is critical:
- Flask intercepts HTTP request (@app.route`).
- We check if the user is logged in (“@login_required`).
- We check if the user has the necessary permissions (
@permission_required("admin")).
4.5 Summary of Module 4
| Concept | Description |
|---|---|
| Decorator factory | External function that receives decorator arguments and returns a decorator. |
| 3-level structure | Factory → Decorator → Wrapper, allowing access to arguments in the internal closure. |
| Stacking | Stack multiple decorators on a function. The topmost decorator runs first. |
Flask @route | Decorator with argument to associate a URL with a function. |
@login_required | Custom authentication decorator, placed under @route. |
@permission_required | Authorization decorator with argument, to control access by role. |
5. Decorate classes and Class Decorators
5.1 Using classes as decorators
A class can act as a decorator by implementing two special methods:
__init__: receives decorator arguments.__call__: receives the function to decorate and returns a closure.
Correspondence with decorator factory
| Decorator factory | Class Decorator |
|---|---|
| Factory function (arg) | __init__(self, arg) |
| Internal decorator function | __call__(self, func) |
| Closure wrapper | Defined in __call__ |
# 05/demos/1. Using Classes as Decorators/script.py
from functools import wraps
class EmailDecorator:
def __init__(self, fromWho):
self.fromWho = fromWho # Stocke l'argument dans un attribut d'instance
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Dear interns,")
func(*args, **kwargs)
print("Best regards,")
print(f"your new {self.fromWho}") # Accès via self, pas comme variable non-locale
return wrapper
# Équivalent commenté avec decorator factory :
# def email_decorator(fromWho="boss"):
# def _email_decorator(func):
# @wraps(func)
# def wrapper(*args, **kwargs):
# print("Dear interns,")
# func(*args, **kwargs)
# print("Best regards,")
# print(f"your new {fromWho}")
# return wrapper
# return _email_decorator
@EmailDecorator(fromWho="team leader")
def greeting_message(x):
"""Function displays a greeting message"""
print(f"Welcome to your new {x}!")
greeting_message("job")
Resolution of @EmailDecorator(fromWho="team leader"):
EmailDecorator(fromWho="team leader")is called → creates an instance withself.fromWho = "team leader".- This instance is called with the function
greeting_message→__call__(greeting_message)is invoked. __call__returns thewrapperclosure.greeting_messagenow points towrapper.
Key difference: In a decorator factory, the argument is accessible as a non-local variable. In a class, it is accessible via self.fromWho — an instance attribute.
5.2 property is a Class Decorator
Python’s built-in property class is actually a class, not a function, although its lowercase nomenclature can be confusing.
Signature of property
property(fget=None, fset=None, fdel=None, doc=None)
All parameters are optional. We generally provide the first two.
Protocol Descriptor
The property class implements the descriptor protocol. A descriptor object is an object whose class implements the special methods __get__, __set__, and/or __delete__.
When you access an attribute via obj.x and x is a descriptor, Python does not return the value directly — it calls the descriptor’s __get__ method.
class MyClass:
def __init__(self, x):
self._x = x
@property
def x(self):
return self._x
@x.setter
def x(self, value):
if value < 5:
self._x = value
How @property works behind the scenes:
@propertyondef x(self)is equivalent to:x = property(fget=x). This creates apropertyobject with the getter method.@x.setterondef x(self, value)is equivalent to:x = x.setter(x). This creates a newpropertyobject which incorporates the setter.
The x attribute in the class is a descriptor object of type property. When Python sees obj.x, it invokes property.__get__, which calls the getter method.
Educational objective
The goal of this lesson is to train the mind to see through the syntax of decorators and understand how they are actually implemented. When you see @property, think of a class descriptor object that intercepts access to attributes.
5.3 Decorating classes
Until now, decorators applied to functions. But the @decorator syntax also works with classes.
@decorator
class MyClass:
pass
# Équivalent à :
# MyClass = decorator(MyClass)
The decorator receives the class object as an argument and must return a class (usually the same, modified).
Monkey Patching
Monkey patching consists of dynamically adding attributes to an object at runtime. This is the main use of class decorators.
# 05/demos/3. Decorating Classes/script.py
def add_speech(cls):
cls.speak = lambda self: f"Hello, I'm a {self.__class__.__name__} instance"
return cls # ← Important : ne pas oublier de retourner la classe !
@add_speech
class SomeClass:
pass
# SomeClass = add_speech(SomeClass)
obj = SomeClass()
print(obj.speak())
Common error: If the decorator does not return the class, the SomeClass variable points to None (implicit return), and the instantiation fails with TypeError: 'NoneType' object is not callable.
Class register
Another classic use case: automatically register classes in a registry.
# 05/demos/3. Decorating Classes/registry.py
registry = {}
def register_class(cls):
registry[cls.__name__] = cls
return cls
@register_class
class SomeClass:
pass
# registry == {"SomeClass": <class 'SomeClass'>}
This pattern is used in frameworks to auto-discover and register components (handlers, plugins, models, etc.).
5.4 Standard library decorators
@total_ordering (module functools)
When you create classes whose instances must be compared with comparison operators (<, >, <=, >=, ==, !=), you should normally define all these special methods (dunder methods).
@total_ordering simplifies this: just define __eq__ and one of the methods __lt__, __le__, __gt__, or __ge__. The decorator dynamically infers and attaches the other missing methods.
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
def __eq__(self, other):
return self.grade == other.grade
def __lt__(self, other):
return self.grade < other.grade
# total_ordering génère automatiquement __le__, __gt__, __ge__
Warning:
@total_orderingmay have performance drawbacks. Use with discretion.
@dataclass (module dataclasses)
Many Python classes follow the same boilerplate:
__init__: takes arguments and assigns them to instance attributes.__repr__: converts the instance to a readable string.__eq__: compares two instances.
The @dataclass decorator automatically generates all this boilerplate from the class’s type annotations.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
z: float = 0.0 # Valeur par défaut
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
print(p1) # Point(x=1.0, y=2.0, z=0.0)
print(p1 == p2) # True — __eq__ généré automatiquement
What @dataclass automatically generates:
__init__: initialization of attributes.__repr__: readable string representation.__eq__: equality comparison of attributes.
The decorator inspects class attributes with type annotations to know which attributes should be part of the generated __init__.
5.5 Module 5 Summary
| Concept | Description |
|---|---|
| Class decorator | Class implementing __init__ (receives args) and __call__ (receives function, returns a closure). |
property is a class | property implements the descriptor protocol; its objects intercept access to attributes. |
| Descriptor protocol | Interface (__get__, __set__, __delete__) allowing an object to manage access to attributes. |
| Decorate classrooms | The decorator receives a class, can modify its attributes (monkey patching), and must return the class. |
| Monkey patching | Dynamically adding attributes/methods to an object or class at runtime. |
@total_ordering | Infers missing comparison operators from __eq__ and another operator. |
@dataclass | Automatically generate __init__, __repr__, __eq__ and other dunder methods. |
6. General Summary
Architecture of a complete decorator
from functools import wraps
def decorator_with_arg(arg): # Decorator factory (optionnel)
def decorator(func): # Vrai décorateur
@wraps(func) # Préserve les métadonnées
def wrapper(*args, **kwargs): # Closure qui enveloppe la fonction
# Comportement AVANT
result = func(*args, **kwargs) # Appel de la fonction originale
# Comportement APRÈS
return result # Retourner le résultat
return wrapper
return decorator
Summary of Fundamental Concepts
| Concept | Role in decorators |
|---|---|
| First-class objects | Functions can be passed as arguments and returned. |
| Higher-order functions | Decorator foundation: takes a function, returns a closure. |
| Inner functions | The wrapper closure is defined inside the decorator. |
| Free nonlocal variables | func and arg are captured in the wrapper closure. |
| Closures | The closure keeps non-local variables alive after the end of the enclosing scope. |
@wraps(func) | Copy __name__, __doc__, etc. from the original function to the closure. |
*args, **kwargs | Makes the decorator generic, compatible with all function signatures. |
| Decorator factory | Adds a layer to allow arguments to the decorator itself. |
| Stacking | Multiple @decorator on a function — order of execution from top to bottom. |
| Class as decorator | __init__ + __call__ replace factory + decorator. |
| Decorating classes | The decorator receives and returns a class (monkey patching). |
Standard decorators to know
| Decorator | Module | Usage |
|---|---|---|
@wraps | functools | Preserve metadata in custom decorators |
@lru_cache | functools | Caching function calls |
@total_ordering | functools | Complete comparison operators |
@dataclass | dataclasses | Generate data class boilerplate |
@classmethod | built-in | Class method (receives cls) |
@staticmethod | built-in | Static method (no self or cls) |
@property | built-in | Transparent getter/setter (descriptor) |
@app.route | Flask | Associate a URL with a view function |
Search Terms
python · decorators · foundations · data · analysis · engineering · analytics · decorator · functions · function · arguments · closures · class · classes · implement · property · argument · custom · decorate · decorating · higher-order · metadata · objective · order