Decorators
Decorators
Decorators
to an existing object without modifying its structure. Decorators are very powerful
and useful tool in Python since it allows programmers to modify the behavior of
function or class. Decorators allow us to wrap another function in order to extend
the behavior of the wrapped function, without permanently modifying it.
python
Copy
def my_decorator(func):
def wrapper():
# Do something before the original function is called.
# ...
# Call the original function.
result = func()
# Do something after the original function.
# ...
return result
# Return the wrapped function.
return wrapper
my_function()
When you use the @my_decorator syntax, you are telling Python to call the function
my_decorator with the function my_function as an argument, and then use the
function that my_decorator returns instead of the original my_function.
Class Decorators: Python also allows you to use decorators on classes, which can
alter the class definition itself. This can be used for adding methods, changing
inheritance, or modifying class attributes.
python
Copy
def method_decorator(method):
def wrapper(self, *args, **kwargs):
print("Something is happening before the method is called.")
result = method(self, *args, **kwargs)
print("Something is happening after the method is called.")
return result
return wrapper
class MyClass:
@method_decorator
def my_method(self):
print("Hello, world!")
obj = MyClass()
obj.my_method()
In this example, the method_decorator is applied to my_method of MyClass. When
my_method is called on an instance of MyClass, the wrapper function in
method_decorator is executed instead.
Can you provide more examples of how decorators can be used in Python?
GPT-4
GPT-4
Poe
python
Copy
import time
def timer_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} ran in: {end_time - start_time} secs")
return result
return wrapper
@timer_decorator
def long_running_function():
time.sleep(2)
long_running_function()
2. Debugging Code
Decorators can be used to log information for debugging purposes when a function is
called.
python
Copy
def debug_decorator(func):
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"Calling {func.__name__}({signature})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@debug_decorator
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
greet("Alice", greeting="Hi")
3. Enforcing Access Control
Use decorators to enforce rules about which users can access a particular function.
python
Copy
def admin_required(func):
def wrapper(user, *args, **kwargs):
if user != 'admin':
raise PermissionError("User must be admin to access this function.")
return func(*args, **kwargs)
return wrapper
@admin_required
def sensitive_operation():
print("Performing a sensitive operation...")
user = "admin"
sensitive_operation(user)
4. Caching Results (Memoization)
Decorators can be used to store the results of expensive function calls and return
the cached result when the same inputs occur again.
python
Copy
def memoize_decorator(func):
cache = {}
def wrapper(*args):
if args in cache:
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
@memoize_decorator
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10))
5. Checking Argument Types
A decorator can be used to enforce type checking of arguments passed to a function.
python
Copy
def type_check_decorator(*arg_types, **kwarg_types):
def decorator(func):
def wrapper(*args, **kwargs):
for arg, arg_type in zip(args, arg_types):
if not isinstance(arg, arg_type):
raise TypeError(f"Argument {arg} is not of type {arg_type}")
for kwarg, kwarg_type in kwarg_types.items():
if kwarg in kwargs and not isinstance(kwargs[kwarg], kwarg_type):
raise TypeError(f"Argument {kwarg}={kwargs[kwarg]} is not of
type {kwarg_type}")
return func(*args, **kwargs)
return wrapper
return decorator
@type_check_decorator(int, int)
def add(x, y):
return x + y
===================================================================================
===================================================================================
=
What is a Decorator?
In Python, a decorator is a function that takes another function and extends its
behavior without explicitly modifying it. Decorators are represented by the @
symbol and are placed on top of the function you want to decorate.
Here's a simple example of a decorator that prints a statement before and after the
execution of a function:
python
Copy
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Decorators and OOP
Decorators can be related to OOP in several ways:
python
Copy
from functools import wraps
def authorization(level):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
user = kwargs.get('user')
if (user.level >= level):
return func(*args, **kwargs)
else:
raise Exception("Unauthorized access attempt.")
return wrapper
return decorator
class DataService:
def __init__(self):
self.records = {"Gold": ["Record1", "Record2 (Premium)"], "Silver":
["Record1"]}
# Usage
gold_user = User(level=2)
silver_user = User(level=1)
service = DataService()
print(service.get_records(user=gold_user, record_type="Gold")) # User has access
print(service.get_records(user=silver_user, record_type="Gold")) # Raises an
exception
In this example, the authorization decorator takes an argument level and creates a
decorator that checks whether the user has sufficient privilege. The DataService
class uses the decorator to protect the get_records method, so that only
appropriately privileged users can access the records.
Topic[ real world complex problem of each Functionalities and Use Cases of
decorators]
GPT-4
GPT-4
Poe
Real-World Complex Problem Examples for Each Decorator Use Case
1. Logging
A common use case in real-world applications is to have detailed logging for API
requests to troubleshoot issues and monitor behavior.
Decorator Implementation:
python
Copy
import logging
def log_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Executing {func.__name__}")
result = func(*args, **kwargs)
logging.info(f"Executed {func.__name__}")
return result
return wrapper
@log_decorator
def process_transaction(transaction):
# Implement transaction processing
pass
2. Authorization
In web development, especially in a microservices architecture, services often need
to authenticate and authorize requests.
Problem Example: A content management system where only users with an "editor" role
can publish articles.
Decorator Implementation:
python
Copy
def authorize_user(role_required):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
user = kwargs.get('user')
if user.role != role_required:
raise Exception("You do not have the required role to perform this
action.")
return func(*args, **kwargs)
return wrapper
return decorator
@authorize_user(role_required="editor")
def publish_article(article, user):
# Implement article publishing
pass
3. Caching
Caching is frequently used to improve the performance of data retrieval operations
by storing the results of expensive function calls.
Problem Example: A weather application that makes API calls to retrieve weather
data, which can be cached because weather data does not change every minute.
Decorator Implementation:
python
Copy
from functools import lru_cache
@lru_cache(maxsize=32)
def get_weather_data(location):
# Expensive API call to fetch weather data
pass
4. Timing
Timing functions is essential for performance profiling, especially in scenarios
where speed and efficiency are critical.
Problem Example: A machine learning model prediction API where response time is
crucial.
Decorator Implementation:
python
Copy
import time
def timing_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} executed in {end_time - start_time} seconds")
return result
return wrapper
@timing_decorator
def model_predict(data):
# Model prediction logic
pass
5. Error Handling
Centralized error handling can make the code cleaner and error management more
consistent.
Problem Example: An e-commerce platform where multiple payment gateways can throw
different exceptions which need to be handled gracefully.
Decorator Implementation:
python
Copy
def error_handler_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except ValueError as e:
# Handle specific error
print(f"Handling ValueError: {e}")
except Exception as e:
# Handle any other kind of error
print(f"An error occurred: {e}")
return wrapper
@error_handler_decorator
def process_payment(payment_details):
# Payment processing logic which might throw errors
pass
6. Type Checking
Type checking is useful to ensure that the inputs to a function are of the expected
type, which can prevent bugs.
Problem Example: A scientific computing function that expects numerical inputs and
must validate them before proceeding.
Decorator Implementation:
python
Copy
def type_check_decorator(*type_args, **type_kwargs):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for a, t in zip(args, type_args):
assert isinstance(a, t), f"Argument {a} is not of type {t}"
for k, v in kwargs.items():
if k in type_kwargs:
assert isinstance(v, type_kwargs[k]), f"Argument {k} is not of
type {type_kwargs[k]}"
return func(*args, **kwargs)
return wrapper
return decorator
Decorator Implementation:
python
Copy
from threading import Lock
lock = Lock()
def synchronized(lock):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
with lock:
return func(*args, **kwargs)
return wrapper
return decorator
@synchronized(lock)
def write_to_shared_log(message):
# Code to write message to a shared log file
pass
In this synchronized decorator example, the lock is acquired before the function
starts executing and released after the function completes. This ensures that only
one thread can execute the write_to_shared_log function at a time, preventing race
conditions on the shared log file.
GPT-4
GPT-4
Poe
python
Copy
def singleton(cls):
instances = {}
@wraps(cls)
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class Database:
def __init__(self):
print("Loading database connection")
# Database connection code...
# Usage
db1 = Database() # "Loading database connection"
db2 = Database() # No output, returns the same instance as db1
print(db1 is db2) # True
Factory Pattern
The Factory pattern is a creational design pattern that provides an interface for
creating objects in a superclass, but allows subclasses to alter the type of
objects that will be created. Here's an example of a decorator that can register
and create instances of classes based on a key:
python
Copy
class PetFactory:
registry = {}
@classmethod
def register(cls, key):
def decorator(subclass):
cls.registry[key] = subclass
return subclass
return decorator
@classmethod
def create_pet(cls, key, *args, **kwargs):
if key not in cls.registry:
raise ValueError(f"Unrecognized pet: {key}")
pet_cls = cls.registry[key]
return pet_cls(*args, **kwargs)
@PetFactory.register('dog')
class Dog:
def speak(self):
return "Woof!"
@PetFactory.register('cat')
class Cat:
def speak(self):
return "Meow!"
# Usage
pet = PetFactory.create_pet('dog')
print(pet.speak()) # Woof!
Adapter Pattern
The Adapter pattern allows objects with incompatible interfaces to collaborate. A
decorator can be used to adapt one interface to another without modifying the
existing class.
python
Copy
class OldSystem:
def specific_request(self):
return "Specific behavior of the old system"
def adapter(cls):
class Adapter(cls):
def request(self):
return self.specific_request()
return Adapter
@adapter
class AdaptedOldSystem(OldSystem):
pass
# Usage
adapted_system = AdaptedOldSystem()
print(adapted_system.request()) # "Specific behavior of the old system"
Adding New Functionality Dynamically
Dynamic Extension of Functionality
Decorators can be used to dynamically add new functionality to functions or
methods, akin to mixins in OOP.
python
Copy
def add_greeting(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Hello! This is an extended greeting.")
return func(*args, **kwargs)
return wrapper
class Person:
def say_name(self, name):
print(f"My name is {name}.")
p = Person()
p.say_name('Alice') # "Hello! This is an extended greeting." followed by "My name
is Alice."
Dynamic Extension of Class Functionality
If you want to add functionality to an instance of a class rather than the class
itself, you could use a decorator for that as well:
python
Copy
def trace_method_calls(cls):
class Wrapper:
def __init__(self, *args, **kwargs):
self._wrapped_instance = cls(*args, **kwargs)
@trace_method_calls
class Calculator:
def add(self, a, b):
return a + b
calc = Calculator()
print(calc.add(3, 4))
In this example, trace_method_calls is a class decorator that wraps the Calculator
class to provide tracing for each method call. This adds a new functionality
(tracing) dynamically to the class without changing its original implementation.