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?

È stato utile?

Soluzione

  1. Installa il modulo decorator :

    $ pip install decorator
    
  2. 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:

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top