Preservare le firme delle funzioni decorate


111

Supponiamo che io abbia scritto un decoratore che fa qualcosa di molto generico. Ad esempio, potrebbe convertire tutti gli argomenti in un tipo specifico, eseguire la registrazione, implementare la memorizzazione, ecc.

Ecco un esempio:

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

Finora tutto bene. Tuttavia, c'è un problema. La funzione decorata non conserva la documentazione della funzione originale:

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

g(*args, **kwargs)

Fortunatamente, c'è una soluzione alternativa:

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

Questa volta, il nome della funzione e la documentazione sono corretti:

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

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

Ma c'è ancora un problema: la firma della funzione è sbagliata. L'informazione "* args, ** kwargs" è quasi inutile.

Cosa fare? Posso pensare a due soluzioni alternative semplici ma imperfette:

1 - Includere la firma corretta nella docstring:

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

Questo è negativo a causa della duplicazione. La firma non verrà comunque mostrata correttamente nella documentazione generata automaticamente. È facile aggiornare la funzione e dimenticare di modificare la docstring o di fare un errore di battitura. [ E sì, sono consapevole del fatto che la docstring duplica già il corpo della funzione. Si prega di ignorarlo; funny_function è solo un esempio casuale. ]

2 - Non utilizzare un decoratore o un decoratore speciale per ogni firma specifica:

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

Funziona bene per un insieme di funzioni che hanno una firma identica, ma in generale è inutile. Come ho detto all'inizio, voglio essere in grado di utilizzare i decoratori in modo del tutto generico.

Sto cercando una soluzione che sia completamente generale e automatica.

Quindi la domanda è: c'è un modo per modificare la firma della funzione decorata dopo che è stata creata?

Altrimenti, posso scrivere un decoratore che estrae la firma della funzione e utilizza quell'informazione invece di "* kwargs, ** kwargs" quando costruisce la funzione decorata? Come estraggo queste informazioni? Come dovrei costruire la funzione decorata - con exec?

Qualche altro approccio?


1
Mai detto "scaduto". Mi chiedevo più o meno cosa inspect.Signatureaggiungere alla gestione delle funzioni decorate.
NightShadeQueen

Risposte:


79
  1. Installa il modulo decoratore :

    $ pip install decorator
  2. Adattare la definizione di 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()da stdlib conserva le firme da Python 3.4:

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()è disponibile almeno da Python 2.5 ma non conserva la firma lì:

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

Avviso: *args, **kwargsinvece di x, y, z=3.


La tua non è stata la prima risposta, ma la più completa finora :-) In realtà preferirei una soluzione che non coinvolgesse un modulo di terze parti, ma guardando l'origine del modulo decoratore, è abbastanza semplice che sarò in grado di farlo basta copiarlo.
Fredrik Johansson,

1
@MarkLodato: functools.wraps()conserva già le firme in Python 3.4+ (come detto nella risposta). Intendi impostare gli wrapper.__signature__aiuti sulle versioni precedenti? (quali versioni hai testato?)
jfs

1
@MarkLodato: help()mostra la firma corretta su Python 3.4. Perché pensi che functools.wraps()sia rotto e non IPython?
jfs

1
@MarkLodato: è rotto se dobbiamo scrivere codice per risolverlo. Dato che help()produce il risultato corretto, la domanda è quale parte di software dovrebbe essere riparata: functools.wraps()o IPython? In ogni caso, l'assegnazione manuale __signature__è nella migliore delle ipotesi una soluzione alternativa, non è una soluzione a lungo termine.
jfs

1
Sembra che inspect.getfullargspec()ancora non restituisca la firma corretta per functools.wrapsin Python 3.4 e che devi usare inspect.signature()invece.
Tuukka Mustonen

16

Questo è risolto con la libreria standard di Python functoolse in particolare la functools.wrapsfunzione, che è progettata per " aggiornare una funzione wrapper in modo che assomigli alla funzione wrapper ". Il suo comportamento dipende dalla versione di Python, tuttavia, come mostrato di seguito. Applicato all'esempio della domanda, il codice sarebbe simile a:

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

Quando eseguito in Python 3, questo produrrebbe quanto segue:

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

Il suo unico svantaggio è che in Python 2, tuttavia, non aggiorna l'elenco degli argomenti della funzione. Quando eseguito in Python 2, produrrà:

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

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

Non sono sicuro che sia Sphinx, ma questo non sembra funzionare quando la funzione wrapping è un metodo di una classe. Sphinx continua a riportare la firma della chiamata del decoratore.
alphabetasoup

9

C'è un modulodecorator decoratore con decoratore che puoi usare:

@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)

Quindi la firma e l'aiuto del metodo vengono preservati:

>>> 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 ha sottolineato che non ho modificato la args_as_intsfunzione - ora è stato risolto.



6

Seconda opzione:

  1. Installa il modulo wrapt:

$ easy_install wrapt

wrapt ha un bonus, conserva la firma della classe.


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

2

Come commentato sopra nella risposta di jfs ; se ti preoccupi della firma in termini di aspetto ( helpe inspect.signature), l'uso functools.wrapsva benissimo.

Se ti preoccupi della firma in termini di comportamento (in particolare TypeErrorin caso di mancata corrispondenza degli argomenti), functools.wrapsnon la preserva. Dovresti piuttosto usare decoratorper quello, o la mia generalizzazione del suo motore principale, denominato 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)

Vedi anche questo post sufunctools.wraps .


1
Inoltre, il risultato di inspect.getfullargspecnon viene mantenuto chiamando functools.wraps.
laike9m

Grazie per l'utile commento aggiuntivo @ laike9m!
smarie
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.