Bewahrung von Unterschriften dekorierter Funktionen

111

Angenommen, ich habe einen Dekorateur geschrieben, der etwas sehr Allgemeines tut. Beispielsweise können alle Argumente in einen bestimmten Typ konvertiert, eine Protokollierung durchgeführt, eine Memoisierung implementiert usw. werden.

Hier ist ein Beispiel:

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

Alles gut soweit. Es gibt jedoch ein Problem. Die dekorierte Funktion behält nicht die Dokumentation der ursprünglichen Funktion bei:

>>> help(funny_function)
Help on function g in module __main__:

g(*args, **kwargs)

Zum Glück gibt es eine Problemumgehung:

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

Diesmal sind der Funktionsname und die Dokumentation korrekt:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z

Es gibt aber immer noch ein Problem: Die Funktionssignatur ist falsch. Die Information "* args, ** kwargs" ist so gut wie nutzlos.

Was ist zu tun? Ich kann mir zwei einfache, aber fehlerhafte Problemumgehungen vorstellen:

1 - Fügen Sie die richtige Signatur in die Dokumentzeichenfolge ein:

def funny_function(x, y, z=3):
    """funny_function(x, y, z=3) -- computes x*y + 2*z"""
    return x*y + 2*z

Dies ist wegen der Duplizierung schlecht. Die Signatur wird in der automatisch generierten Dokumentation immer noch nicht richtig angezeigt. Es ist einfach, die Funktion zu aktualisieren und das Ändern der Dokumentzeichenfolge zu vergessen oder einen Tippfehler zu machen. [ Und ja, mir ist bewusst, dass der Docstring den Funktionskörper bereits dupliziert. Bitte ignorieren Sie dies; lustige_Funktion ist nur ein zufälliges Beispiel.]]

2 - Verwenden Sie für jede Unterschrift keinen Dekorateur oder einen Spezialdekorateur:

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

Dies funktioniert gut für eine Reihe von Funktionen mit identischer Signatur, ist jedoch im Allgemeinen nutzlos. Wie eingangs gesagt, möchte ich Dekorateure ganz generisch einsetzen können.

Ich suche nach einer Lösung, die vollständig allgemein und automatisch ist.

Die Frage ist also: Gibt es eine Möglichkeit, die dekorierte Funktionssignatur zu bearbeiten, nachdem sie erstellt wurde?

Kann ich andernfalls einen Dekorator schreiben, der die Funktionssignatur extrahiert und diese Informationen anstelle von "* kwargs, ** kwargs" verwendet, wenn die dekorierte Funktion erstellt wird? Wie extrahiere ich diese Informationen? Wie soll ich die dekorierte Funktion konstruieren - mit exec?

Irgendwelche anderen Ansätze?

Fredrik Johansson
quelle
1
Nie gesagt "veraltet". Ich habe mich mehr oder weniger gefragt, was inspect.Signaturezum Umgang mit dekorierten Funktionen beigetragen hat.
NightShadeQueen

Antworten:

78
  1. Dekorationsmodul installieren :

    $ pip install decorator
  2. Anpassung der Definition von args_as_ints():

    import decorator
    
    @decorator.decorator
    def args_as_ints(f, *args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return f(*args, **kwargs)
    
    @args_as_ints
    def funny_function(x, y, z=3):
        """Computes x*y + 2*z"""
        return x*y + 2*z
    
    print funny_function("3", 4.0, z="5")
    # 22
    help(funny_function)
    # Help on function funny_function in module __main__:
    # 
    # funny_function(x, y, z=3)
    #     Computes x*y + 2*z

Python 3.4+

functools.wraps()von stdlib behält Signaturen seit Python 3.4 bei:

import functools


def args_as_ints(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# 22
help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

functools.wraps()ist mindestens seit Python 2.5 verfügbar, behält dort jedoch die Signatur nicht bei:

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(*args, **kwargs)
#    Computes x*y + 2*z

Hinweis: *args, **kwargsanstelle von x, y, z=3.

jfs
quelle
Ihre Antwort war nicht die erste, aber die bisher umfassendste :-) Ich würde eigentlich eine Lösung bevorzugen, die kein Modul eines Drittanbieters umfasst, aber wenn ich mir die Quelle für das Dekorationsmodul anschaue, ist es einfach genug, dass ich dazu in der Lage sein werde kopiere es einfach.
Fredrik Johansson
1
@ MarkLodato: Bewahrt functools.wraps()bereits Signaturen in Python 3.4+ auf (wie in der Antwort angegeben). Meinst du, die Einstellung wrapper.__signature__hilft bei früheren Versionen? (
Welche
1
@MarkLodato: help()Zeigt die korrekte Signatur in Python 3.4 an. Warum denkst du functools.wraps()ist kaputt und nicht IPython?
JFS
1
@ MarkLodato: Es ist kaputt, wenn wir Code schreiben müssen, um es zu beheben. Angesichts help()der Tatsache, dass das richtige Ergebnis erzielt wird, stellt sich die Frage, welche Software repariert werden sollte: functools.wraps()oder IPython? In jedem Fall ist die manuelle Zuweisung __signature__bestenfalls eine Problemumgehung - es ist keine langfristige Lösung.
JFS
1
Sieht so aus, als würde inspect.getfullargspec()immer noch nicht die richtige Signatur für functools.wrapsPython 3.4 zurückgegeben, die Sie inspect.signature()stattdessen verwenden müssen.
Tuukka Mustonen
16

Dies wird mit der Standardbibliothek functoolsund speziell der Python- functools.wrapsFunktion gelöst, mit der " eine Wrapper-Funktion so aktualisiert werden soll, dass sie wie die Wrapped-Funktion aussieht ". Das Verhalten hängt jedoch von der Python-Version ab, wie unten gezeigt. Auf das Beispiel aus der Frage angewendet würde der Code folgendermaßen aussehen:

from functools import wraps

def args_as_ints(f):
    @wraps(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

Bei Ausführung in Python 3 würde dies Folgendes ergeben:

>>> funny_function("3", 4.0, z="5")
22
>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

Der einzige Nachteil ist, dass in Python 2 die Argumentliste der Funktion nicht aktualisiert wird. Bei der Ausführung in Python 2 wird Folgendes erzeugt:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(*args, **kwargs)
    Computes x*y + 2*z
Timur
quelle
Ich bin mir nicht sicher, ob es sich um Sphinx handelt, aber dies scheint nicht zu funktionieren, wenn die umschlossene Funktion eine Methode einer Klasse ist. Sphinx meldet weiterhin die Anrufsignatur des Dekorateurs.
Alphabetasoup
9

Es gibt ein Dekorationsmodul mit decoratorDekorator, das Sie verwenden können:

@decorator
def args_as_ints(f, *args, **kwargs):
    args = [int(x) for x in args]
    kwargs = dict((k, int(v)) for k, v in kwargs.items())
    return f(*args, **kwargs)

Dann bleibt die Signatur und Hilfe der Methode erhalten:

>>> help(funny_function)
Help on function funny_function in module __main__:

funny_function(x, y, z=3)
    Computes x*y + 2*z

EDIT: JF Sebastian hat darauf hingewiesen, dass ich die args_as_intsFunktion nicht geändert habe - sie ist jetzt behoben.

DzinX
quelle
8

Werfen Sie einen Blick auf die Dekorateur Modul - speziell die Dekorateur Dekorateur, die dieses Problem löst.

Brian
quelle
6

Zweite Option:

  1. Installieren Sie das Wrapt-Modul:

$ easy_install wrapt

Wrapt haben einen Bonus, Klassenunterschrift bewahren.


import wrapt
import inspect

@wrapt.decorator def args_as_ints(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs) @args_as_ints def funny_function(x, y, z=3): """Computes x*y + 2*z""" return x * y + 2 * z >>> funny_function(3, 4, z=5)) # 22 >>> help(funny_function) Help on function funny_function in module __main__: funny_function(x, y, z=3) Computes x*y + 2*z
macm
quelle
2

Wie oben in der Antwort von jfs kommentiert ; Wenn Sie sich mit der Signatur in Bezug auf das Erscheinungsbild ( help, und inspect.signature) befassen , ist die Verwendung functools.wrapsvollkommen in Ordnung.

Wenn Sie sich mit der Signatur in Bezug auf das Verhalten befassen (insbesondere TypeErrorbei nicht übereinstimmenden Argumenten), wird functools.wrapsdiese nicht beibehalten. Sie sollten eher decoratordafür oder meine Verallgemeinerung der Core Engine namens verwenden makefun.

from makefun import wraps

def args_as_ints(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("wrapper executes")
        args = [int(x) for x in args]
        kwargs = dict((k, int(v)) for k, v in kwargs.items())
        return func(*args, **kwargs)
    return wrapper


@args_as_ints
def funny_function(x, y, z=3):
    """Computes x*y + 2*z"""
    return x*y + 2*z


print(funny_function("3", 4.0, z="5"))
# wrapper executes
# 22

help(funny_function)
# Help on function funny_function in module __main__:
#
# funny_function(x, y, z=3)
#     Computes x*y + 2*z

funny_function(0)  
# observe: no "wrapper executes" is printed! (with functools it would)
# TypeError: funny_function() takes at least 2 arguments (1 given)

Siehe auch diesen Beitrag überfunctools.wraps .

smarie
quelle
1
Auch das Ergebnis von inspect.getfullargspecwird nicht durch Aufruf gespeichert functools.wraps.
laike9m
Danke für den nützlichen zusätzlichen Kommentar @ laike9m!
Smarie