Do czego służy funkools.wraps?

650

W komentarzu do tej odpowiedzi na inne pytanie ktoś powiedział, że nie jest pewien, co się functools.wrapsdzieje. Zadaję więc to pytanie, aby na StackOverflow zapisano go na przyszłość: co functools.wrapsdokładnie robi ?

Eli Courtwright
źródło

Odpowiedzi:

1069

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 fzostanie zastąpiona funkcją with_logging. Niestety oznacza to, że jeśli to powiesz

print(f.__name__)

wydrukuje się, with_loggingponieważ taka jest nazwa twojej nowej funkcji. W rzeczywistości, jeśli spojrzysz na dokumentację f, będzie ona pusta, ponieważ with_loggingnie 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 *argsi **kwargsponieważ 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ż wrapssam 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'
Eli Courtwright
źródło
7
Tak, wolę unikać modułu dekoratora, ponieważ funools.wraps jest częścią standardowej biblioteki i dlatego nie wprowadza innej zależności zewnętrznej. Ale moduł dekoratora rzeczywiście rozwiązuje problem pomocy, który, jak mam nadzieję, kiedyś również zrobi funools.wraps.
Eli Courtwright,
6
oto przykład tego, co może się stać, jeśli nie użyjesz zawijania: testy doctools mogą nagle zniknąć. dzieje się tak, ponieważ doctools nie mogą znaleźć testów w dekorowanych funkcjach, chyba że coś takiego jak wraps () je skopiowało.
Andrew Cooke
88
dlaczego potrzebujemy functools.wrapstej pracy, czy nie powinna to być po prostu część wzoru dekoratora? kiedy nie chcesz używać @wraps?
wim
56
@ wim: Napisałem kilka dekoratorów, którzy wykonują własną wersję @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 @wrapstrzeba go podać ręcznie, zamiast korzystać z jakiegoś specjalnego mechanizmu rezygnacji.)
ssokolow
35
@LucasMalor Nie wszyscy dekoratorzy pakują funkcje, które dekorują. Niektóre stosują skutki uboczne, takie jak rejestrowanie ich w pewnego rodzaju systemie wyszukiwania.
ssokolow
22

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)
Josh
źródło
7
Jak @wrapsmówią doktorzy z , @wrapsjest 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 DecBasew ogóle, można po prostu to na __init__()z process_loginlinii: update_wrapper(self, func). To wszystko.
Fabiano
14

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:

  • Program kopiuje __module__, __name__, __qualname__, __doc__, i __annotations__atrybuty z fna g. Ta domyślna lista jest dostępna WRAPPER_ASSIGNMENTS, można ją zobaczyć w źródle funools .
  • aktualizuje __dict__od gwszystkich elementów z f.__dict__. (patrz WRAPPER_UPDATESw źródle)
  • ustawia nowy __wrapped__=fatrybutg

Konsekwencją jest to, że gwydaje 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.signaturedomyś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:

  • kod opakowania zostanie wykonany, nawet jeśli podane argumenty będą niepoprawne.
  • kod opakowania nie może łatwo uzyskać dostępu do argumentu przy użyciu jego nazwy z otrzymanych * args, ** kwargs. Rzeczywiście należałoby obsłużyć wszystkie przypadki (pozycyjne, kluczowe, domyślne), a zatem użyć czegoś takiego Signature.bind().

Teraz jest trochę zamieszania między functools.wrapsdekoratorami, 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 makefunpolega na tej samej sprawdzonej sztuczce, co słynna decoratorbiblioteka.

smarie
źródło
3

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)
Baliang
źródło
2
  1. 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.

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

3rdi
źródło
-4

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:

  1. wraps (f) zwraca obiekt, powiedzmy O1 . Jest to obiekt klasy Częściowa
  2. Następnym krokiem jest @ O1 ... który jest notacją dekoratora w pythonie. To znaczy

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.

Yong Yang
źródło