Decorators are a powerful feature in Python that allows you to modify or enhance the behavior of functions or methods. Decorators wrap a function, enabling you to run additional code before or after the function executes, without changing the function's original structure.
Decorators take a function as an argument and return a new function that adds some behavior to the original one. They are a form of meta-programming because a part of the program tries to modify another part at runtime.
In Python, functions are first-class objects, meaning they can:
• Be passed as arguments to other functions.
• Be returned from other functions.
• Be assigned to variables.
def greet(name):
return f"Hello, {name}!"
# Assigning a function to a variable
say_hello = greet
print(say_hello("John")) # Output: Hello, John!
Functions can be defined within other functions. These are called inner functions and are often used in decorators.
def outer_func():
def inner_func():
print("This is the inner function.")
inner_func()
outer_func()
# Output: This is the inner function.
Basic Decorator Without Arguments
def my_decorator(func):
def wrapper():
print("Something before the function.")
func()
print("Something after the function.")
return wrapper
# Applying the decorator to a function
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Something before the function.
Hello!
Something after the function.
In this example:
• my_decorator is the decorator function that takes another function (say_hello) as an argument.
• The wrapper function adds behavior before and after calling the original function (func()).
You can decorate functions that accept parameters by passing them through the wrapper function.
def my_decorator(func):
def wrapper(name):
print(f"Hello {name}, before function
execution.")
func(name)
print(f"Goodbye {name}, after function
execution.")
return wrapper
@my_decorator
def greet(name):
print(f"Welcome {name}!")
greet("Alice")
Hello Alice, before function execution.
Welcome Alice!
Goodbye Alice, after function execution.
Using the @ symbol is a more Pythonic and concise way to apply decorators, which eliminates the need to manually assign the decorator.
@my_decorator
def say_hello():
print("Hello!")
is equivalent to:
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello)
In addition to function decorators, you can use classes as decorators, which is useful when you need to maintain the state.
class MyDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("Something before the function.")
self.func(*args, **kwargs)
print("Something after the function.")
@MyDecorator
def say_hello(name):
print(f"Hello {name}!")
say_hello("Alice")
Something before the function.
Hello Alice!
Something after the function.
You can pass arguments to decorators by using another layer of functions.
def repeat(n):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args,
**kwargs)
return wrapper
return decorator
@repeat(3)
def say_hello(name):
print(f"Hello {name}!")
say_hello("Alice")
Hello Alice!
Hello Alice!
Hello Alice!
1. @staticmethod: Used for static methods in a class.
2. @classmethod: Used for class methods, which take the class as the first argument.
3. @property: Used to define getter/setter methods for class attributes.
class MyClass:
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
self._value = new_value
obj = MyClass(10)
print(obj.value) # Output: 10
obj.value = 20
print(obj.value) # Output: 20
• Code reuse: Allows you to reuse functionality without modifying the original function.
• Code modularity: Separation of concerns, improving code readability and maintainability.
• Useful in scenarios like logging, authentication, caching, and more.