The decorator helps us add some processing steps before and/or after executing an original function. These common tasks can be anything, like- checking access, object cleanup, setup caching or logging, etc. but that depends on your implementation.
Using a decorator, we can add some additional steps before and/or after the original function execution, without touching/changing the original function.
In Python –
A decorator is a function that accepts a function and returns a function.
We can perform the additional steps of operation(that we want to add before/after the original function), in the implementation of the decorator.
NOTES
In Python, functions are first-class entities. That is why we can-
Decorator Capability/Rules
Here is what a Python Decorator can and can not do-
Decorators can do
Decorators can not do
Basic Understanding
WARNING
This is not the exact implementation of the Decorator, but the initial step to understand the Decorator implementation.
Make sure to check this before going to the next step.
First, let’s see what is going on in the decorator implementation. As a decorator is a function that accepts a function and returns a function to create such a function first.
Decorator Function:
def my_decorator(func):
def inner_func():
print("This is some additional processing in the dcorator")
func()
print("Again some additional processing")
return inner_func
def big_box_process():
print("running my Big Box process")
# Demo usage
if __name__ == '__main__':
decorator_obj = my_decorator(big_box_process)
decorator_obj()
# Or you can use the following line to do that in a single line
# my_decorator(big_box_process)()
PythonUsage:
Let’s see how can we apply the decorator to our own function.
This way we are adding the decorator “my_decorator” to our own function “big_box_process“.
Output:
This is some additional processing in the dcorator
running my simple process
Again some additional processing
PlaintextUsing the Decorator Properly
NOTES
In this step, we will see how we should use the decorator.
We just need to add the “@” sign and then the decorator function name before the function definition, and that will apply the decorator on that function.
def my_decorator(func):
def inner_func():
print("This is some additional processing in the dcorator")
func()
print("Again some additional processing")
return inner_func
@my_decorator
def big_box_process():
print("running my Big Box process")
# Demo usage
if __name__ == '__main__':
big_box_process()
PythonOutput:
This is some additional processing in the dcorator
running my Big Box process
Again some additional processing
PlaintextPass Arguments to Function
def my_decorator(func):
def inner_func(name: str, value: int):
print("This is some additional processing in the dcorator")
print(f"Using the name and value inside inner_func- name is {name} and value is {value}")
# call the provided function
func(name, value)
print("Again some additional processing")
return inner_func
@my_decorator
def big_box_process(name: str, value: int):
print("running my Big Box process")
print("name: ", name)
print("value: ", value)
# Demo usage
if __name__ == '__main__':
big_box_process("abc", 100)
PythonOutput:
This is some additional processing in the dcorator
Using the name and value inside inner_func- name is abc and value is 100
running my Big Box process
name: abc
value: 100
Again some additional processing
PlaintextPass Argument Effectively
To pass the arguments effectively, use the following arguments for the inner function inside the decorator.
Also pass the same arguments to the function(that applies the decorator), when you want to use it inside the decorator.
def my_decorator(func):
def inner_func(*args, **kwargs):
print("This is some additional processing in the dcorator")
print(f"Using the **args inside inner_func- {args}")
print(f"Using the **kwargs inside inner_func- {kwargs}")
# call the provided function
func(*args, **kwargs)
print("Again some additional processing")
return inner_func
@my_decorator
def big_box_process(name: str, value: int):
print("running my Big Box process")
print("name: ", name)
print("value: ", value)
# Demo usage
if __name__ == "__main__":
big_box_process("abc", value=100)
PythonOutput:
This is some additional processing in the dcorator
Using the **args inside inner_func- ('abc',)
Using the **kwargs inside inner_func- {'value': 100}
running my Big Box process
name: abc
value: 100
Again some additional processing
PlaintextDecorator with Return Value
import math
import time
# Decorator for time execution
def log_execution_time(func):
def wrapper(*args, **kwargs):
start_time = time.time_ns()
result = func(*args, **kwargs)
end_time = time.time_ns()
print(f"Execution time for {func.__name__}: {end_time - start_time:.6f} ns")
return result # return at the end of the wrapper
return wrapper
# Calculate circle area
@log_execution_time
def circle_area(radius):
if radius < 0:
raise ValueError("Radius must be a non-negative.")
return math.pi * (radius**2)
# Calculate rectangle area
@log_execution_time
def rectangle_area(length, width):
if length < 0 or width < 0:
raise ValueError("Length and width must be non-negative.")
return length * width
# Demo usage
if __name__ == "__main__":
try:
print("Circle Area:", circle_area(10))
print("Rectangle Area:", rectangle_area(5, 12))
except ValueError as e:
print(e)
PythonOutput:
Execution time for circle_area: 2601.000000 ns
Circle Area: 314.1592653589793
Execution time for rectangle_area: 739.000000 ns
Rectangle Area: 60
PlaintextPass Argument to Decorator
If the decorator needs to accept one or more param, then wrap the whole decorator inside a function that returns the decorator at the end.
def font_size(size: int):
def decorator(func):
def wrapper(*args, **kwargs):
return f'<span style="font-size: {size}px;">{func(*args, **kwargs)}</span>'
return wrapper
return decorator
# Demo usage
@font_size(24)
def sample_output():
return "Welcome to BigBoxCode"
print(sample_output())
PythonOutput:
<span style="font-size: 24px;">Welcome to BigBoxCode</span>
PlaintextChain Multiple Decorators
We can apply multiple decorators to the same function, just write the decorators one by one.
NOTES
The decorator that is applied first(at the top) will process first, and hold the rest of the implementation inside it.
def first_decorator(func):
def wrapper(*args, **kwargs):
print("Starting first_decorator")
func(*args, **kwargs)
print("Ending first_decorator")
return wrapper
def second_decorator(func):
def wrapper(*args, **kwargs):
print("Starting second_decorator")
func(*args, **kwargs)
print("Ending second_decorator")
return wrapper
# Usage
@first_decorator
@second_decorator
def big_box_process():
print("Executing big_box_process")
big_box_process()
PythonOutput:
Starting first_decorator
Starting second_decorator
Executing big_box_process
Ending second_decorator
Ending first_decorator
PlaintextAccess Function Metadata in Decorator
We can access the function meta data like __name__, __doc__,etc inside the decorator.
from functools import wraps
from typing import Callable
def my_decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling function [inside decorator]: {func.__name__}")
print(f"Docstring [inside decorator]: {func.__doc__}")
print(f"Annotations [inside decorator]: {func.__annotations__}")
result = func(*args, **kwargs)
return result
return wrapper
# Usage
@my_decorator
def big_box_process(a: int, b: int) -> int:
"My implementation for big box process execution"
return a + b
# Call the function
result = big_box_process(5, 3)
print(f"Result: {result}")
# Access metadata directly
print(f"Function Name: {big_box_process.__name__}")
print(f"Docstring: {big_box_process.__doc__}")
print(f"Annotations: {big_box_process.__annotations__}")
PythonOutput:
Calling function [inside decorator]: big_box_process
Docstring [inside decorator]: My implementation for big box process execution
Annotations [inside decorator]: {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
Result: 8
Function Name: big_box_process
Docstring: My implementation for big box process execution
Annotations: {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
PlaintextModify Function Metadata in Decorator
We can also change or add metadata inside the decorator.
from functools import wraps
from typing import Callable
def my_custom_metadata(key: str, value: str) -> Callable:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# Add the custom metadata to the function
setattr(wrapper, key, value)
return wrapper
return decorator
# Usage
@my_custom_metadata("custom_metadata", "This is a custom metadata for function.")
def big_box_process(a: int, b: int) -> int:
"My implementation for big box process execution"
return a + b
# Call the function
result = big_box_process(5, 3)
print(f"Result: {result}")
# Access metadata
print(f"Function Name: {big_box_process.__name__}")
print(f"Docstring: {big_box_process.__doc__}")
print(f"Annotations: {big_box_process.__annotations__}")
print(f"Custom Metadata: {getattr(big_box_process, 'custom_metadata', 'Not Found')}")
PythonOutput:
Result: 8
Function Name: big_box_process
Docstring: My implementation for big box process execution
Annotations: {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}
Custom Metadata: This is a custom metadata for function.
PlaintextProtect Function Metadata
When the function is passed to the decorator, the decorator creates a new function and returns that. So the metadata of the original function is no longer reserved, contained in the returned function form decorator.
Let’s see, what that means, and how we can preserve the original metadata of a function inside the decorator.
Without Metadata Protection
When we apply a decorator to a functional check of the metadata, it returns the metadata of wrapper function(not of the original function).
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def big_box_process(val: int) -> str:
"""My big box process implementation"""
return f"Provided value: {val}!"
print(big_box_process.__name__)
print(big_box_process.__doc__)
print(big_box_process.__annotations__)
PythonOutput:
wrapper
None
{}
PlaintextWith Metadata Protection
Apply decorator “@functools.wraps()” to preserve the metadata of the original function.
from functools import wraps
def my_decorator(func):
@wraps(func) # Use this to prserve the metadata of original function
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def big_box_process(val: int) -> str:
"""My big box process implementation"""
return f"Provided value: {val}!"
print(big_box_process.__name__)
print(big_box_process.__doc__)
print(big_box_process.__annotations__)
PythonOutput:
big_box_process
My big box process implementation
{'val': <class 'int'>, 'return': <class 'str'>}
PlaintextClass Decorator
Here is an example of class decorator-
from functools import wraps
# Class decorator
def my_class_decorator(cls):
# Add a new class attribute
cls.new_attribute = "This is a new attribute"
# Add a new method to the class
def new_method(self):
return f"New method called. Original attribute: {self.original_attribute}"
setattr(cls, "new_method", new_method)
# Decorate existing methods
for attr_name, attr_value in list(cls.__dict__.items()):
if callable(attr_value) and not attr_name.startswith("__"):
setattr(cls, attr_name, log_method(attr_value))
return cls
# Method decorator to log calls
def log_method(method):
@wraps(method)
def wrapper(*args, **kwargs):
print(
f"Calling method: {method.__name__} with args: {args[1:]}, kwargs: {kwargs}"
)
return method(*args, **kwargs)
return wrapper
# Applying the class decorator
@my_class_decorator
class MyClass:
def __init__(self, original_attribute):
self.original_attribute = original_attribute
def operation1(self, name: str):
"""Greet a person."""
return f"Hello, {name}! Your attribute is {self.original_attribute}"
def operation2(self, a: int, b: int) -> int:
"""Adds two numbers."""
return a + b
# Using the decorated class
obj = MyClass("Decorated Class")
# Accessing original and added attributes
print(f"Original Attribute: {obj.original_attribute}")
print(f"New Attribute: {MyClass.new_attribute}")
# Calling original methods (now logged)
print(obj.operation1("BigBoxCode"))
print(obj.operation2(10, 20))
# Calling the new method added by the decorator
print(obj.new_method())
PythonOutput:
Original Attribute: Decorated Class
New Attribute: This is a new attribute
Calling method: operation1 with args: ('BigBoxCode',), kwargs: {}
Hello, BigBoxCode! Your attribute is Decorated Class
Calling method: operation2 with args: (10, 20), kwargs: {}
30
Calling method: new_method with args: (), kwargs: {}
New method called. Original attribute: Decorated Class
PlaintextExamples [Custom Decorators]
Here are some examples of how we can define and use decorators-
Example #1: Uppercase Decorator
from typing import Callable
def uppercase(func: Callable[..., str]) -> Callable[..., str]:
def wrapper(*args: object, **kwargs: object) -> str:
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase
def user_greeting(name: str) -> str:
return f"Hello, {name}"
print(user_greeting("John Doe"))
PythonOutput:
HELLO, JOHN DOE
PlaintextExample #2: Apply HTML Tags(chaining decorators)
def bold(func):
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
def font_size(size: int):
def decorator(func):
def wrapper(*args, **kwargs):
return f'<span style="font-size: {size}px;">{func(*args, **kwargs)}</span>'
return wrapper
return decorator
# Demo usage
@bold
@italic
@font_size(24)
def sample_output():
return "Welcome to BigBoxCode"
print(sample_output())
PythonOutput:
<b><i><span style="font-size: 24px;">Welcome to BigBoxCode</span></i></b>
PlaintextExample #3: State Management
from functools import wraps
from typing import Callable, Any
def memoize(func: Callable) -> Callable:
cache = {}
@wraps(func)
def wrapper(*args, **kwargs) -> Any:
# Create a key from the function arguments
key = (args, frozenset(kwargs.items()))
print(f"Cache Key: {key}")
if key not in cache:
print(f"Cache miss. calculating...")
cache[key] = func(*args, **kwargs)
else:
print(f"Cache hit.")
return cache[key]
# Expose if required(for debugging or any other reason)
wrapper.cache = cache
return wrapper
# Usage
@memoize
def big_box_process(x: int, y: int) -> int:
"""Simulates an expensive computation."""
return x**y
# Function calls
print(big_box_process(2, 10))
print(big_box_process(2, 10))
print(big_box_process(3, 5))
print(big_box_process(3, 5))
# Inspecting the cache
print("Cache contents:", big_box_process.cache)
PythonOutput:
Cache Key: ((2, 10), frozenset())
Cache miss. calculating...
1024
Cache Key: ((2, 10), frozenset())
Cache hit.
1024
Cache Key: ((3, 5), frozenset())
Cache miss. calculating...
243
Cache Key: ((3, 5), frozenset())
Cache hit.
243
Cache contents: {((2, 10), frozenset()): 1024, ((3, 5), frozenset()): 243}
PlaintextExample #4: Request Router
class App:
routes = {}
# Router decorator
def route(self, path):
def decorator(func):
# Perform some checking in the decorator
if not self.check_auth():
return "403 Forbidden: Unauthorized"
# Function to just wrap the full request
# Not directly related to decorator
def request_wrapper(*args, **kwargs):
response = func(*args, **kwargs)
headers = {"X-Customer-Id": "100"}
return f"Headers: {headers}\n{response}"
# Register the wrapped function
self.routes[path] = request_wrapper
return request_wrapper
return decorator
def check_auth(self):
# Dummy authentication
# Returing true for dummy purpose
return True
def handle_reqeust(self, path):
if path in self.routes:
return self.routes[path]()
else:
return "404 Not Found"
# Demo usage
app = App()
@app.route("/")
def home_page():
return "bigboxcode home"
@app.route("/contact")
def contact_page():
return "Contact us at support@bigboxcode.com"
# handle request when it comes from the user
print(app.handle_reqeust("/"))
print(app.handle_reqeust("/contact"))
print(app.handle_reqeust("/sone-unknown-path"))
PythonOutput:
Headers: {'X-Customer-Id': '100'}
bigboxcode home
Headers: {'X-Customer-Id': '100'}
Contact us at support@bigboxcode.com
404 Not Found
PlaintextBuiltin Decorators [Commonly Used]
@staticmethod
class BigBoxOps:
@staticmethod
def my_static(a: int, b: int) -> int:
return a + b
# Demo usage
result = BigBoxOps.my_static(2, 3)
print(result)
PythonOutput:
5
Plaintext@classmethod
class BigBoxOps:
no_of_days: int = 7
@classmethod
def get_no_of_days(self) -> int:
return self.no_of_days
# Demo usage
result = BigBoxOps.get_no_of_days()
print(result)
PythonOutput:
7
Plaintext@property
class Rectangle:
def __init__(self, height: float, width: float) -> None:
self.__height = height
self.__width = width
@property
def height(self) -> float:
return self.__height
@property
def width(self) -> float:
return self.__width
@property
def area(self) -> float:
return self.__height * self.__width
# demo usage
rectangle = Rectangle(50, 100)
print(f"Height: {rectangle.height}")
print(f"Width: {rectangle.width}")
print(f"Area: {rectangle.area}")
PythonOutput:
Height: 50
Width: 100
Area: 5000
Plaintext@abstractmethod
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
# Demo usage
rect = Rectangle(4, 5)
print(rect.area())
PythonOutput:
20
Plaintext@functools.lru_cache
from functools import lru_cache
@lru_cache(maxsize=None)
def slow_function(x: float) -> float:
"""This simulates some slow functions"""
print(f"Cache not found for {x}. Calculating...")
return x * x
# Demo usage
print(f"Slow function result for 4: {slow_function(4)}")
print(f"Slow function result for 4: {slow_function(4)}")
print(f"Slow function result for 5: {slow_function(5)}")
print(f"Slow function result for 4: {slow_function(4)}")
print(f"Slow function result for 5: {slow_function(5)}")
print(f"Slow function result for 6: {slow_function(6)}")
print(f"Slow function result for 6: {slow_function(6)}")
PythonOutput:
Cache not found for 4. Calculating...
Slow function result for 4: 16
Slow function result for 4: 16
Cache not found for 5. Calculating...
Slow function result for 5: 25
Slow function result for 4: 16
Slow function result for 5: 25
Cache not found for 6. Calculating...
Slow function result for 6: 36
Slow function result for 6: 36
Plaintext@functools.wraps
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_func(name):
"""Sample function"""
return f"Hello, {name}"
# Demo usage
print(my_func.__name__)
print(my_func.__doc__)
PythonOutput:
my_func
Sample function
Plaintext@contextlib.contextmanager
import os
import tempfile
from contextlib import contextmanager
@contextmanager
def temporary_file():
"""
Context manager for creating and automatically cleaning up a temporary file.
"""
temp_file = tempfile.NamedTemporaryFile(delete=False)
try:
print(f"Created temporary file: {temp_file.name}")
yield temp_file.name
finally:
try:
os.remove(temp_file.name)
print(f"Deleted temporary file: {temp_file.name}")
except FileNotFoundError:
print("File already deleted.")
# Demo usage
with temporary_file() as file_path:
print(f"Working with temporary file: {file_path}")
with open(file_path, "w") as f:
f.write("Hello, this is a temporary file!")
# File exists at this point
with open(file_path, "r") as f:
print(f.read())
PythonOutput:
Created temporary file: /tmp/tmppvhgz_vi
Working with temporary file: /tmp/tmppvhgz_vi
Hello, this is a temporary file!
Deleted temporary file: /tmp/tmppvhgz_vi
Plaintext