مرکزی مواد پر جائیں

Custom Decorators

James wants every Note method call logged during debugging. He starts adding print statements:

class Note:
def add_tag(self, tag: str) -> None:
print(f"Calling add_tag with {tag}") # ← debugging
if tag not in self.tags:
self.tags.append(tag)

def remove_tag(self, tag: str) -> None:
print(f"Calling remove_tag with {tag}") # ← debugging
if tag not in self.tags:
raise ValueError(f"Tag '{tag}' not found")
self.tags.remove(tag)

Two methods, two identical print lines. If he has ten methods, he writes ten identical lines. When debugging is done, he deletes ten lines. When debugging starts again, he adds them back.

"That is cross-cutting logic," Emma says. "The logging behavior has nothing to do with tags. It applies to every method the same way. Write it once, apply it everywhere. That is a decorator."


If you're new to programming

A decorator is a function that takes another function as input and returns a new function that adds behavior. The @decorator syntax is a shortcut: instead of calling add_tag = log_calls(add_tag) manually, you write @log_calls above the function definition.

If you've coded before

You have already used decorators: @dataclass transforms a class, @property transforms a method into a descriptor, @pytest.fixture registers a function as a test fixture. Writing a custom decorator means creating a function that accepts a callable, wraps it in a new callable, and returns the wrapper. functools.wraps copies __name__, __doc__, and other attributes from the original to the wrapper.


Decorators You Already Know

You have used decorators throughout this book without writing your own:

DecoratorChapterWhat It Does
@dataclass51Auto-generates __init__, __repr__, __eq__ for a class
@pytest.fixture52Registers a function as a test fixture
@property61 L1Turns a method into a computed attribute
@staticmethod61 L2Makes a method callable without self or cls
@classmethod61 L2Passes cls as the first argument

Every one of these follows the same pattern: take a function (or class), modify its behavior, return the result. Now you will write your own.


Writing @log_calls

A decorator is a function that wraps another function:

import functools

def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Finished {func.__name__}")
return result
return wrapper

Apply it:

@log_calls
def greet(name: str) -> str:
return f"Hello, {name}"

print(greet("James"))

Output:

Calling greet
Finished greet
Hello, James

What Just Happened

The @log_calls line is equivalent to writing:

def greet(name: str) -> str:
return f"Hello, {name}"

greet = log_calls(greet) # ← this is what @log_calls does
  1. Python passes greet to log_calls
  2. log_calls creates wrapper, which calls the original greet inside
  3. log_calls returns wrapper
  4. greet now points to wrapper
  5. Calling greet("James") runs wrapper("James"), which prints, calls the original, prints again, and returns the result

James's analogy: a quality inspection stamp. Every product coming off the line gets the same stamp process, regardless of what the product is. The stamp (decorator) is applied once to the production line (function definition), and every unit (function call) gets stamped automatically.


Why functools.wraps Matters

Without @functools.wraps(func), the wrapper replaces the original function's identity:

def log_calls_bad(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@log_calls_bad
def greet(name: str) -> str:
"""Say hello."""
return f"Hello, {name}"

print(greet.__name__)
print(greet.__doc__)

Output:

wrapper
None

The function's name and docstring are gone. functools.wraps copies them from the original:

@log_calls  # uses @functools.wraps(func)
def greet(name: str) -> str:
"""Say hello."""
return f"Hello, {name}"

print(greet.__name__)
print(greet.__doc__)

Output:

greet
Say hello.

Rule: always use @functools.wraps(func) in your wrapper. It costs nothing and prevents debugging confusion.


PRIMM-AI+ Practice: Decorator Mechanism

Predict [AI-FREE]

Press Shift+Tab to enter Plan Mode before predicting.

Read this code and predict the exact output. Write your prediction and a confidence score from 1 to 5.

import functools

def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Finished {func.__name__}")
return result
return wrapper

@log_calls
def add(a: int, b: int) -> int:
return a + b

result = add(2, 3)
print(result)
print(add.__name__)
Check your prediction
Calling add
Finished add
5
add

log_calls wraps add inside wrapper. Calling add(2, 3) runs wrapper(2, 3), which prints "Calling add", calls the original add (returning 5), prints "Finished add", and returns 5. Because @functools.wraps(func) copies metadata, add.__name__ still shows "add" instead of "wrapper".

Run

Press Shift+Tab to exit Plan Mode.

Create a file called decorator_mechanism.py. Write the log_calls decorator and add function from the prediction exercise. Run it with uv run python decorator_mechanism.py and compare output to your prediction.

Investigate

Write first: Why does add.__name__ still show "add" and not "wrapper"? Write your answer in one sentence before checking.

Then remove @functools.wraps(func) from the decorator and predict what changes. Run it again to verify.

Modify

Add a second decorated function multiply(a, b) and verify that each function's calls are logged independently with the correct name.

Make [Mastery Gate]

Write a @time_it decorator that prints how long a function takes to execute (using time.time()). Apply it to a function that does some work (e.g., summing a range of numbers). Use functools.wraps to preserve metadata. Use /tdg in Claude Code to drive the cycle.


Try With AI

Opening Claude Code

If Claude Code is not already running, open your terminal, navigate to your SmartNotes project folder, and type claude. If you need a refresher, Chapter 44 covers the setup.

Prompt 1: Understand the Wrapping Mechanism

I know that @log_calls above a function is the same
as func = log_calls(func). Walk me through what happens
step by step when Python processes this code:

@log_calls
def add_tag(self, tag: str) -> None:
self.tags.append(tag)

What does log_calls receive? What does it return?
What does add_tag point to after the decoration?

Read the AI's explanation. Verify it matches what you learned: log_calls receives the original add_tag, creates wrapper, and returns wrapper. After decoration, add_tag points to wrapper.

What you're learning: You are building the mechanical understanding of decoration. The @ syntax is convenience; the real operation is function replacement through assignment.

Prompt 2: Step Through Without functools.wraps

Show me what happens when I write a decorator WITHOUT
@functools.wraps. Create a decorated function, then
print its __name__ and __doc__. Then add @functools.wraps
and show the difference.

What you're learning: You are seeing firsthand why functools.wraps matters. Without it, decorated functions lose their identity. Debugging becomes harder because stack traces and help() show "wrapper" instead of the real function name.

Prompt 3: Decorator for Your Domain

I work in [describe your professional domain]. I have
functions that need [describe a repeated behavior:
audit logging, input validation, retry on failure,
permission checking, etc.].

Write a decorator that handles this cross-cutting concern.
Show it applied to a realistic function from my domain.

What you're learning: You are transferring the decorator pattern to your own context. The AI generates the decorator, but you evaluate whether it correctly wraps the function, uses functools.wraps, and handles the cross-cutting concern appropriately.


James stares at the five-step breakdown. "So @log_calls just means greet = log_calls(greet). The original function is still there, inside the wrapper."

"Exactly," Emma says. "And functools.wraps makes sure nobody forgets that. The wrapper carries the original's name tag."

"Now I want to write one that does something useful for SmartNotes. Like validating that nobody passes an empty string to add_tag."

"That is the next lesson."