Что делает functools.wraps?
В комментарии к этому ответу на другой вопрос кто-то сказал, что не был уверен, что functools.wraps
делает. Итак, я задаю этот вопрос, чтобы в StackOverflow была запись об этом для дальнейшего использования: что именно functools.wraps
делает?
Переведено автоматически
Ответ 1
Когда вы используете декоратор, вы заменяете одну функцию другой. Другими словами, если у вас есть декоратор
def logged(func):
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
затем, когда вы говорите
@logged
def f(x):
"""does some math"""
return x + x * x
это точно то же самое, что сказать
def f(x):
"""does some math"""
return x + x * x
f = logged(f)
и ваша функция f
заменяется функцией with_logging
. К сожалению, это означает, что если вы затем скажете
print(f.__name__)
он будет напечатан with_logging
потому что это имя вашей новой функции. На самом деле, если вы посмотрите на строку документа для f
, она будет пустой, потому что with_logging
не содержит строки документа, и поэтому строки документа, которую вы написали, там больше не будет. Кроме того, если вы посмотрите на результат pydoc для этой функции, он не будет указан как принимающий один аргумент x
; вместо этого он будет указан как принимающий *args
и **kwargs
, потому что это то, что принимает with_logging .
Если бы использование декоратора всегда означало потерю этой информации о функции, это было бы серьезной проблемой. Вот почему у нас есть functools.wraps
. Это принимает функцию, используемую в декораторе, и добавляет функциональность копирования имени функции, строки документации, списка аргументов и т.д. И поскольку wraps
сам является декоратором, следующий код выполняет правильные действия:
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
@logged
def f(x):
"""does some math"""
return x + x * x
print(f.__name__) # prints 'f'
print(f.__doc__) # prints 'does some math'
Ответ 2
Начиная с python 3.5+:
@functools.wraps(f)
def g():
pass
Is an alias for g = functools.update_wrapper(g, f)
. It does exactly three things:
- it copies the
__module__
,__name__
,__qualname__
,__doc__
, and__annotations__
attributes off
ong
. This default list is inWRAPPER_ASSIGNMENTS
, you can see it in the functools source. - it updates the
__dict__
ofg
with all elements fromf.__dict__
. (seeWRAPPER_UPDATES
in the source) - it sets a new
__wrapped__=f
attribute ong
The consequence is that g
appears as having the same name, docstring, module name, and signature than f
. The only problem is that concerning the signature this is not actually true: it is just that inspect.signature
follows wrapper chains by default. You can check it by using inspect.signature(g, follow_wrapped=False)
as explained in the doc. This has annoying consequences:
- the wrapper code will execute even when the provided arguments are invalid.
- the wrapper code can not easily access an argument using its name, from the received *args, **kwargs. Indeed one would have to handle all cases (positional, keyword, default) and therefore to use something like
Signature.bind()
.
Now there is a bit of confusion between functools.wraps
and decorators, because a very frequent use case for developing decorators is to wrap functions. But both are completely independent concepts. If you're interested in understanding the difference, I implemented helper libraries for both: decopatch to write decorators easily, and makefun to provide a signature-preserving replacement for @wraps
. Note that makefun
relies on the same proven trick than the famous decorator
library.
Ответ 3
- Assume we have this: Simple Decorator which takes a function’s output and puts it into a string, followed by three !!!!.
def mydeco(func):
def wrapper(*args, **kwargs):
return f'{func(*args, **kwargs)}!!!'
return wrapper
- Let’s now decorate two different functions with “mydeco”:
@mydeco
def add(a, b):
'''Add two objects together, the long way'''
return a + b
@mydeco
def mysum(*args):
'''Sum any numbers together, the long way'''
total = 0
for one_item in args:
total += one_item
return total
- when run add(10,20), mysum(1,2,3,4), it worked!
>>> add(10,20)
'30!!!'
>>> mysum(1,2,3,4)
'10!!!!'
- However, the name attribute, which gives us the name of a function when we define it,
>>>add.__name__
'wrapper`
>>>mysum.__name__
'wrapper'
- Worse
>>> help(add)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
>>> help(mysum)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
- we can fix partially by:
def mydeco(func):
def wrapper(*args, **kwargs):
return f'{func(*args, **kwargs)}!!!'
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
- now we run step 5 (2nd time) again:
>>> help(add)
Help on function add in module __main__:
add(*args, **kwargs)
Add two objects together, the long way
>>> help(mysum)
Help on function mysum in module __main__:
mysum(*args, **kwargs)
Sum any numbers together, the long way
- but we can use functools.wraps (decotator tool)
from functools import wraps
def mydeco(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f'{func(*args, **kwargs)}!!!'
return wrapper
- now run step 5 (3rd time) again
>>> help(add)
Help on function add in module main:
add(a, b)
Add two objects together, the long way
>>> help(mysum)
Help on function mysum in module main:
mysum(*args)
Sum any numbers together, the long way
Ответ 4
I very often use classes, rather than functions, for my decorators. I was having some trouble with this because an object won't have all the same attributes that are expected of a function. For example, an object won't have the attribute __name__
. I had a specific issue with this that was pretty hard to trace where Django was reporting the error "object has no attribute '__name__
'". Unfortunately, for class-style decorators, I don't believe that @wrap will do the job. I have instead created a base decorator class like so:
class DecBase(object):
func = None
def __init__(self, func):
self.__func = func
def __getattribute__(self, name):
if name == "func":
return super(DecBase, self).__getattribute__(name)
return self.func.__getattribute__(name)
def __setattr__(self, name, value):
if name == "func":
return super(DecBase, self).__setattr__(name, value)
return self.func.__setattr__(name, value)
This class proxies all the attribute calls over to the function that is being decorated. So, you can now create a simple decorator that checks that 2 arguments are specified like so:
class process_login(DecBase):
def __call__(self, *args):
if len(args) != 2:
raise Exception("You can only specify two arguments")
return self.func(*args)