Reputation: 91
I'm brand new to coding and I've been trying to absorb as much as possible. I don't understand a lot of the technical explanations you guys post, so please try to keep it in simple English. I get the mechanics of how a decorator function works, but my issue is following the code logic - specifically why we have to add *args and ** kwargs. Is it correct to state that whatever we pass in to the decorator function that takes in a function which has arguments, will always pass the same arguments into the wrapper function because it's nested within the decorator? That's what I'm missing here. I don't understand how the arguments from the original function get passed in.
Upvotes: 8
Views: 8197
Reputation: 365717
Let's take a simple example:
def tracing(func):
@functools.wraps
def wrapper(*args, **kwargs):
logging.debug(f'Calling {func.__name__}')
try:
return func(*args, **kwargs)
finally:
logging.debug(f'Called {func.__name__}')
return wrapper
@tracing
def spam():
print('spam')
@tracing
def add3(n):
return n+3
You're right that the reason we need to take *args, **kwargs
is so that we can pass that same *args, **kwargs
on to the wrapped function.
This is called "forwarding", or "perfect forwarding". The idea is that tracing
doesn't have to know anything about the function it's wrapping—it could take any set of positional and keyword arguments, and return anything, and the wrapper still works.
For some decorators, that isn't appropriate. For example, a decorator that's designed to cache a set of functions that all have the same API, using one specific parameter as the cache key, could use *args, **kwargs
and then munge through the list and dict to find that specific parameter, but it's a lot simpler, and cleaner, to be explicit:
def caching_spam(func):
cache = {}
@functool.wraps
def wrapper(eggs, beans, spam, cheese):
if spam not in cache:
cache[spam] = func(eggs, beans, spam, cheese)
return cache[spam]
return wrapper
But there are a lot more generic decorators than specific ones.
Upvotes: 10