Yesterday's @logged took no arguments. What if you want @repeat(times=3) — call the wrapped function three times?
def repeat(times):
def decorator(fn):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = fn(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def bump(counter):
counter["n"] = counter["n"] + 1
return counter["n"]
state = {"n": 0}
print(bump(state)) # 3 — bumped three timesThree nested functions. That's a lot.
Each layer has a job. Outermost: repeat(times) accepts the decorator argument. Middle: decorator(fn) accepts the function being decorated. Innermost: wrapper(*args, **kwargs) is the new callable that replaces fn. Three layers because there are three steps: receive the decorator arg, receive the function, do the work.
And @repeat(times=3) — is repeat(times=3) itself a decorator?
Yes. repeat(times=3) evaluates to decorator — a function that takes one function and returns one. That's exactly the contract @ needs. The pattern is sometimes called a decorator factory — repeat is a factory that, given arguments, makes a decorator.
Is there a simpler version when I want 0 args?
Yesterday's @logged is the 0-arg version. Two layers (logged(fn) → wrapper). Today's is three. If you ever need to support both @logged and @logged(prefix="DEBUG"), there's a clever trick — but most codebases just pick one form and stick with it.
def repeat(times): # outer: accepts decorator arg
def decorator(fn): # middle: accepts the function
def wrapper(*args, **kwargs): # inner: replaces the function
result = None
for _ in range(times):
result = fn(*args, **kwargs)
return result
return wrapper
return decorator@repeat(times=3) is two evaluations@repeat(times=3)
def bump(counter):
...
# Equivalent to:
def bump(counter):
...
bump = repeat(times=3)(bump)repeat(times=3) runs first — returns decoratordecorator(bump) runs next — returns wrapperbump = wrapperWhen you later call bump(state), you're calling wrapper(state) — which loops times times, calls the original fn each time, and returns the last result.
repeat) — exists so @repeat(times=3) is callable. The () after repeat triggers this layer.decorator) — the actual decorator. Takes the function being decorated.wrapper) — replaces the original function. Runs the wrapped behaviour.Fewer than three layers and you can't do both "take an argument" and "wrap a function."
@retry(times=3)
def fetch(url):
...
@cached(maxsize=128)
def expensive(x):
...
@requires_role("admin")
def delete_user(user_id):
...Each one is a decorator factory that takes a configuration argument and returns a decorator.
If the decorator never takes arguments, two layers is enough — yesterday's @logged. If you'd be tempted to write @logged() (with empty parens), promote to a factory; otherwise keep it two-layer.
()@repeat # WRONG — repeat is the outer factory, not a decorator
def bump(counter):
...This assigns bump = repeat(bump) — but repeat expects times, not a function. You'd get TypeError later when calling bump(...) because it'd be trying to use fn as a count. Always include the () for parametrised decorators.
Yesterday's @logged took no arguments. What if you want @repeat(times=3) — call the wrapped function three times?
def repeat(times):
def decorator(fn):
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = fn(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def bump(counter):
counter["n"] = counter["n"] + 1
return counter["n"]
state = {"n": 0}
print(bump(state)) # 3 — bumped three timesThree nested functions. That's a lot.
Each layer has a job. Outermost: repeat(times) accepts the decorator argument. Middle: decorator(fn) accepts the function being decorated. Innermost: wrapper(*args, **kwargs) is the new callable that replaces fn. Three layers because there are three steps: receive the decorator arg, receive the function, do the work.
And @repeat(times=3) — is repeat(times=3) itself a decorator?
Yes. repeat(times=3) evaluates to decorator — a function that takes one function and returns one. That's exactly the contract @ needs. The pattern is sometimes called a decorator factory — repeat is a factory that, given arguments, makes a decorator.
Is there a simpler version when I want 0 args?
Yesterday's @logged is the 0-arg version. Two layers (logged(fn) → wrapper). Today's is three. If you ever need to support both @logged and @logged(prefix="DEBUG"), there's a clever trick — but most codebases just pick one form and stick with it.
def repeat(times): # outer: accepts decorator arg
def decorator(fn): # middle: accepts the function
def wrapper(*args, **kwargs): # inner: replaces the function
result = None
for _ in range(times):
result = fn(*args, **kwargs)
return result
return wrapper
return decorator@repeat(times=3) is two evaluations@repeat(times=3)
def bump(counter):
...
# Equivalent to:
def bump(counter):
...
bump = repeat(times=3)(bump)repeat(times=3) runs first — returns decoratordecorator(bump) runs next — returns wrapperbump = wrapperWhen you later call bump(state), you're calling wrapper(state) — which loops times times, calls the original fn each time, and returns the last result.
repeat) — exists so @repeat(times=3) is callable. The () after repeat triggers this layer.decorator) — the actual decorator. Takes the function being decorated.wrapper) — replaces the original function. Runs the wrapped behaviour.Fewer than three layers and you can't do both "take an argument" and "wrap a function."
@retry(times=3)
def fetch(url):
...
@cached(maxsize=128)
def expensive(x):
...
@requires_role("admin")
def delete_user(user_id):
...Each one is a decorator factory that takes a configuration argument and returns a decorator.
If the decorator never takes arguments, two layers is enough — yesterday's @logged. If you'd be tempted to write @logged() (with empty parens), promote to a factory; otherwise keep it two-layer.
()@repeat # WRONG — repeat is the outer factory, not a decorator
def bump(counter):
...This assigns bump = repeat(bump) — but repeat expects times, not a function. You'd get TypeError later when calling bump(...) because it'd be trying to use fn as a count. Always include the () for parametrised decorators.
Create a free account to get started. Paid plans unlock all tracks.