Topics
When we annotate a function with functools.wraps as follows:
@functools.wraps(f)
def g():
passwe are basically doing:
# update wrapper updates in-place, so no need to capture return
g = functools.update_wrapper(g, f)It does three things:
- copies
WRAPPER_ASSIGNMENTS(__module__, __name__, __doc__, etc) fromftog - updates the attribute dictionary (
__dict__) ofgwith all elements fromf.__dict__ - sets a new
__wrapped__= <function f at 0x123... >attribute ong
The consequence is that g appears as having the same name, docstring, module name, and signature as f. This is useful when combined with decorators. When we decorate a function for example, the closure (inner) is the one that runs our wrapped function (cached):
def wrapper(func):
def inner(*args):
return cached(func, *args)
return inner
@wrapper
def norm(vec):
"Get the norm of the vector"
print("Computing...")
return math.sqrt(sum(i*i for i in vec))
If we do something like the following: print(norm.__doc__), we get None and print(norm) gives us function wrapper.<locals>.inner at 0x123...>. To fix this we use @wraps:
def wrapper(func):
@functools.wraps(func) # The fix
def inner(*args):
return cached(func, *args)
return inner
# ...
print(norm.__doc__) # Get the norm of the vector
print(norm) # <function norm at 0x123...>This is good, but what if we are using class-based decorator:
class wrapper:
def __init__(self, func):
self.func = func
def __call__(self, *args):
return cached(self.func, *args)In this case, we can’t use the annotation syntax of wraps, but instead call it directly as such:
class wrapper:
def __init__(self, func):
self.func = func
functools.wraps(func)(self)
# OR
# functools.update_wrapper(self, func)
def __call__(self, *args):
return cached(self.func, *args)
# this is to make print(norm) behave the same
def __repr__(self):
return self.func.__repr__()Another way to achieve the same is during instance creation:
class wrapper:
def __init__(self, func):
self.func = func
def __new__(cls, func):
return functools.update_wrapper(
super().__new__(cls),
func
)
def __call__(self, *args):
return cached(self.func, *args)
def __repr__(self):
return self.func.__repr__()It’s simpler and more convenient to call update_wrapper in __init__, instead of __new__.