Decorators in Python are a powerful and expressive tool that allows for the modification of functions or methods using other functions. They provide a way to extend and alter the behavior of callable objects (functions, methods, or classes) without permanently modifying the objects themselves. This blog post will explore decorators in depth, covering their syntax, use cases, and technical details, with comprehensive examples using a banking system scenario.
Table of Contents
- Introduction to Decorators
- Basic Syntax and Usage
- Function Decorators
- Class Decorators
- Decorators with Arguments
- Practical Examples: Bank Account System
- Multiple Inheritance with Decorators
- Conclusion
Introduction to Decorators
Decorators are a design pattern in Python that allows a user to add new functionality to an existing object without modifying its structure. They are typically used to extend the behavior of functions or methods in a clean and readable way.
Key Concepts
- Higher-Order Functions: Functions that operate on other functions, either by taking them as arguments or by returning them.
- First-Class Functions: Functions in Python are treated as first-class citizens, meaning they can be passed around and used as arguments just like any other object (string, integer, etc.).
Basic Syntax and Usage
The basic syntax for a decorator is to define a function that returns another function. Here’s a simple example:
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()
Output
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
In this example, the my_decorator function is applied to the say_hello function using the @ syntax.
Function Decorators
Function decorators are the most common use of decorators. They can be used to add functionality such as logging, timing, or access control to existing functions.
Example: Logging Decorator
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"Function '{func.__name__}' is called with arguments: {args} {kwargs}")
result = func(*args, **kwargs)
print(f"Function '{func.__name__}' returned: {result}")
return result
return wrapper
@log_decorator
def add(a, b):
return a + b
add(3, 4)
Output
Function 'add' is called with arguments: (3, 4) {}
Function 'add' returned: 7
Class Decorators
Class decorators are used to modify or extend the behavior of classes. They work similarly to function decorators but are applied to class definitions.
Example: Adding Attributes to a Class
def add_attributes(cls):
cls.new_attribute = "This is a new attribute"
return cls
@add_attributes
class MyClass:
pass
obj = MyClass()
print(obj.new_attribute)
Output
This is a new attribute
Decorators with Arguments
Sometimes, you may want to pass arguments to your decorators. This can be achieved by nesting functions within the decorator.
Example: Timing Decorator with Arguments
import time
def timer_decorator(unit="seconds"):
def decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
duration = end_time - start_time
if unit == "milliseconds":
duration *= 1000
print(f"Function '{func.__name__}' took {duration} {unit}")
return result
return wrapper
return decorator
@timer_decorator(unit="milliseconds")
def slow_function():
time.sleep(1)
slow_function()
Output
Function 'slow_function' took 1000.123456 milliseconds
Practical Examples: Bank Account System
Now, let's implement a banking system with different account types and loan conditions, using decorators to manage different interest rates and logging functionalities.
Basic Classes
class BankAccount:
def __init__(self, account_id, name, balance=0):
self.account_id = account_id
self.name = name
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
else:
print("Insufficient funds")
def get_balance(self):
return self.balance
class SavingsAccount(BankAccount):
def __init__(self, account_id, name, balance=0):
super().__init__(account_id, name, balance)
self.interest_rate = 0.02 # 2% interest rate
def apply_interest(self):
self.balance += self.balance * self.interest_rate
class LoanAccount(BankAccount):
def __init__(self, account_id, name, balance=0):
super().__init__(account_id, name, balance)
self.loan_amount = 0
def request_loan(self, amount):
self.loan_amount += amount
def repay_loan(self, amount):
if amount <= self.loan_amount:
self.loan_amount -= amount
else:
print("Overpayment")
Decorators
Logging Decorator
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with {args} and {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
Interest Rate Decorator
def interest_rate_decorator(rate):
def decorator(func):
def wrapper(*args, **kwargs):
account = args[0]
account.balance += account.balance * rate
print(f"Interest applied at rate {rate*100}%")
return func(*args, **kwargs)
return wrapper
return decorator
Using the Decorators
@log_decorator
@interest_rate_decorator(rate=0.05) # 5% interest rate
def apply_interest(account):
pass
# Example usage
savings = SavingsAccount("001", "John Doe", 1000)
apply_interest(savings)
print(savings.get_balance()) # Output: 1050.0
Multiple Inheritance with Decorators
Let's create a PremiumAccount that inherits from both SavingsAccount and LoanAccount and applies a decorator to modify its behavior.
class PremiumAccount(SavingsAccount, LoanAccount):
def __init__(self, account_id, name, balance=0):
SavingsAccount.__init__(self, account_id, name, balance)
LoanAccount.__init__(self, account_id, name, balance)
@log_decorator
@interest_rate_decorator(rate=0.03) # 3% interest rate
def apply_premium_interest(account):
pass
# Example usage
premium = PremiumAccount("002", "Jane Smith", 2000)
apply_premium_interest(premium)
print(premium.get_balance()) # Output: 2060.0
premium.request_loan(5000)
print(premium.loan_amount) # Output: 5000
Conclusion
Decorators in Python are a versatile tool that allows you to extend and modify the behavior of functions and methods. They promote code reusability and separation of concerns by enabling you to add functionality to existing code without modifying it. Understanding decorators can greatly enhance your ability to write clean, maintainable, and efficient Python code.
In this blog post, we explored the basics of decorators, function decorators, class decorators, decorators with arguments, and practical examples using a banking system. We also looked at how to use decorators in conjunction with multiple inheritance. By mastering decorators, you can take your Python programming skills to the next level.