Python Topic
Python Decorators & Closures
1. Higher-Order Functions
Purpose: Functions that take other functions as arguments or return functions as results.
Functions as Arguments:
python
def greet(name):
return f"Hello, {name}!"
def shout(func, name):
"""A higher-order function that takes a function as argument."""
result = func(name)
return result.upper() + "!"
def whisper(func, name):
"""Another higher-order function."""
result = func(name)
return result.lower() + "..."
# Using higher-order functions
print(shout(greet, "Alice")) # HELLO, ALICE!!
print(whisper(greet, "Bob")) # hello, bob...
Functions as Return Values:
python
def create_multiplier(factor):
"""Returns a new function that multiplies by the given factor."""
def multiplier(x):
return x * factor
return multiplier
# Create specialized functions
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# You can create any multiplier you want
times_ten = create_multiplier(10)
print(times_ten(7)) # 70
Practical Example:
python
def apply_operation(numbers, operation):
"""Apply an operation to all numbers in a list."""
return [operation(num) for num in numbers]
def square(x):
return x ** 2
def cube(x):
return x ** 3
numbers = [1, 2, 3, 4, 5]
squared_numbers = apply_operation(numbers, square)
cubed_numbers = apply_operation(numbers, cube)
print(f"Squared: {squared_numbers}") # [1, 4, 9, 16, 25]
print(f"Cubed: {cubed_numbers}") # [1, 8, 27, 64, 125]
2. Closures
Purpose: Functions that "remember" the environment in which they were created, even after that environment is gone.
Basic Closure:
python
def outer_function(message):
"""Outer function that creates a closure."""
def inner_function():
# inner_function remembers 'message' from outer scope
print(message)
return inner_function
# Create closures
hello_closure = outer_function("Hello, World!")
goodbye_closure = outer_function("Goodbye, World!")
# Call the closures
hello_closure() # Hello, World!
goodbye_closure() # Goodbye, World!
Closure with Parameters:
python
def power_factory(exponent):
"""Create a power function with a fixed exponent."""
def power(base):
return base ** exponent
return power
# Create specialized power functions
square = power_factory(2)
cube = power_factory(3)
sqrt = power_factory(0.5)
print(square(5)) # 25
print(cube(3)) # 27
print(sqrt(16)) # 4.0
Closure with Mutable State:
python
def counter_factory():
"""Create a counter that remembers its state."""
count = 0 # This is remembered by the closure
def counter():
nonlocal count # Needed to modify the outer variable
count += 1
return count
return counter
# Create independent counters
counter1 = counter_factory()
counter2 = counter_factory()
print(counter1()) # 1
print(counter1()) # 2
print(counter1()) # 3
print(counter2()) # 1 (independent state)
print(counter2()) # 2
Practical Closure Example:
python
def create_authenticator(valid_tokens):
"""Create an authentication function for specific tokens."""
attempts = 0
max_attempts = 3
def authenticate(token):
nonlocal attempts
if attempts >= max_attempts:
return "Blocked: Too many failed attempts"
if token in valid_tokens:
attempts = 0 # Reset on success
return "Access granted"
else:
attempts += 1
remaining = max_attempts - attempts
return f"Access denied. {remaining} attempts remaining"
return authenticate
# Create authenticators for different systems
admin_auth = create_authenticator({"admin123", "superuser456"})
user_auth = create_authenticator({"user789", "guest000"})
print(admin_auth("wrong")) # Access denied. 2 attempts remaining
print(admin_auth("admin123")) # Access granted
print(user_auth("user789")) # Access granted
3. Function Decorators
Purpose: Modify or enhance functions without changing their source code.
Basic Decorator Syntax:
python
def my_decorator(func):
"""A simple decorator that adds functionality."""
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!")
# Using the decorated function
say_hello()
# Output:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.
What @decorator Actually Does:
python
# This:
@my_decorator
def say_hello():
print("Hello!")
# Is equivalent to:
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello)
Decorators for Functions with Arguments:
python
def smart_divide(func):
"""Decorator that handles division by zero."""
def wrapper(a, b):
if b == 0:
return "Error: Division by zero!"
return func(a, b)
return wrapper
@smart_divide
def divide(a, b):
return a / b
print(divide(10, 2)) # 5.0
print(divide(10, 0)) # Error: Division by zero!
Decorators with *args and **kwargs:
python
def logger(func):
"""Decorator that logs function calls."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@logger
def add(a, b):
return a + b
@logger
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(add(5, 3))
# Output:
# Calling add with args: (5, 3), kwargs: {}
# add returned: 8
# 8
print(greet("Alice", greeting="Hi"))
# Output:
# Calling greet with args: ('Alice',), kwargs: {'greeting': 'Hi'}
# greet returned: Hi, Alice!
# Hi, Alice!
4. Advanced Decorators
Decorators with Arguments:
python
def repeat(times):
"""Decorator that repeats function execution."""
def decorator(func):
def wrapper(*args, **kwargs):
for i in range(times):
print(f"Execution {i + 1}:")
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
# Output:
# Execution 1:
# Hello, Alice!
# Execution 2:
# Hello, Alice!
# Execution 3:
# Hello, Alice!
Multiple Decorators:
python
def bold(func):
def wrapper():
return "<b>" + func() + "</b>"
return wrapper
def italic(func):
def wrapper():
return "<i>" + func() + "</i>"
return wrapper
@bold
@italic
def hello():
return "Hello, World!"
print(hello()) # <b><i>Hello, World!</i></b>
# Equivalent to:
# hello = bold(italic(hello))
Class-based Decorators:
python
class Timer:
"""Decorator as a class to measure execution time."""
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
import time
start = time.time()
result = self.func(*args, **kwargs)
end = time.time()
print(f"{self.func.__name__} executed in {end - start:.4f} seconds")
return result
@Timer
def slow_function():
import time
time.sleep(1)
return "Done"
print(slow_function())
# Output:
# slow_function executed in 1.0012 seconds
# Done
5. Practical Examples
Example 1: Authentication and Authorization System
python
def require_auth(role="user"):
"""Decorator to require authentication and specific role."""
def decorator(func):
def wrapper(user, *args, **kwargs):
if not user.get('authenticated', False):
return "Error: Authentication required"
if user.get('role') != role and role != "any":
return f"Error: {role} role required"
return func(user, *args, **kwargs)
return wrapper
return decorator
# User database (simulated)
users = {
'alice': {'name': 'Alice', 'role': 'user', 'authenticated': True},
'bob': {'name': 'Bob', 'role': 'admin', 'authenticated': True},
'eve': {'name': 'Eve', 'role': 'user', 'authenticated': False}
}
class UserSystem:
@require_auth(role="any")
def view_profile(self, user):
return f"Viewing profile of {user['name']}"
@require_auth(role="user")
def edit_profile(self, user):
return f"{user['name']} is editing their profile"
@require_auth(role="admin")
def delete_user(self, user):
return f"{user['name']} is deleting a user (admin action)"
# Test the system
system = UserSystem()
print(system.view_profile(users['alice'])) # Viewing profile of Alice
print(system.edit_profile(users['alice'])) # Alice is editing their profile
print(system.delete_user(users['alice'])) # Error: admin role required
print(system.delete_user(users['bob'])) # Bob is deleting a user (admin action)
print(system.view_profile(users['eve'])) # Error: Authentication required
Example 2: Caching Decorator (Memoization)
python
def cache(func):
"""Decorator that caches function results."""
cache_dict = {}
def wrapper(*args):
if args in cache_dict:
print(f"Cache hit for {args}")
return cache_dict[args]
print(f"Computing for {args}...")
result = func(*args)
cache_dict[args] = result
return result
return wrapper
@cache
def fibonacci(n):
"""Compute nth Fibonacci number (inefficient without caching)."""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@cache
def factorial(n):
"""Compute factorial."""
if n == 0:
return 1
return n * factorial(n - 1)
# Test caching
print("Fibonacci(5):", fibonacci(5))
print("\nFactorial(5):", factorial(5))
print("\nFibonacci(5) again:", fibonacci(5)) # From cache
Example 3: Rate Limiting Decorator
python
import time
def rate_limit(max_calls, period):
"""Decorator to limit function calls per time period."""
def decorator(func):
calls = []
def wrapper(*args, **kwargs):
now = time.time()
# Remove calls outside the current period
calls[:] = [call_time for call_time in calls if now - call_time < period]
if len(calls) >= max_calls:
oldest_call = calls[0]
wait_time = period - (now - oldest_call)
return f"Rate limit exceeded. Try again in {wait_time:.1f} seconds"
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=3, period=10) # 3 calls per 10 seconds
def api_call(endpoint):
return f"Calling {endpoint}... Success!"
# Test rate limiting
for i in range(5):
result = api_call(f"/users/{i}")
print(f"Attempt {i + 1}: {result}")
time.sleep(1)
Example 4: Validation Decorator
python
def validate_input(**validators):
"""Decorator to validate function arguments."""
def decorator(func):
def wrapper(*args, **kwargs):
# Combine args and kwargs for validation
from inspect import signature
sig = signature(func)
bound_args = sig.bind(*args, **kwargs)
bound_args.apply_defaults()
all_args = bound_args.arguments
# Validate each argument
for arg_name, validator in validators.items():
if arg_name in all_args:
value = all_args[arg_name]
if not validator(value):
raise ValueError(f"Invalid value for {arg_name}: {value}")
return func(*args, **kwargs)
return wrapper
return decorator
# Validator functions
def is_positive(x):
return x > 0
def is_non_empty_string(s):
return isinstance(s, str) and len(s.strip()) > 0
def is_valid_email(email):
return '@' in email and '.' in email
@validate_input(
age=is_positive,
name=is_non_empty_string,
email=is_valid_email
)
def create_user(name, age, email):
return f"User created: {name}, {age}, {email}"
# Test validation
try:
print(create_user("Alice", 25, "alice@example.com")) # Valid
print(create_user("", 25, "alice@example.com")) # Invalid name
except ValueError as e:
print(f"Error: {e}")
try:
print(create_user("Bob", -5, "bob@example.com")) # Invalid age
except ValueError as e:
print(f"Error: {e}")
Example 5: Retry Decorator
python
import random
import time
def retry(max_attempts=3, delay=1, backoff=2, exceptions=(Exception,)):
"""Decorator to retry function on exception."""
def decorator(func):
def wrapper(*args, **kwargs):
attempts = 0
current_delay = delay
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except exceptions as e:
attempts += 1
if attempts == max_attempts:
raise e
print(f"Attempt {attempts} failed: {e}. Retrying in {current_delay}s...")
time.sleep(current_delay)
current_delay *= backoff
return func(*args, **kwargs) # Final attempt
return wrapper
return decorator
@retry(max_attempts=3, delay=1, exceptions=(ValueError,))
def unreliable_operation():
"""Simulate an operation that might fail."""
if random.random() < 0.7: # 70% chance of failure
raise ValueError("Temporary failure")
return "Operation successful!"
# Test retry mechanism
for i in range(3):
try:
result = unreliable_operation()
print(f"Run {i + 1}: {result}")
except Exception as e:
print(f"Run {i + 1}: Failed after all retries - {e}")
print()
6. Preserving Function Metadata
Using functools.wraps:
python
from functools import wraps
def logger(func):
@wraps(func) # Preserves function metadata
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logger
def say_hello(name):
"""Greets a person by name."""
return f"Hello, {name}!"
print(say_hello.__name__) # say_hello (without @wraps, it would be 'wrapper')
print(say_hello.__doc__) # Greets a person by name.
Key Takeaways:
-
Higher-Order Functions: Functions that take or return other functions
-
Closures: Functions that remember their creation environment
-
Decorators: Functions that modify other functions
-
@decorator syntax: Syntactic sugar for function wrapping
-
Use
functools.wraps: To preserve function metadata
Comments (0)
No comments yet. Be the first to comment!