W komentarzu do tej odpowiedzi na inne pytanie ktoś powiedział, że nie jest pewien, co się functools.wraps
dzieje. Zadaję więc to pytanie, aby na StackOverflow zapisano go na przyszłość: co functools.wraps
dokładnie robi ?
W komentarzu do tej odpowiedzi na inne pytanie ktoś powiedział, że nie jest pewien, co się functools.wraps
dzieje. Zadaję więc to pytanie, aby na StackOverflow zapisano go na przyszłość: co functools.wraps
dokładnie robi ?
Odpowiedzi:
Kiedy używasz dekoratora, zamieniasz jedną funkcję na inną. Innymi słowy, jeśli masz dekoratora
def logged(func):
def with_logging(*args, **kwargs):
print(func.__name__ + " was called")
return func(*args, **kwargs)
return with_logging
wtedy kiedy powiesz
@logged
def f(x):
"""does some math"""
return x + x * x
to dokładnie to samo co mówienie
def f(x):
"""does some math"""
return x + x * x
f = logged(f)
a twoja funkcja f
zostanie zastąpiona funkcją with_logging
. Niestety oznacza to, że jeśli to powiesz
print(f.__name__)
wydrukuje się, with_logging
ponieważ taka jest nazwa twojej nowej funkcji. W rzeczywistości, jeśli spojrzysz na dokumentację f
, będzie ona pusta, ponieważ with_logging
nie ma dokumentacji, a więc dokumentacja, którą napisałeś, już jej nie będzie. Ponadto, jeśli spojrzysz na wynik pydoc dla tej funkcji, nie zostanie wymieniony jako przyjmujący jeden argument x
; zamiast tego zostanie wymieniony jako wzięcie *args
i **kwargs
ponieważ to właśnie zajmuje się w usłudze logowanie.
Jeśli użycie dekoratora zawsze oznaczało utratę informacji o funkcji, byłby to poważny problem. Właśnie dlatego mamy functools.wraps
. Pobiera to funkcję używaną w dekoratorze i dodaje funkcjonalność kopiowania nad nazwą funkcji, dokumentacją, listą argumentów itp. A ponieważ wraps
sam jest dekoratorem, następujący kod robi to poprawnie:
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'
functools.wraps
tej pracy, czy nie powinna to być po prostu część wzoru dekoratora? kiedy nie chcesz używać @wraps?
@wraps
, aby wykonać różne typy modyfikacji lub adnotacji na kopiowanych wartościach. Zasadniczo jest to rozszerzenie filozofii Python, które jawne jest lepsze niż niejawne, a przypadki szczególne nie są na tyle wyjątkowe, by łamać reguły. (Kod jest znacznie prostszy, a język łatwiejszy do zrozumienia, jeśli @wraps
trzeba go podać ręcznie, zamiast korzystać z jakiegoś specjalnego mechanizmu rezygnacji.)
Bardzo często używam klas, a nie funkcji, dla moich dekoratorów. Miałem z tym pewne problemy, ponieważ obiekt nie będzie miał tych samych atrybutów, jakich oczekuje się od funkcji. Na przykład obiekt nie będzie miał atrybutu __name__
. Miałem z tym konkretny problem, który był dość trudny do wykrycia, gdy Django zgłaszał błąd „obiekt nie ma atrybutu __name__
” ”. Niestety, dla dekoratorów w stylu klasowym nie wierzę, że @wrap wykona zadanie. Zamiast tego stworzyłem podstawową klasę dekoratora taką:
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)
Ta klasa zastępuje wszystkie wywołania atrybutów dekorowanej funkcji. Możesz teraz utworzyć prosty dekorator, który sprawdza, czy podano 2 argumenty w następujący sposób:
class process_login(DecBase):
def __call__(self, *args):
if len(args) != 2:
raise Exception("You can only specify two arguments")
return self.func(*args)
@wraps
mówią doktorzy z , @wraps
jest to po prostu funkcja wygody dla functools.update_wrapper()
. W przypadku dekoratora klas możesz wywoływać update_wrapper()
bezpośrednio z __init__()
metody Tak więc, nie ma potrzeby tworzenia DecBase
w ogóle, można po prostu to na __init__()
z process_login
linii: update_wrapper(self, func)
. To wszystko.
Począwszy od python 3.5+:
@functools.wraps(f)
def g():
pass
Jest pseudonimem dla g = functools.update_wrapper(g, f)
. Robi dokładnie trzy rzeczy:
__module__
, __name__
, __qualname__
, __doc__
, i __annotations__
atrybuty z f
na g
. Ta domyślna lista jest dostępna WRAPPER_ASSIGNMENTS
, można ją zobaczyć w źródle funools .__dict__
od g
wszystkich elementów z f.__dict__
. (patrz WRAPPER_UPDATES
w źródle)__wrapped__=f
atrybutg
Konsekwencją jest to, że g
wydaje się mieć taką samą nazwę, dokumentację, nazwę modułu i podpis niż f
. Jedyny problem polega na tym, że w przypadku podpisu nie jest to prawdą: po prostu inspect.signature
domyślnie podąża za łańcuchami opakowań. Możesz to sprawdzić, korzystając z inspect.signature(g, follow_wrapped=False)
wyjaśnienia w dokumencie . Ma to irytujące konsekwencje:
Signature.bind()
.Teraz jest trochę zamieszania między functools.wraps
dekoratorami, ponieważ bardzo częstym przypadkiem użycia do programowania dekoratorów jest zawijanie funkcji. Ale oba są całkowicie niezależnymi koncepcjami. Jeśli chcesz zrozumieć różnicę, zaimplementowałem biblioteki pomocnicze dla obu: decopatch, aby łatwo pisać dekoratory, i makefun, aby zapewnić zachowujący podpis zamiennik @wraps
. Pamiętaj, że makefun
polega na tej samej sprawdzonej sztuczce, co słynna decorator
biblioteka.
to jest kod źródłowy o opakowaniach:
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Update a wrapper function to look like the wrapped function
wrapper is the function to be updated
wrapped is the original function
assigned is a tuple naming the attributes assigned directly
from the wrapped function to the wrapper function (defaults to
functools.WRAPPER_ASSIGNMENTS)
updated is a tuple naming the attributes of the wrapper that
are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES)
"""
for attr in assigned:
setattr(wrapper, attr, getattr(wrapped, attr))
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Return the wrapper so this can be used as a decorator via partial()
return wrapper
def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Decorator factory to apply update_wrapper() to a wrapper function
Returns a decorator that invokes update_wrapper() with the decorated
function as the wrapper argument and the arguments to wraps() as the
remaining arguments. Default arguments are as for update_wrapper().
This is a convenience function to simplify applying partial() to
update_wrapper().
"""
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)
Warunek: musisz wiedzieć, jak używać dekoratorów, a zwłaszcza z opakowaniami. Ten komentarz wyjaśnia to trochę jasno lub ten link również wyjaśnia to całkiem dobrze.
Ilekroć używamy np. @: Wraps, a następnie własnej funkcji otoki. Zgodnie ze szczegółami podanymi w tym linku mówi to
funkools.wraps to wygodna funkcja do wywoływania update_wrapper () jako dekoratora funkcji podczas definiowania funkcji otoki.
Jest to równoważne z częściowym (update_wrapper, wrapped = wrapped, przypisany = przypisany, zaktualizowany = zaktualizowany).
Dekorator @wraps faktycznie wywołuje funkcję funools.partial (func [, * args] [, ** keywords]).
Mówi to definicja funkcji funools.partial ()
Funkcja częściowa () służy do częściowej aplikacji funkcji, która „zamraża” pewną część argumentów funkcji i / lub słów kluczowych, w wyniku czego powstaje nowy obiekt z uproszczoną sygnaturą. Na przykład, Partial () może być użyte do utworzenia wywoływalnego programu, który zachowuje się jak funkcja int (), gdzie domyślny argument podstawowy to dwa:
>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18
Co prowadzi mnie do wniosku, że @wraps wywołuje funkcję replace () i przekazuje jej funkcję otoki jako parametr. Funkcja częściowa () na końcu zwraca wersję uproszczoną, tj. Obiekt tego, co znajduje się w funkcji opakowania, a nie samą funkcję opakowania.
Krótko mówiąc, funkools.wraps to zwykła funkcja. Rozważmy ten oficjalny przykład . Za pomocą kodu źródłowego możemy zobaczyć więcej szczegółów na temat implementacji i uruchomionych kroków w następujący sposób:
wrapper = O1 .__ call __ (wrapper)
Sprawdzając implementację __call__ , widzimy, że po tym kroku opakowanie (po lewej stronie) staje się obiektem wynikającym z self.func (* self.args, * args, ** newke words) Sprawdzając tworzenie O1 w __new__ , my wiem self.func to funkcja update_wrapper . Używa parametru * args , opakowania po prawej stronie , jako swojego pierwszego parametru. Sprawdzając ostatni krok update_wrapper , można zobaczyć, czy opakowanie po prawej stronie zostało zwrócone, a niektóre atrybuty zmodyfikowane w razie potrzeby.