Understanding Decorators
Learning Objectives
- By the end of this lesson, you will be able to:
- - Understand functions as first-class objects
- - Work with higher-order functions
- - Understand the concept of decorators
- - Recognize how decorators work
- - Understand function wrapping
- - See decorators in action
- - Understand the foundation for creating decorators
- - Recognize common decorator patterns
- - Understand function metadata preservation
- - Apply decorator concepts
Lesson 13.1: Understanding Decorators
Learning Objectives
By the end of this lesson, you will be able to:
- Understand functions as first-class objects
- Work with higher-order functions
- Understand the concept of decorators
- Recognize how decorators work
- Understand function wrapping
- See decorators in action
- Understand the foundation for creating decorators
- Recognize common decorator patterns
- Understand function metadata preservation
- Apply decorator concepts
Introduction to Decorators
Decorators are a powerful feature in Python that allow you to modify or extend the behavior of functions or classes without permanently modifying their code.
Why Decorators?
- Code reuse: Add functionality to multiple functions
- Separation of concerns: Keep core logic separate from cross-cutting concerns
- Clean code: Avoid repetitive code
- Pythonic: Idiomatic Python pattern
- Flexible: Can be applied to any function
What Are Decorators?
Decorators are functions that take another function as an argument and return a modified version of that function.
Functions as First-Class Objects
In Python, functions are first-class objects, meaning they can be:
- Assigned to variables
- Passed as arguments to other functions
- Returned from functions
- Stored in data structures
Assigning Functions to Variables
def greet(name):
return f"Hello, {name}!"
# Assign function to variable
say_hello = greet
print(say_hello("Alice")) # Hello, Alice!
# Function is an object
print(type(greet)) # <class 'function'>
print(greet) # <function greet at 0x...>
Functions as Arguments
def apply_operation(x, operation):
"""Apply operation to x."""
return operation(x)
def square(n):
return n ** 2
def cube(n):
return n ** 3
# Pass functions as arguments
result1 = apply_operation(5, square) # 25
result2 = apply_operation(5, cube) # 125
print(result1, result2) # 25 125
Functions as Return Values
def get_operation(operation_type):
"""Return a function based on operation type."""
def add(x, y):
return x + y
def multiply(x, y):
return x * y
if operation_type == "add":
return add
elif operation_type == "multiply":
return multiply
# Get and use function
add_func = get_operation("add")
result = add_func(3, 4)
print(result) # 7
Functions in Data Structures
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
# Store functions in list
operations = [add, subtract, multiply]
# Use functions from list
for op in operations:
print(op(10, 5))
# Output: 15, 5, 50
# Store in dictionary
ops_dict = {
'add': add,
'subtract': subtract,
'multiply': multiply
}
result = ops_dict['add'](10, 5)
print(result) # 15
Function Attributes
def greet(name):
"""Greet someone."""
return f"Hello, {name}!"
# Functions have attributes
print(greet.__name__) # greet
print(greet.__doc__) # Greet someone.
print(greet.__module__) # __main__
print(greet.__code__) # <code object ...>
Higher-Order Functions
Higher-order functions are functions that:
- Take other functions as arguments, or
- Return functions as results
Functions That Take Functions as Arguments
def apply_twice(func, value):
"""Apply function twice to value."""
return func(func(value))
def add_one(x):
return x + 1
result = apply_twice(add_one, 5)
print(result) # 7 (5 + 1 + 1)
Functions That Return Functions
def make_multiplier(n):
"""Create a function that multiplies by n."""
def multiplier(x):
return x * n
return multiplier
# Create specialized functions
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
Closure Example
def create_counter():
"""Create a counter function with closure."""
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
# Create counter instances
counter1 = create_counter()
counter2 = create_counter()
print(counter1()) # 1
print(counter1()) # 2
print(counter2()) # 1 (independent)
print(counter1()) # 3
Practical Example: Function Timer
import time
def timer(func):
"""Measure function execution time."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
def slow_function():
time.sleep(1)
return "Done"
# Wrap function
timed_function = timer(slow_function)
result = timed_function()
# Output: slow_function took 1.0000 seconds
Decorator Basics
A decorator is a function that takes another function and extends its behavior without explicitly modifying it.
Simple Decorator Example
def my_decorator(func):
"""Simple decorator that prints before and after."""
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
def say_hello():
print("Hello!")
# Apply decorator manually
decorated_hello = my_decorator(say_hello)
decorated_hello()
# Output:
# Before function call
# Hello!
# After function call
Decorator Syntax (@)
def my_decorator(func):
"""Simple decorator."""
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
# Using @ syntax (decorator syntax)
@my_decorator
def say_hello():
print("Hello!")
# Now say_hello is decorated
say_hello()
# Output:
# Before function call
# Hello!
# After function call
Understanding the Decorator Pattern
# This:
@my_decorator
def my_function():
pass
# Is equivalent to:
def my_function():
pass
my_function = my_decorator(my_function)
Decorator with Arguments
def my_decorator(func):
"""Decorator that handles function arguments."""
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
@my_decorator
def add(x, y):
return x + y
result = add(3, 4)
# Output:
# Calling add with (3, 4) and {}
# add returned 7
Preserving Function Metadata
def my_decorator(func):
"""Decorator that preserves metadata."""
def wrapper(*args, **kwargs):
"""Wrapper function."""
return func(*args, **kwargs)
# Preserve metadata
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
@my_decorator
def greet(name):
"""Greet someone."""
return f"Hello, {name}!"
print(greet.__name__) # greet
print(greet.__doc__) # Greet someone.
Using functools.wraps
from functools import wraps
def my_decorator(func):
"""Decorator using functools.wraps."""
@wraps(func) # Preserves metadata automatically
def wrapper(*args, **kwargs):
"""Wrapper function."""
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greet someone."""
return f"Hello, {name}!"
print(greet.__name__) # greet
print(greet.__doc__) # Greet someone.
Common Decorator Patterns
1. Logging Decorator
from functools import wraps
def log_calls(func):
"""Log function calls."""
@wraps(func)
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
@log_calls
def add(x, y):
return x + y
result = add(3, 4)
# Output:
# Calling add with args=(3, 4), kwargs={}
# add returned 7
2. Timing Decorator
import time
from functools import wraps
def timer(func):
"""Measure function execution time."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "Done"
result = slow_function()
# Output: slow_function took 1.0000 seconds
3. Retry Decorator
from functools import wraps
import time
def retry(max_attempts=3, delay=1):
"""Retry function on failure."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=1)
def unreliable_function():
import random
if random.random() < 0.7:
raise ValueError("Random failure")
return "Success"
4. Validation Decorator
from functools import wraps
def validate_positive(func):
"""Validate that arguments are positive."""
@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError(f"Argument {arg} must be positive")
return func(*args, **kwargs)
return wrapper
@validate_positive
def square_root(x):
return x ** 0.5
result = square_root(16) # 4.0
# result = square_root(-4) # ValueError
Understanding Decorator Execution
Decorator Execution Order
def decorator1(func):
print("Decorator 1 applied")
def wrapper():
print("Wrapper 1")
return func()
return wrapper
def decorator2(func):
print("Decorator 2 applied")
def wrapper():
print("Wrapper 2")
return func()
return wrapper
@decorator1
@decorator2
def my_function():
print("Function executed")
# Decorators are applied bottom-up
# Output when module loads:
# Decorator 2 applied
# Decorator 1 applied
my_function()
# Output:
# Wrapper 1
# Wrapper 2
# Function executed
Stacking Decorators
def bold(func):
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def italic(func):
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@bold
@italic
def hello():
return "Hello"
print(hello()) # <b><i>Hello</i></b>
Practical Examples
Example 1: Function Call Counter
from functools import wraps
def count_calls(func):
"""Count how many times function is called."""
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.call_count += 1
print(f"{func.__name__} called {wrapper.call_count} times")
return func(*args, **kwargs)
wrapper.call_count = 0
return wrapper
@count_calls
def greet(name):
return f"Hello, {name}!"
greet("Alice") # greet called 1 times
greet("Bob") # greet called 2 times
Example 2: Cache Decorator
from functools import wraps
def cache(func):
"""Cache function results."""
cache_dict = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in cache_dict:
cache_dict[key] = func(*args, **kwargs)
return cache_dict[key]
return wrapper
@cache
def expensive_function(n):
print(f"Computing for {n}")
return n ** 2
print(expensive_function(5)) # Computing for 5, then 25
print(expensive_function(5)) # 25 (cached, no computation)
Example 3: Rate Limiting
import time
from functools import wraps
def rate_limit(calls_per_second):
"""Limit function call rate."""
min_interval = 1.0 / calls_per_second
last_called = [0.0]
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.time() - last_called[0]
left_to_wait = min_interval - elapsed
if left_to_wait > 0:
time.sleep(left_to_wait)
last_called[0] = time.time()
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(calls_per_second=2)
def api_call():
return "API response"
# Function can only be called twice per second
Common Mistakes and Pitfalls
1. Forgetting @wraps
# WRONG: Metadata is lost
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greet someone."""
return f"Hello, {name}!"
print(greet.__name__) # wrapper (wrong!)
# CORRECT: Use @wraps
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
2. Not Handling Arguments
# WRONG: Doesn't handle arguments
def my_decorator(func):
def wrapper():
return func()
return wrapper
@my_decorator
def add(x, y):
return x + y
# add(3, 4) # TypeError!
# CORRECT: Use *args, **kwargs
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
3. Decorator Execution Timing
# Decorators are executed at function definition time, not call time
def my_decorator(func):
print(f"Decorating {func.__name__}") # Executes immediately
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_function():
pass
# "Decorating my_function" is printed when module loads
Best Practices
1. Always Use @wraps
from functools import wraps
def my_decorator(func):
@wraps(func) # Always use this
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
2. Handle All Arguments
def my_decorator(func):
def wrapper(*args, **kwargs): # Handle all arguments
return func(*args, **kwargs)
return wrapper
3. Keep Decorators Simple
# Good: Simple, focused decorator
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
# Avoid: Complex logic in decorator
# Move complex logic to separate functions
4. Document Decorators
def my_decorator(func):
"""Decorator that does something.
Args:
func: Function to decorate
Returns:
Decorated function
"""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
Practice Exercise
Exercise: Basic Decorators
Objective: Create a Python program that demonstrates understanding of decorators.
Instructions:
-
Create a file called
decorators_practice.py -
Write a program that:
- Demonstrates functions as first-class objects
- Creates higher-order functions
- Implements basic decorators
- Shows decorator patterns
- Demonstrates practical decorator usage
-
Your program should include:
- Function assignment and passing
- Higher-order functions
- Basic decorator implementation
- Decorator with @ syntax
- Preserving function metadata
- Common decorator patterns
Example Solution:
"""
Understanding Decorators Practice
This program demonstrates functions as first-class objects,
higher-order functions, and basic decorators.
"""
from functools import wraps
import time
print("=" * 60)
print("UNDERSTANDING DECORATORS PRACTICE")
print("=" * 60)
print()
# 1. Functions as first-class objects
print("1. FUNCTIONS AS FIRST-CLASS OBJECTS")
print("-" * 60)
def greet(name):
"""Greet someone."""
return f"Hello, {name}!"
# Assign function to variable
say_hello = greet
print(f"Using variable: {say_hello('Alice')}")
# Function attributes
print(f"Function name: {greet.__name__}")
print(f"Function doc: {greet.__doc__}")
print()
# 2. Functions as arguments
print("2. FUNCTIONS AS ARGUMENTS")
print("-" * 60)
def apply_operation(x, operation):
"""Apply operation to x."""
return operation(x)
def square(n):
return n ** 2
def cube(n):
return n ** 3
result1 = apply_operation(5, square)
result2 = apply_operation(5, cube)
print(f"Square of 5: {result1}")
print(f"Cube of 5: {result2}")
print()
# 3. Functions as return values
print("3. FUNCTIONS AS RETURN VALUES")
print("-" * 60)
def make_multiplier(n):
"""Create a function that multiplies by n."""
def multiplier(x):
return x * n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(f"Double of 5: {double(5)}")
print(f"Triple of 5: {triple(5)}")
print()
# 4. Functions in data structures
print("4. FUNCTIONS IN DATA STRUCTURES")
print("-" * 60)
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
operations = [add, subtract, multiply]
print("Operations on (10, 5):")
for op in operations:
print(f" {op.__name__}: {op(10, 5)}")
print()
# 5. Higher-order function: apply_twice
print("5. HIGHER-ORDER FUNCTION: apply_twice")
print("-" * 60)
def apply_twice(func, value):
"""Apply function twice to value."""
return func(func(value))
def add_one(x):
return x + 1
result = apply_twice(add_one, 5)
print(f"apply_twice(add_one, 5): {result}")
print()
# 6. Closure example
print("6. CLOSURE EXAMPLE")
print("-" * 60)
def create_counter():
"""Create a counter function with closure."""
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
counter1 = create_counter()
counter2 = create_counter()
print(f"Counter1: {counter1()}, {counter1()}, {counter1()}")
print(f"Counter2: {counter2()}, {counter2()}")
print(f"Counter1 again: {counter1()}")
print()
# 7. Basic decorator (manual application)
print("7. BASIC DECORATOR (MANUAL)")
print("-" * 60)
def my_decorator(func):
"""Simple decorator that prints before and after."""
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
def say_hello():
print("Hello!")
decorated_hello = my_decorator(say_hello)
decorated_hello()
print()
# 8. Decorator with @ syntax
print("8. DECORATOR WITH @ SYNTAX")
print("-" * 60)
def my_decorator(func):
"""Simple decorator."""
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
print()
# 9. Decorator with arguments
print("9. DECORATOR WITH ARGUMENTS")
print("-" * 60)
def my_decorator(func):
"""Decorator that handles function arguments."""
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
@my_decorator
def add(x, y):
return x + y
result = add(3, 4)
print()
# 10. Preserving metadata with @wraps
print("10. PRESERVING METADATA WITH @wraps")
print("-" * 60)
from functools import wraps
def my_decorator(func):
"""Decorator using functools.wraps."""
@wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper function."""
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""Greet someone."""
return f"Hello, {name}!"
print(f"Function name: {greet.__name__}")
print(f"Function doc: {greet.__doc__}")
print()
# 11. Logging decorator
print("11. LOGGING DECORATOR")
print("-" * 60)
def log_calls(func):
"""Log function calls."""
@wraps(func)
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
@log_calls
def multiply(x, y):
return x * y
result = multiply(3, 4)
print()
# 12. Timing decorator
print("12. TIMING DECORATOR")
print("-" * 60)
def timer(func):
"""Measure function execution time."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(0.1)
return "Done"
result = slow_function()
print()
# 13. Validation decorator
print("13. VALIDATION DECORATOR")
print("-" * 60)
def validate_positive(func):
"""Validate that arguments are positive."""
@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError(f"Argument {arg} must be positive")
return func(*args, **kwargs)
return wrapper
@validate_positive
def square_root(x):
return x ** 0.5
result = square_root(16)
print(f"Square root of 16: {result}")
print()
# 14. Function call counter
print("14. FUNCTION CALL COUNTER")
print("-" * 60)
def count_calls(func):
"""Count how many times function is called."""
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.call_count += 1
print(f"{func.__name__} called {wrapper.call_count} times")
return func(*args, **kwargs)
wrapper.call_count = 0
return wrapper
@count_calls
def greet(name):
return f"Hello, {name}!"
greet("Alice")
greet("Bob")
print()
# 15. Cache decorator
print("15. CACHE DECORATOR")
print("-" * 60)
def cache(func):
"""Cache function results."""
cache_dict = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in cache_dict:
print(f"Computing {func.__name__} for {args}")
cache_dict[key] = func(*args, **kwargs)
else:
print(f"Using cached result for {func.__name__} with {args}")
return cache_dict[key]
return wrapper
@cache
def expensive_function(n):
return n ** 2
print(f"First call: {expensive_function(5)}")
print(f"Second call: {expensive_function(5)}")
print()
# 16. Stacking decorators
print("16. STACKING DECORATORS")
print("-" * 60)
def bold(func):
@wraps(func)
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@bold
@italic
def hello():
return "Hello"
print(f"Stacked decorators: {hello()}")
print()
# 17. Decorator execution order
print("17. DECORATOR EXECUTION ORDER")
print("-" * 60)
def decorator1(func):
print("Decorator 1 applied")
@wraps(func)
def wrapper():
print("Wrapper 1")
return func()
return wrapper
def decorator2(func):
print("Decorator 2 applied")
@wraps(func)
def wrapper():
print("Wrapper 2")
return func()
return wrapper
@decorator1
@decorator2
def my_function():
print("Function executed")
print("Calling decorated function:")
my_function()
print()
print("=" * 60)
print("PRACTICE COMPLETE!")
print("=" * 60)
Expected Output (truncated):
============================================================
UNDERSTANDING DECORATORS PRACTICE
============================================================
1. FUNCTIONS AS FIRST-CLASS OBJECTS
------------------------------------------------------------
Using variable: Hello, Alice!
Function name: greet
Function doc: Greet someone.
[... rest of output ...]
Challenge (Optional):
- Create a decorator that measures memory usage
- Build a decorator that validates function return types
- Implement a decorator that handles exceptions
- Create a decorator that adds authentication
Key Takeaways
- Functions are first-class objects - can be assigned, passed, returned
- Higher-order functions - take or return functions
- Decorators - functions that modify other functions
- @ syntax - shorthand for applying decorators
- @wraps - preserves function metadata
- *args, kwargs** - handle all arguments in decorators
- Decorators execute at function definition time
- Stacking decorators - apply multiple decorators
- Common patterns - logging, timing, validation, caching
- Closures - inner functions can access outer scope
- Function metadata - name, doc, etc.
- Decorator pattern - powerful for cross-cutting concerns
- Separation of concerns - keep logic separate
- Code reuse - apply same behavior to multiple functions
- Pythonic - idiomatic Python pattern
Quiz: Decorator Basics
Test your understanding with these questions:
-
What are first-class objects in Python?
- A) Only classes
- B) Objects that can be assigned, passed, returned
- C) Only functions
- D) Only variables
-
What is a higher-order function?
- A) Function that takes other functions as arguments
- B) Function that returns functions
- C) Both A and B
- D) Function with high complexity
-
What does the @ symbol do in Python?
- A) Creates a new function
- B) Applies a decorator
- C) Imports a module
- D) Defines a class
-
What does
@wrapsdo?- A) Wraps a function
- B) Preserves function metadata
- C) Creates a wrapper
- D) Removes metadata
-
When are decorators executed?
- A) When function is called
- B) When function is defined
- C) When module is imported
- D) Never
-
What should decorator wrappers use to handle arguments?
- A)
*args - B)
**kwargs - C) Both A and B
- D) No special handling needed
- A)
-
What is a closure?
- A) A function that closes
- B) Inner function accessing outer scope
- C) A decorator
- D) A class method
-
Can you stack multiple decorators?
- A) No
- B) Yes, but only two
- C) Yes, any number
- D) Only with special syntax
-
What is the purpose of decorators?
- A) Modify function behavior
- B) Add functionality without changing code
- C) Code reuse
- D) All of the above
-
What module provides
@wraps?- A)
decorators - B)
functools - C)
wraps - D)
functions
- A)
Answers:
- B) Objects that can be assigned, passed, returned (first-class objects definition)
- C) Both A and B (higher-order function definition)
- B) Applies a decorator (@ syntax purpose)
- B) Preserves function metadata (@wraps purpose)
- B) When function is defined (decorator execution timing)
- C) Both A and B (handle all arguments)
- B) Inner function accessing outer scope (closure definition)
- C) Yes, any number (stacking decorators)
- D) All of the above (decorator purposes)
- B)
functools(module with @wraps)
Next Steps
Excellent work! You've mastered the basics of decorators. You now understand:
- Functions as first-class objects
- Higher-order functions
- Basic decorator concepts
- How decorators work
What's Next?
- Lesson 13.2: Creating Decorators
- Learn to create your own decorators
- Understand decorators with arguments
- Explore advanced decorator patterns
Additional Resources
- Decorators: docs.python.org/3/glossary.html#term-decorator
- functools.wraps: docs.python.org/3/library/functools.html#functools.wraps
- First-Class Functions: docs.python.org/3/tutorial/controlflow.html#defining-functions
Lesson completed! You're ready to move on to the next lesson.
Course Navigation
- Understanding Decorators
- Creating Decorators
- Class Decorators
- Advanced Decorator Patterns