Preservare le firme delle funzioni decorate
Domanda
Supponiamo di aver 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 memoization, 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. C'è un problema, tuttavia. La funzione decorata non conserva la documentazione della funzione originale:
>>> help(funny_function)
Help on function g in module __main__:
g(*args, **kwargs)
Fortunatamente, esiste 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. Le informazioni "args, ** kwargs" è quasi inutile.
Cosa fare? Mi vengono in mente due soluzioni semplici ma imperfette:
1 - Includi la firma corretta nel docstring:
def funny_function(x, y, z=3):
"""funny_function(x, y, z=3) -- computes x*y + 2*z"""
return x*y + 2*z
Ciò è negativo a causa della duplicazione. La firma non verrà comunque mostrata correttamente nella documentazione generata automaticamente. È facile aggiornare la funzione e dimenticare di modificare il docstring o di fare un refuso. [ E sì, sono consapevole del fatto che il docstring duplica già il corpo della funzione. Per favore, ignoralo; funny_function è solo un esempio casuale. ]
2 - Non utilizzare un decoratore o utilizzare 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 set di funzioni con firma identica, ma è inutile in generale. Come ho detto all'inizio, voglio essere in grado di utilizzare i decoratori in modo del tutto generico.
Sto cercando una soluzione 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 usa tali informazioni invece di " * kwargs, ** kwargs " quando si costruisce la funzione decorata? Come estraggo tali informazioni? Come dovrei costruire la funzione decorata - con exec?
Qualche altro approccio?
Soluzione
-
Installa il modulo decorator :
$ pip install decorator
-
Adatta 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, ** kwargs
invece di x, y, z = 3
.
Altri suggerimenti
Questo è risolto con la libreria standard functools
di Python e in particolare functools.wraps
, progettato per " aggiornare una funzione wrapper per assomigliare alla funzione wrapped " ;. 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
Se eseguito in Python 3, ciò 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
L'unico inconveniente è che in Python 2, tuttavia, non aggiorna l'elenco degli argomenti della funzione. Se 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
Esiste un modulo decoratore con decoratore
decoratore 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 conservati:
>>> help(funny_function)
Help on function funny_function in module __main__:
funny_function(x, y, z=3)
Computes x*y + 2*z
EDIT: J. F. Sebastian ha sottolineato che non ho modificato la funzione args_as_ints
- ora è stata risolta.
Dai un'occhiata al decorator , in particolare il < a href = "http://www.phyast.pitt.edu/~micheles/python/documentation.html#decorator-is-a-decorator" rel = "noreferrer"> decorator decorator, che risolve questo problema .
Seconda opzione:
- Installa il modulo wrapt:
$ easy_install wrapt
wrapt ha un bonus, preserva 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
Come commentato sopra in la risposta di jfs ; se ti preoccupi della firma in termini di aspetto ( help
e inspect.signature
), l'utilizzo di functools.wraps
va benissimo.
Se sei interessato alla firma in termini di comportamento (in particolare TypeError
in caso di mancata corrispondenza degli argomenti), functools.wraps
non la conserva. Dovresti piuttosto usare decorator
per questo, o la mia generalizzazione del suo motore principale, chiamato 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 su functools.wraps
.