Załóżmy, że napisałem dekoratora, który robi coś bardzo ogólnego. Na przykład może przekonwertować wszystkie argumenty na określony typ, przeprowadzić rejestrowanie, zaimplementować zapamiętywanie itp.
Oto przykład:
def args_as_ints(f):
def g(*args, **kwargs):
args = [int(x) for x in args]
kwargs = dict((k, int(v)) for k, v in kwargs.items())
return f(*args, **kwargs)
return g
@args_as_ints
def funny_function(x, y, z=3):
"""Computes x*y + 2*z"""
return x*y + 2*z
>>> funny_function("3", 4.0, z="5")
22
Jak dotąd wszystko dobrze. Jest jednak jeden problem. Dekorowana funkcja nie zachowuje dokumentacji oryginalnej funkcji:
>>> help(funny_function)
Help on function g in module __main__:
g(*args, **kwargs)
Na szczęście istnieje obejście:
def args_as_ints(f):
def g(*args, **kwargs):
args = [int(x) for x in args]
kwargs = dict((k, int(v)) for k, v in kwargs.items())
return f(*args, **kwargs)
g.__name__ = f.__name__
g.__doc__ = f.__doc__
return g
@args_as_ints
def funny_function(x, y, z=3):
"""Computes x*y + 2*z"""
return x*y + 2*z
Tym razem nazwa funkcji i dokumentacja są poprawne:
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(*args, **kwargs)
Computes x*y + 2*z
Ale nadal jest problem: podpis funkcji jest nieprawidłowy. Informacja "* args, ** kwargs" jest prawie bezużyteczna.
Co robić? Przychodzą mi do głowy dwa proste, ale wadliwe obejścia:
1 - Dołącz poprawny podpis w dokumencie:
def funny_function(x, y, z=3):
"""funny_function(x, y, z=3) -- computes x*y + 2*z"""
return x*y + 2*z
To jest złe z powodu duplikacji. Podpis nadal nie będzie poprawnie wyświetlany w automatycznie generowanej dokumentacji. Łatwo jest zaktualizować funkcję i zapomnieć o zmianie dokumentu lub popełnieniu literówki. [ I tak, zdaję sobie sprawę z faktu, że dokumentacja już powiela treść funkcji. Proszę zignoruj to; funny_function to tylko przypadkowy przykład. ]
2 - Nie używaj dekoratora lub używaj dekoratora specjalnego przeznaczenia do każdego konkretnego podpisu:
def funny_functions_decorator(f):
def g(x, y, z=3):
return f(int(x), int(y), z=int(z))
g.__name__ = f.__name__
g.__doc__ = f.__doc__
return g
Działa to dobrze w przypadku zestawu funkcji, które mają identyczny podpis, ale ogólnie jest bezużyteczne. Jak powiedziałem na początku, chcę mieć możliwość używania dekoratorów w sposób całkowicie ogólny.
Szukam rozwiązania w pełni ogólnego i automatycznego.
Pytanie brzmi więc: czy istnieje sposób na edycję podpisu dekorowanej funkcji po jej utworzeniu?
W przeciwnym razie, czy mogę napisać dekorator, który wyodrębnia sygnaturę funkcji i używa tych informacji zamiast "* kwargs, ** kwargs" podczas konstruowania funkcji dekorowanej? Jak wyodrębnić te informacje? Jak mam skonstruować dekorowaną funkcję - z exec?
Jakieś inne podejścia?
inspect.Signature
dodało do zajmowania się dekorowanymi funkcjami.Odpowiedzi:
Zainstaluj moduł dekoratora :
Dostosuj definicję
args_as_ints()
:Python 3.4+
functools.wraps()
ze stdlib zachowuje podpisy od Pythona 3.4:functools.wraps()
jest dostępny co najmniej od Pythona 2.5, ale nie zachowuje tam podpisu:Uwaga:
*args, **kwargs
zamiastx, y, z=3
.źródło
functools.wraps()
już zachowuje podpisy w Pythonie 3.4+ (jak wspomniano w odpowiedzi). Czy masz na myśli ustawieniewrapper.__signature__
pomocy we wcześniejszych wersjach? (które wersje testowałeś?)help()
pokazuje poprawny podpis w Pythonie 3.4. Jak myślisz, dlaczegofunctools.wraps()
jest uszkodzony, a nie IPython?help()
daje to poprawny wynik, pytanie brzmi, które oprogramowanie powinno zostać naprawione:functools.wraps()
czy IPython? W każdym razie ręczne przypisywanie__signature__
jest w najlepszym przypadku obejściem - nie jest to rozwiązanie długoterminowe.inspect.getfullargspec()
nadal nie zwraca prawidłowego podpisu dlafunctools.wraps
w Pythonie 3.4 i należy goinspect.signature()
zamiast tego użyć .Można to rozwiązać za pomocą standardowej biblioteki Pythona,
functools
a konkretniefunctools.wraps
funkcji, która ma na celu „ aktualizację funkcji opakowującej, aby wyglądała jak funkcja opakowana ”. Jego zachowanie zależy jednak od wersji Pythona, jak pokazano poniżej. Zastosowany do przykładu z pytania kod wyglądałby następująco:Po wykonaniu w Pythonie 3 dałoby to następujące efekty:
Jego jedyną wadą jest to, że w Pythonie 2 nie aktualizuje listy argumentów funkcji. Po uruchomieniu w Pythonie 2 wygeneruje:
źródło
Istnieje moduł
decorator
dekoratora z dekoratorem, którego możesz użyć:Następnie zostaje zachowany podpis i pomoc metody:
EDYCJA: JF Sebastian zwrócił uwagę, że nie modyfikowałem
args_as_ints
funkcji - teraz jest to naprawione.źródło
Przyjrzyj się modułowi dekoratora - konkretnie dekoratorowi dekoratora, który rozwiązuje ten problem.
źródło
Druga opcja:
$ easy_install wrapt
wrapt mają bonus, zachowaj podpis klasy.
źródło
Jak skomentowano powyżej w odpowiedzi jfs ; jeśli obawiasz się podpisu pod względem wyglądu (
help
iinspect.signature
), używaniefunctools.wraps
jest w porządku.Jeśli martwisz się o podpis pod względem zachowania (w szczególności
TypeError
w przypadku niezgodności argumentów),functools.wraps
nie zachowuje go. Powinieneś raczej użyćdecorator
do tego lub mojego uogólnienia jego podstawowego silnika o nazwiemakefun
.Zobacz także ten post o
functools.wraps
.źródło
inspect.getfullargspec
nie jest również utrzymywany przez wywołaniefunctools.wraps
.