What are decorators in Python
Decorators are a powerful Python tool that allows you to change the behavior of functions, class methods, and other objects without changing their source code. They are an elegant way to add additional functionality to existing objects by wrapping them in other functions.
How Python decorators work
A decorator in Python is a function that takes another function as an argument and returns a new function. This new function usually extends or modifies the behavior of the original function by adding additional functionality to it.
Basic syntax of the decorator
def my_decorator(func):
def wrapper():
print("Additional code before calling the function")
func()
print("Additional code after calling the function")
return wrapper
Using decorators with the @
symbolA special syntax with the symbol @ is used to use the decorator. The decorator is placed immediately before the function definition:
@my_decorator
def say_hello():
print("Hello world!")
say_hello()
Execution result:
Additional code before calling the function
Hello, world!
Additional code after calling the function
Decorators with arguments
When the function being decorated accepts arguments, it is necessary to use *args and **kwargs to pass parameters:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Additional code before calling the function")
result = func(*args, **kwargs)
print("Additional code after calling the function")
return result
return wrapper
@my_decorator
def greet(name):
print("Hello, {}!".format(name))
hello("The World")
Decorators for class methods
Decorators are successfully used to decorate class methods, which is especially useful for logging, checking access rights, or caching:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Additional code before calling the method")
result = func(*args, **kwargs)
print("Additional code after calling the method")
return result
return wrapper
class MyClass:
@my_decorator
def greet(self, name):
print("Hello, {}!".format(name))
obj = MyClass()
obj.greet("The world")
Multiple decorators
Python allows you to apply multiple decorators to a single function. Decorators are applied from bottom to top:
def uppercase(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
def bold(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return "<b>{}</b>".format(result)
return wrapper
@bold
@uppercase
def greet(name):
return "Hello, {}!".format(name)
print(greet("The World")) # HELLO WORLD!</b>
Python's built-in Decorators
Python provides several built-in decorators:
@property
Turns a method into a property of a class:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2
@staticmethod
Creates a static method of the class:
class MathUtils:
@staticmethod
def add(a, b):
return a + b
@classmethod
Creates a class method:
class Person:
@classmethod
def from_string(cls, name_str):
return cls(name_str)
Decorators with parameters
To create decorators with parameters, a three-level structure is used:
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Hello!")
Practical applications of decorators
Logging
import functools
import logging
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logging.info (f"Calling the function {func.__name__}")
return func(*args, **kwargs)
return wrapper
Measurement of execution time
import time
import functools
def timing(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} completed in {end - start:.4f} seconds")
return result
return wrapper
Caching of results
import functools
def cache(func):
cache_dict = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in cache_dict:
cache_dict[key] = func(*args, **kwargs)
return cache_dict[key]
return wrapper
Best practices for using decorators
- Use functiontools.wraps: This preserves the metadata of the original function
- Return the result: Always return the result of the original function
- Handle exceptions: Consider possible exceptions in the decorator
- Document the decorators: Add a docstring to explain the purpose of the decorator