Quest 18: Function Decorators - Enchanting Your Spells

Add Superpowers to Any Function Without Rewriting It

โœจ QUEST 18 | Difficulty: Intermediate | Time: 7 minutes

๐Ÿ“Š Complexity Level: Advanced โญโญโญ

Advanced topic that requires solid understanding of programming fundamentals. Recommended for students who have completed most beginner and intermediate quests, or those with prior programming experience. Not recommended for college fair demos.

๐Ÿ’ป Interactive Options:

  • ๐Ÿ““ Open in JupyterLite - Full Jupyter environment in your browser
  • โ–ถ๏ธ Run code directly below - All code cells on this page are editable and runnable
  • ๐Ÿ“ฅ Download Notebook - For use in local Jupyter or Google Colab

๐Ÿ“– Introduction: Enchanting Your Spells

Imagine you have a perfectly good fireball spell. It works great! But now your wizard guild wants you to log every spell cast, time how long each spell takes, and check that the caster has enough mana โ€” all without changing any of the spells themselves.

Thatโ€™s exactly what decorators do in Python! A decorator is a special function that wraps around another function, adding extra behaviour before and/or after it runs โ€” all without touching the original code.

โš—๏ธ Story Time: Think of a decorator like an enchanted ring. You put the ring on your finger (your function), and suddenly you shoot fire from your fingertips whenever you cast any spell. The ring added a superpower โ€” but your original spell didnโ€™t change at all!

๐Ÿ’ก Explanation: Building a Decorator Step by Step

Before we use the fancy @ symbol, letโ€™s understand whatโ€™s happening underneath. A decorator is just a function that takes a function and returns a new (wrapped) function.

def my_decorator(func):
    def wrapper():
        print("Before the function runs!")
        func()
        print("After the function runs!")
    return wrapper

def say_hi():
    print("Hi!")

# Wrap say_hi with my_decorator
say_hi = my_decorator(say_hi)
say_hi()

๐ŸŽฏ What Just Happened?

  1. We defined my_decorator โ€” it accepts a function and returns a wrapped version
  2. We wrapped say_hi by writing say_hi = my_decorator(say_hi)
  3. Now calling say_hi() runs the wrapper, which adds extra behaviour around the original

Python has a shorthand for step 2 โ€” the @ symbol:

@my_decorator       # โ† This is exactly the same as: say_hi = my_decorator(say_hi)
def say_hi():
    print("Hi!")

The @ syntax is just syntactic sugar โ€” a sweeter way to write the same thing!

๐ŸŽฎ Activity 1: Your First Decorator

Letโ€™s write a decorator that announces when a spell is cast:

See how all three functions get the same announcement behaviour โ€” even though you only wrote that logic once in announce?

๐ŸŽฎ Activity 2: Decorators with Arguments (Using *args and **kwargs)

Real functions take arguments! Letโ€™s make our decorator pass arguments through properly:

๐Ÿ’ก What is *args and **kwargs?

  • *args catches any number of positional arguments as a tuple: func(1, 2, 3)
  • **kwargs catches any keyword arguments as a dict: func(name="Ali", level=5)
  • Using both in your wrapper means it works with any function, no matter what arguments it takes!

๐ŸŽฎ Activity 3: A Timer Decorator

One of the most common real-world uses of decorators is measuring how long a function takes to run:

๐ŸŽฎ Activity 4: Stacking Decorators

You can stack multiple decorators on a single function! They apply from bottom to top (the decorator closest to def runs first):

๐ŸŽฎ Activity 5: A Practical Decorator โ€” Mana Check

Letโ€™s build something that feels like a real game feature โ€” a decorator that checks whether the caster has enough mana before running a spell:

๐Ÿงฉ Challenge: Write a Logger Decorator

Write a decorator called log_call that prints the function name and its arguments every time itโ€™s called.

โœ… Challenge Solution

๐ŸŽ“ Summary

Decorator Pattern When to Use
Simple @decorator Add behaviour to any no-arg function
wrapper(*args, **kwargs) Wrap any function, regardless of arguments
Decorator factory decorator(arg) Customise decorator behaviour with a parameter
Stacking @d1 @d2 @d3 Apply multiple behaviours in layers

๐ŸŒ Decorators in the Real World

Decorators are used everywhere in Python: - Web frameworks (like Flask): @app.route("/home") marks a function as a web page - Testing: @pytest.mark.skip skips a test function - Caching: @functools.lru_cache stores results to avoid re-computing - Class methods: @staticmethod and @classmethod change how methods behave

Youโ€™ve just learned one of Pythonโ€™s most elegant features โ€” the decorator. Now those @ symbols you see in real code will feel like old friends! ๐ŸŽ‰