# Decorators — Part 2: Arguments, Classes, and Internals

## 1. Decorators That Take Arguments — Three Layers of Functions

```python
import functools

def repeat(times):              # layer 1: takes the decorator's OWN arguments
    def decorator(func):         # layer 2: takes the function being decorated
        @functools.wraps(func)
        def wrapper(*args, **kwargs):   # layer 3: the actual call-time wrapper
            results = []
            for _ in range(times):
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator

@repeat(times=3)
def roll():
    import random
    return random.randint(1, 6)

print(roll())  # e.g. [4, 1, 6]
```

`@repeat(times=3)` first calls `repeat(times=3)`, which returns `decorator` — **that** is what actually gets applied to `roll` (`decorator(roll)`), exactly like an argument-less decorator from Part 1. A parameterized decorator is a function that *returns* a decorator, which is why it needs one extra layer of nesting compared to a plain decorator.

## 2. Class-Based Decorators

```python
import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)   # class equivalent of @wraps
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.func(*args, **kwargs)

@CountCalls
def greet():
    return "hi"

greet(); greet()
print(greet.count)  # 2
```

Any object implementing `__call__` can decorate a function — `@CountCalls` here means `greet = CountCalls(greet)`, producing an *instance* of `CountCalls` that behaves like a callable, with the added benefit of easily holding extra state (`self.count`) that a closure-based decorator would need `nonlocal` for.

## 3. Decorating Classes Themselves

```python
def add_repr(cls):
    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{cls.__name__}({fields})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

print(Point(1, 2))  # Point(x=1, y=2)
```

A decorator can be applied to a `class` statement exactly like a function — it receives the class object itself and can return it modified, or return an entirely different object. `dataclasses.dataclass` is the most common real-world class decorator in the standard library, generating `__init__`/`__repr__`/`__eq__` automatically.

## 4. `functools.lru_cache` — A Decorator Built on These Ideas

```python
import functools

@functools.lru_cache(maxsize=128)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

fib(30)  # fast — memoized, exponential recursion collapses to linear work
print(fib.cache_info())  # CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)
```

`lru_cache` is itself a parameterized decorator (section 1's pattern) implemented in C for speed in CPython, storing a dict keyed by the call's arguments (which must therefore be hashable). It demonstrates why the mutable-default-argument-style closures matter: the cache dict is created once, when `lru_cache(maxsize=128)` produces the actual decorator, and persists across every call to `fib` for the lifetime of that function object.

## 5. Under the Hood: What `@` Actually Compiles To

### The `@` syntax is evaluated top-to-bottom, then applied bottom-to-top, at `def`/`class` compile time

```python
@bold
@italic
def text():
    return "hi"
```

compiles, roughly, to bytecode equivalent to:

```python
def text():
    return "hi"
text = italic(text)   # closest decorator to the def applies FIRST
text = bold(text)      # then the next one up wraps that result
```

Both decorator expressions (`bold`, `italic`) are evaluated (looked up as names/attributes/calls) *before* either is applied, in top-to-bottom order — but the actual *application* (`func = decorator(func)`) happens bottom-to-top, working outward from the function definition. This is exactly why call order (section 4 of Part 1) ends up outside-in: the outermost decorator (`bold`) wraps a `text` that is already fully wrapped by `italic`.

![@bold then @italic: decorator expressions evaluated top-down, but application happens bottom-up — text = bold(italic(text))](diagrams/01-decorator-stack-order.svg)

### There is no separate "decorator" concept at the bytecode/object level

Mechanically, a decorated function is completely ordinary once decoration finishes — there is no runtime flag, marker, or special object type indicating "this function was decorated." `greet` after `@shout` is just whatever `shout(greet)` returned; if that's a closure named `wrapper`, then `greet` **is** `wrapper` from that point on, full stop. Anything inspecting `greet.__code__` after decoration sees `wrapper`'s code object, not the original function's — which is precisely the introspection problem `functools.wraps` patches over (it copies `__name__`/`__doc__`/`__dict__`/`__wrapped__` from the original onto the wrapper, and sets `__wrapped__` so tools *can* still find the original if they know to look for it).

```python
import functools

def shout(func):
    @functools.wraps(func)
    def wrapper(*a, **kw):
        return func(*a, **kw).upper()
    return wrapper

@shout
def greet(name):
    return f"hi {name}"

print(greet.__wrapped__)   # the ORIGINAL undecorated greet function object
print(greet.__wrapped__("ana"))  # "hi ana" — bypasses the decorator entirely
```

### Parameterized decorators cost one extra closure layer, every time the module loads

`@repeat(times=3)` means `repeat(times=3)` actually executes at module-import time (or whenever the enclosing `def`/`class` is defined), producing a fresh `decorator` closure over `times=3` — this closure allocation happens once per decorated definition, not once per call, using the same cell-object mechanism as any other closure (see the Variables and Scope topic's "Under the Hood" section).

## 6. Common Pitfalls

1. **Forgetting the third layer for parameterized decorators** — `@repeat(times=3)` requires `repeat` to return a decorator, which itself returns a wrapper; skipping a layer produces a `TypeError` about calling a function with the wrong number of arguments.
2. **Applying `@functools.lru_cache` to a method with mutable/unhashable arguments** — the cache key is built from the arguments; passing a `list` or `dict` argument raises `TypeError: unhashable type`.
3. **Decorating instance methods without accounting for `self`** — a decorator's wrapper needs to pass `self` through via `*args, **kwargs` like any other positional argument; forgetting this breaks methods specifically (functions work fine, methods don't automatically special-case anything).
4. **Assuming `@classmethod`/`@staticmethod` compose freely with custom decorators in any order** — ordering matters; a custom decorator generally needs to be closer to the function (applied first / listed last, right above `def`) than `@classmethod`/`@staticmethod`, or it will receive the descriptor object instead of a plain callable.

## 7. Quick Reference

```python
def dec(func):
    @functools.wraps(func)
    def wrapper(*a, **kw): return func(*a, **kw)
    return wrapper                            # plain decorator

def param_dec(opt):
    def dec(func):
        @functools.wraps(func)
        def wrapper(*a, **kw): return func(*a, **kw)
        return wrapper
    return dec                                 # parameterized decorator: 3 layers

@bold
@italic
def f(): ...                                    # applied bottom-up: bold(italic(f))

f.__wrapped__                                    # original function, if @wraps was used
functools.lru_cache(maxsize=None)                 # built-in memoization decorator
```
