Wrap functions in superpowers without touching their code
Functions Are First-Class Citizens
In Python, functions are just objects — like any other value. You can store them in variables, pass them as arguments, and return them from other functions. This might feel weird at first, but it's the foundation for everything in this lesson.
Think of it this way: a function is like a recipe card. You can hand the card to someone else (pass it as an argument), photocopy it (assign to another variable), or even create new recipe cards inside a factory (return a function from a function).
Functions as Objects
# Functions can be assigned to variables
defshout(text):
return text.upper()+"!!!"
yell = shout # No parentheses — we're passing the function itself
A closure is a function that "remembers" variables from the place where it was created, even after that place no longer exists. It's like a snowglobe — the little world inside is frozen in time, but you can still look at it.
Closures happen when an inner function references a variable from the outer function's scope. The inner function "closes over" those variables and carries them along.
Closures in Action
# A function that creates customized functions
defmake_multiplier(factor):
defmultiply(x):
return x * factor # 'factor' is remembered!
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5))# 10
print(triple(5))# 15
print(double(100))# 200
# The closure remembers 'factor' even though make_multiplier finished!
print()
# Practical closure: a counter
defmake_counter(start=0):
count =[start]# Using a list so the inner function can modify it
defincrement():
count[0]+=1
return count[0]
return increment
page_views = make_counter()
print(f"Visit 1: {page_views()}")
print(f"Visit 2: {page_views()}")
print(f"Visit 3: {page_views()}")
Output
10
15
200
Visit 1: 1
Visit 2: 2
Visit 3: 3
Decorators — The Main Event
A decorator is a function that takes a function, adds some behavior to it, and returns a new enhanced function. It's like gift-wrapping a present — the present inside is unchanged, but the wrapping adds something extra (a bow, a tag, sparkles).
The @decorator syntax is just syntactic sugar. Writing @my_decorator above a function definition is the same as my_func = my_decorator(my_func).
Building a Decorator
import functools
import time
# A simple timer decorator
deftimer(func):
@functools.wraps(func)# Preserves the original function's name/docstring
defwrapper(*args,**kwargs):
start = time.perf_counter()
result = func(*args,**kwargs)# Call the original function
elapsed = time.perf_counter()- start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
defslow_greeting(name):
"""Greet someone slowly."""
time.sleep(0.1)# Simulate slow work
returnf"Hello, {name}!"
# Using the decorated function
result = slow_greeting("Alice")
print(result)
print(f"Function name: {slow_greeting.__name__}")# Thanks to @wraps
print(f"Docstring: {slow_greeting.__doc__}")
Output
slow_greeting took 0.1003s
Hello, Alice!
Function name: slow_greeting
Docstring: Greet someone slowly.
Real-World Decorator Patterns
Decorators are everywhere in professional Python code. Here are some of the most common patterns:
Logging — Track when functions are called and with what arguments
Authentication — Check if a user is allowed to call a function
Caching/Memoization — Remember results to avoid re-computing
Sometimes you want to configure a decorator — like telling it how many times to retry, or what message to log. For this, you need a decorator factory: a function that takes arguments and returns a decorator.
Parameterized Decorators
import functools
defrepeat(n):
"""Decorator factory: repeat a function n times."""
defdecorator(func):
@functools.wraps(func)
defwrapper(*args,**kwargs):
result =None
for _ inrange(n):
result = func(*args,**kwargs)
return result
return wrapper
return decorator
@repeat(3)# repeat(3) returns the actual decorator
Note: Always use @functools.wraps(func) in your decorator's wrapper function. Without it, the decorated function loses its original name and docstring, which makes debugging a nightmare. It's one line of code that saves hours of confusion.