Вопрос-Ответ

What does functools.wraps do?

Что делает 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 of f on g. This default list is in WRAPPER_ASSIGNMENTS, you can see it in the functools source.

  • it updates the __dict__ of g with all elements from f.__dict__. (see WRAPPER_UPDATES in the source)

  • it sets a new __wrapped__=f attribute on g

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

  1. 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

  1. 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

  1. when run add(10,20), mysum(1,2,3,4), it worked!

>>> add(10,20)
'30!!!'

>>> mysum(1,2,3,4)
'10!!!!'

  1. However, the name attribute, which gives us the name of a function when we define it,

>>>add.__name__
'wrapper`

>>>mysum.__name__
'
wrapper'

  1. 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)

  1. 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

  1. 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


  1. 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

  1. 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

Reference

Ответ 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)
2024-01-29 15:16 python