Leapcell: The Next — Gen Serverless Platform for Python app Hosting
Detailed Explanation of Python Decorators
I. What are Decorators
In Python, a decorator is essentially a Python function. It has unique capabilities that allow it to add additional functionality to other functions without modifying their original code. The return value of a decorator is also a function object. In simple terms, it is a function specifically designed to return another function.
Decorators play a crucial role in many scenarios where aspect — oriented requirements exist. For example:
- Inserting Logs: Facilitates recording the execution process and relevant information of functions, which is helpful for debugging and system monitoring.
- Performance Testing: Can calculate the execution time of a function, thereby evaluating its performance.
- Transaction Processing: Ensures that a series of operations either all succeed or all fail, guaranteeing data consistency and integrity.
- Caching: For functions with high computational costs, caches their calculation results. When the same input occurs next time, it directly returns the cached value, improving efficiency.
- Permission Verification: Checks whether a user has the corresponding permissions before executing a function, ensuring the security of the system.
Decorators provide an excellent design solution for solving such problems. By using decorators, we can extract a large amount of code that is unrelated to the core functionality of the function but appears repeatedly, achieving high — level code reuse.
In summary, the core function of a decorator is to add additional functionality to existing objects, making the code structure clearer and the functions more abundant and flexible.
II. Why are Decorators Needed
(I) A Simple Example
First, consider a simple function:
def foo():
print('i am foo')
This function simply prints the string i am foo
.
(II) Adding Requirements
Now, there is a new requirement to record the execution log of the function. So, add log — related code to the code:
def foo():
print('i am foo')
print("foo is running")
At this point, the foo
function, in addition to its original functionality, has added the function of printing logs.
(III) Requirements for More Functions
Suppose there are 100 functions that all need to add such log — recording requirements, and in the future, it may be necessary to add the requirement of printing logs before execution for these 100 functions. If we modify the function code one by one, a large amount of duplicate code will be generated, which is obviously not a good solution.
To reduce duplicate code writing, we can re — define a function to specifically handle log — related operations. After the log processing is completed, the actual business code is executed. An example is as follows:
def use_logging(func):
print("%s is running" % func.__name__)
func()
def bar():
print('i am bar')
use_logging(bar)
The running result is:
bar is running
i am bar
In this example, the function use_logging
is a decorator. It wraps the func
that executes the actual business method inside the function. Formally, it seems that the bar
function is decorated by use_logging
. The log - recording operations when the function enters and exits are called an aspect, and this programming method is called Aspect - Oriented Programming.
Through this use_logging
function, we have successfully added a logging function to the function. In the future, no matter how many functions need to add a logging function, or if we need to modify the log format, we only need to modify the use_logging
function and call use_logging(the decorated function)
to achieve the desired effect. For example:
def use_logging(func):
print("%s is running" % func.__name__)
return func
@use_logging
def bar():
print('i am bar')
bar()
III. Introduction to Basic Decorators
(I) Decorator Syntax Sugar
Python provides the @
symbol as the syntax sugar for decorators, which makes it more convenient for us to apply decorating functions. However, there is a requirement for using syntax sugar, that is, the decorating function must return a function object. Therefore, we usually wrap the function to be decorated with an inner function and return this inner function.
Take the following code as an example. The decorator use_logging
is equivalent to first executing this function and then returning the decorated function bar
. So when bar()
is called, it is actually equivalent to executing two functions, which is equivalent to directly calling use_logging(bar)()
.
def use_logging(func):
def _deco():
print("%s is running" % func.__name__)
func()
return _deco
@use_logging
def bar():
print('i am bar')
bar()
(II) Decorating Functions with Parameters
When our function needs to receive two parameters and perform calculations, we need to make corresponding changes to the inner function to receive the two parameters a
and b
passed in. At this time, calling bar(1, 2)
is equivalent to calling use_logging(bar)(1, 2)
. The sample code is as follows:
def use_logging(func):
def _deco(a, b):
print("%s is running" % func.__name__)
func(a, b)
return _deco
@use_logging
def bar(a, b):
print('i am bar:%s' % (a + b))
bar(1, 2)
However, in practical applications, the number and types of parameters of the functions we decorate may vary. It is obviously not scientific to modify the decorator for different parameter situations each time. To solve this parameter problem, we can use Python’s variable — length parameters *args
and **kwargs
.
(III) Uncertain Number of Function Parameters
The following is the version of the decorator without parameters, and this format is suitable for decorating functions without parameters. By using *args
and **kwargs
, our decorator can already adapt to parameters of various lengths and types, that is, this version of the decorator can decorate any type of parameter - free function. The sample code is as follows:
def use_logging(func):
def _deco(*args, **kwargs):
print("%s is running" % func.__name__)
func(*args, **kwargs)
return _deco
@use_logging
def bar(a, b):
print('i am bar:%s' % (a + b))
@use_logging
def foo(a, b, c):
print('i am bar:%s' % (a + b + c))
bar(1, 2)
foo(1, 2, 3)
(IV) Decorators with Parameters
In some cases, we need to make the decorator take parameters. This requires writing a higher — order function that returns a decorator, which is relatively more complex to implement. For example:
#! /usr/bin/env python
def use_logging(level):
def _deco(func):
def __deco(*args, **kwargs):
if level == "warn":
print "%s is running" % func.__name__
return func(*args, **kwargs)
return __deco
return _deco
@use_logging(level="warn")
def bar(a, b):
print('i am bar:%s' % (a + b))
bar(1, 3)
# Equivalent to use_logging(level="warn")(bar)(1, 3)
(V) functools.wraps
Using decorators greatly reuses the code, but it has a drawback that the meta — information of the original function will be lost. For example, information such as the function’s docstring
, __name__
, and parameter list. First, look at the following example:
def use_logging(func):
def _deco(*args, **kwargs):
print("%s is running" % func.__name__)
func(*args, **kwargs)
return _deco
@use_logging
def bar():
print('i am bar')
print(bar.__name__)
bar()
# The output result is:
# bar is running
# i am bar
# _deco
It can be seen that the function name has become _deco
instead of the original bar
. When we use reflection features, this situation will cause problems. To solve this problem, we can introduce functools.wraps
. The sample code using functools.wraps
is as follows:
import functools
def use_logging(func):
@functools.wraps(func)
def _deco(*args, **kwargs):
print("%s is running" % func.__name__)
func(*args, **kwargs)
return _deco
@use_logging
def bar():
print('i am bar')
print(bar.__name__)
bar()
# The output result is:
# bar is running
# i am bar
# bar
As can be seen from the above results, after using functools.wraps
, we get the expected result and successfully retain the name of the original function.
(VI) Achieving Adaptability for Decorators with and without Parameters
import functools
def use_logging(arg):
if callable(arg): # Determine whether the passed parameter is a function. The decorator without parameters calls this branch.
@functools.wraps(arg)
def _deco(*args, **kwargs):
print("%s is running" % arg.__name__)
arg(*args, **kwargs)
return _deco
else: # The decorator with parameters calls this branch.
def _deco(func):
@functools.wraps(func)
def __deco(*args, **kwargs):
if arg == "warn":
print "warn%s is running" % func.__name__
return func(*args, **kwargs)
return __deco
return _deco
@use_logging("warn")
# @use_logging
def bar():
print('i am bar')
print(bar.__name__)
bar()
IV. Class Decorators
Using class decorators can not only achieve the effect of decorators with parameters, but also the implementation method is more elegant and concise. At the same time, it can be flexibly extended through inheritance.
(I) Class Decorators
class loging(object):
def __init__(self, level="warn"):
self.level = level
def __call__(self, func):
@functools.wraps(func)
def _deco(*args, **kwargs):
if self.level == "warn":
self.notify(func)
return func(*args, **kwargs)
return _deco
def notify(self, func):
# logit only logs and does nothing else
print "%s is running" % func.__name__
@loging(level="warn") # Execute the __call__ method
def bar(a, b):
print('i am bar:%s' % (a + b))
bar(1, 3)
(II) Inheriting and Extending Class Decorators
class email_loging(Loging):
'''
An implementation version of loging that can send an email to the administrator when the function is called
'''
def __init__(self, email='admin@myproject.com', *args, **kwargs):
self.email = email
super(email_loging, self).__init__(*args, **kwargs)
def notify(self, func):
# Send an email to self.email
print "%s is running" % func.__name__
print "sending email to %s" % self.email
@email_loging(level="warn")
def bar(a, b):
print('i am bar:%s' % (a + b))
bar(1, 3)
In the above code, the email_loging
class inherits from the Loging
class. Through this inheritance relationship, we can add new functions to it without changing the core logic of the original class, such as sending an email to the administrator when the function is called. This fully reflects the advantages of class decorators in code expansion and reuse.
Leapcell: The Next — Gen Serverless Platform for Python app Hosting
Finally, I would like to recommend the best platform for deploying Python services: Leapcell
1. Multi — Language Support
- Develop with JavaScript, Python, Go, or Rust.
2. Deploy unlimited projects for free
- Pay only for usage — no requests, no charges.
3. Unbeatable Cost Efficiency
- Pay — as — you — go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
4. Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real — time metrics and logging for actionable insights.
5. Effortless Scalability and High Performance
- Auto — scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ