🎁 Python Decorators: The Gift-Wrapping Magic
Imagine this: You have a beautiful gift. But before giving it, you wrap it in shiny paper, add a ribbon, and stick a bow on top. The gift inside stays the same, but now it looks amazing and has extra features!
That’s exactly what decorators do to functions in Python!
🧠 The Big Picture
A decorator is a special wrapper that adds superpowers to your functions—without changing what’s inside them.
graph TD A["Your Function"] --> B["Decorator Wraps It"] B --> C["New Enhanced Function"] C --> D["Same core + Extra powers!"]
1. 🎀 Decorator Basics
What’s Happening?
Think of a decorator like a gift-wrapping station:
- Your function is the gift
- The decorator is the wrapper
- The result? A prettier, more powerful gift!
How It Works
A decorator is just a function that takes another function and returns a new, enhanced version.
def my_decorator(func):
def wrapper():
print("Before the gift!")
func()
print("After the gift!")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Output:
Before the gift!
Hello!
After the gift!
🎯 Key Insight
The @my_decorator is just a shortcut for:
say_hello = my_decorator(say_hello)
It’s that simple! You’re replacing your function with a wrapped version.
2. 🎁 Decorators with Arguments
The Challenge
What if your function needs to receive information?
Like giving a birthday gift—you need to know whose birthday it is!
The Solution
Make your wrapper accept *args and **kwargs:
def gift_wrapper(func):
def wrapper(*args, **kwargs):
print("🎀 Wrapping your gift...")
result = func(*args, **kwargs)
print("🎁 Gift delivered!")
return result
return wrapper
@gift_wrapper
def greet(name, message="Happy day!"):
print(f"{name}: {message}")
return "Done!"
greet("Alice", message="Happy Birthday!")
Output:
🎀 Wrapping your gift...
Alice: Happy Birthday!
🎁 Gift delivered!
🌟 Pro Tip
Always use *args, **kwargs in your wrapper—it makes your decorator work with ANY function!
3. 📚 Stacking Decorators
Multiple Wrappers!
What if you want to add MORE decorations? Like wrapping paper, THEN a ribbon, THEN a bow?
You can stack decorators!
def add_ribbon(func):
def wrapper(*args, **kwargs):
print("🎀 Adding ribbon...")
return func(*args, **kwargs)
return wrapper
def add_bow(func):
def wrapper(*args, **kwargs):
print("🎀 Adding bow...")
return func(*args, **kwargs)
return wrapper
@add_bow
@add_ribbon
def give_gift():
print("🎁 Here's your gift!")
give_gift()
Output:
🎀 Adding bow...
🎀 Adding ribbon...
🎁 Here's your gift!
🔑 Order Matters!
graph TD A["@add_bow #40;outer#41;"] --> B["@add_ribbon #40;inner#41;"] B --> C["give_gift"]
Bottom decorator wraps first, top decorator wraps last!
Think of it like layers:
- First, wrap the gift in ribbon
- Then, put a bow on top of the ribbon
4. 🔧 Using functools.wraps
The Hidden Problem
When you wrap a function, you lose its identity!
def simple_decorator(func):
def wrapper():
return func()
return wrapper
@simple_decorator
def my_function():
"""I am a cool function!"""
pass
print(my_function.__name__) # 'wrapper' 😢
print(my_function.__doc__) # None 😢
Your function forgot its own name!
The Fix: functools.wraps
from functools import wraps
def smart_decorator(func):
@wraps(func)
def wrapper():
return func()
return wrapper
@smart_decorator
def my_function():
"""I am a cool function!"""
pass
print(my_function.__name__) # 'my_function' ✅
print(my_function.__doc__) # 'I am a cool...' ✅
🎯 Always Use @wraps!
It copies:
__name__(function name)__doc__(docstring)__module__(where it lives)- And more!
Rule of Thumb: Always put @wraps(func) on your wrapper function.
5. 🏛️ Class Decorators
Decorating Entire Classes!
Decorators aren’t just for functions—they can wrap classes too!
Example: Add a Greeting
def add_greeting(cls):
cls.greet = lambda self: f"Hi, I'm {self.name}!"
return cls
@add_greeting
class Person:
def __init__(self, name):
self.name = name
bob = Person("Bob")
print(bob.greet()) # "Hi, I'm Bob!"
The decorator added a new method to the class!
Example: Track All Instances
def count_instances(cls):
cls._count = 0
original_init = cls.__init__
def new_init(self, *args, **kwargs):
cls._count += 1
original_init(self, *args, **kwargs)
cls.__init__ = new_init
return cls
@count_instances
class Cat:
def __init__(self, name):
self.name = name
c1 = Cat("Whiskers")
c2 = Cat("Fluffy")
print(Cat._count) # 2
🎮 Putting It All Together
Here’s a real-world example combining everything:
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"📞 Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"✅ {func.__name__} returned!")
return result
return wrapper
def validate_positive(func):
@wraps(func)
def wrapper(x):
if x < 0:
raise ValueError("Must be positive!")
return func(x)
return wrapper
@log_calls
@validate_positive
def square(n):
"""Returns n squared."""
return n * n
print(square(5)) # 25
print(square.__name__) # 'square' ✅
✨ Summary
| Concept | What It Does |
|---|---|
| Decorator Basics | Wraps a function with extra behavior |
| With Arguments | Use *args, **kwargs to handle any inputs |
| Stacking | Apply multiple decorators (bottom-up order) |
| functools.wraps | Preserves the original function’s identity |
| Class Decorators | Enhance entire classes with new features |
🚀 You Did It!
Decorators are like magical gift-wrapping paper for your code. They add superpowers without changing what’s inside.
Now you can:
- ✅ Write basic decorators
- ✅ Handle functions with arguments
- ✅ Stack multiple decorators
- ✅ Preserve function metadata
- ✅ Decorate entire classes
Go wrap some functions! 🎁
