Decorators

Python Intermediate — Decorators
Python Intermediate
Course 2 · Chapter 6 · Decorators

🎀 Decorators

A decorator wraps a function to add behavior around it — logging, timing, caching, access control — without changing the function's own code. This chapter covers how decorators actually work under the hood, functools.wraps (a fix for a subtle side effect), @property for attribute-like method access, and functools.lru_cache for automatic memoization.

🔧 Functions Are Objects

Decorators only make sense once you know functions in Python are ordinary objects — they can be assigned to variables, passed as arguments, and returned from other functions:

def say_hello(): return "Hello!" greeting = say_hello # no parentheses — assigning the FUNCTION itself, not calling it print(greeting()) # Hello! — greeting is just another name for the same function def run_twice(func): # a function that takes ANOTHER function as an argument func() func() run_twice(say_hello) # calls say_hello() twice

🎁 Writing a Basic Decorator

def shout(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result.upper() return wrapper @shout def greet(name): return f"hello, {name}" print(greet("Ada")) # HELLO, ADA

@shout above def greet is syntax sugar for greet = shout(greet)greet now actually refers to wrapper, which calls the original greet logic internally (as func) and transforms its result before returning it.

⚠ Always Use *args, **kwargs in the Wrapper

If wrapper is defined as def wrapper(): with no parameters, decorating greet(name) breaks entirely — calling greet("Ada") actually calls wrapper("Ada"), which doesn't accept any arguments. Using *args, **kwargs (Course 1, Chapter 8) lets the wrapper forward whatever arguments it received straight through to func, so the decorator works on any function regardless of its parameter list.

🏷️ functools.wraps

print(greet.__name__) # wrapper — NOT "greet"! Metadata got lost. from functools import wraps def shout(func): @wraps(func) # preserves func's __name__, docstring, etc. def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result.upper() return wrapper @shout def greet(name): return f"hello, {name}" print(greet.__name__) # greet — fixed!

Without @wraps(func), the decorated function's identity (its __name__, docstring, and other metadata) silently becomes wrapper's instead of the original function's — confusing for debugging, introspection, and tools that rely on that metadata. @wraps(func) from functools is standard practice in every real decorator.

📐 @property

class Circle: def __init__(self, radius): self._radius = radius @property def area(self): return 3.14159 * self._radius ** 2 c = Circle(4) print(c.area) # 50.26544 — accessed like an ATTRIBUTE, no parentheses, but computed live

@property makes a method accessible as if it were a plain attribute — c.area, not c.area() — while still running real code every time it's read. This is useful for a value that's always derived from other attributes (like area from radius) rather than stored directly.

💾 functools.lru_cache

from functools import lru_cache @lru_cache def fibonacci(n): if n <= 1: return n return fibonacci(n - 1) + fibonacci(n - 2) print(fibonacci(30)) # fast — each fibonacci(n) is only ever actually computed once

@lru_cache automatically remembers previous return values for previous arguments, so calling fibonacci(30) a second time (or any recursive call the first computation already made) returns the cached result instantly instead of recomputing it. This turns the naive recursive Fibonacci implementation — normally exponentially slow — into something fast, with a single decorator line and no change to the function's actual logic.

Wrapping Behavior: Go vs Kotlin vs Python

LanguageApproach
GoNo decorator syntax at all — the same effect is achieved manually with higher-order functions (a function that takes and returns a func) or the "middleware" pattern common in HTTP handlers.
KotlinNo direct decorator syntax either — achieved with higher-order functions and lambdas, or with annotations processed at compile/build time for a different (metadata-driven) purpose.
PythonFirst-class @decorator syntax, built directly into the language.

Python's @decorator syntax is a genuine convenience neither Go nor Kotlin has natively — both can express the same underlying higher-order-function idea, just without the dedicated syntax sugar.

@decorator

Sugar for func = decorator(func) — wraps a function with extra behavior.

*args, **kwargs

Required in the wrapper so it can forward any function's arguments through.

@property

Makes a method readable like a plain attribute, computed live.

@lru_cache

Automatic memoization — remembers results for previously-seen arguments.

💻 Coding Challenges

Challenge 1: A Timing Decorator

Write a decorator @timer that prints how long the decorated function took to run (use the time module's time.time() before and after calling the function), then returns the function's original result unchanged. Apply it to any simple function.

Goal: Practice writing a decorator that measures something without altering the wrapped function's return value.

→ Solution

Challenge 2: Rectangle with @property

Write a Rectangle class with width and height instance attributes, plus an @property method area that returns width * height. Create an instance and print its .area.

Goal: Practice defining and using a computed @property.

→ Solution

Challenge 3: Cached Fibonacci, Verified

Using @lru_cache, write the recursive fibonacci(n) function from this chapter, then call fibonacci(35) and print the result — something the same function without @lru_cache would take noticeably longer to compute.

Goal: See @lru_cache's real performance impact firsthand, not just read about it.

→ Solution

🎯 What's Next

Next chapter: Working with Data — JSON, CSV, and the datetime module.