Topics
When we annotate a function with functools.wraps
as follows:
@functools.wraps(f)
def g():
pass
we 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) fromf
tog
- updates the attribute dictionary (
__dict__
) ofg
with 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__
.