Pregunta

Estoy escribiendo un decorador que necesita llamar a otras funciones antes de llamar a la función que está decorando. La función decorada puede tener argumentos posicionales, pero las funciones que llamará el decorador solo pueden aceptar argumentos de palabras clave. ¿Alguien tiene una manera práctica de convertir argumentos posicionales en argumentos de palabras clave?

Sé que puedo obtener una lista de los nombres de las variables de la función decorada:

>>> def a(one, two=2):
...    pass

>>> a.func_code.co_varnames
('one', 'two')

Pero no puedo entender cómo saber qué se pasó en posición y qué fue como palabra clave.

Mi decorador se ve así:

class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f

    def __call__(self, *args, **kwargs):
        hozer(**kwargs)
        self.f(*args, **kwargs)

¿Hay alguna otra manera que no sea solo comparar kwargs y co_varnames, agregar a kwargs algo que no esté allí y esperar lo mejor?

¿Fue útil?

Solución

Nota: co_varnames incluirá variables locales y palabras clave. Esto probablemente no importará, ya que zip trunca la secuencia más corta, pero puede generar mensajes de error confusos si pasa el número incorrecto de argumentos.

Puede evitar esto con func_code.co_varnames [: func_code.co_argcount] , pero es mejor usar inspeccionar el módulo. es decir:

import inspect
argnames, varargs, kwargs, defaults = inspect.getargspec(func)

También es posible que desee manejar el caso en el que la función define ** kwargs o * args (aunque solo sea para generar una excepción cuando se usa con el decorador). Si se configuran, el segundo y tercer resultado de getargspec devolverán su nombre de variable, de lo contrario serán Ninguno.

Otros consejos

Cualquier argumento que se haya pasado posicionalmente se pasará a * args. Y cualquier argumento pasado como palabra clave se pasará a ** kwargs. Si tiene valores y nombres de argumentos posicionales, puede hacer:

kwargs.update(dict(zip(myfunc.func_code.co_varnames, args)))

para convertirlos a todos en argumentos de palabras clave.

Si está utilizando Python > = 2.7 inspect.getcallargs () lo hace de forma inmediata. Simplemente debe pasarle la función decorada como primer argumento, y luego el resto de los argumentos exactamente como planea llamarlo. Ejemplo:

>>> def f(p1, p2, k1=None, k2=None, **kwargs):
...     pass
>>> from inspect import getcallargs

Estoy planeando hacer f ('p1', 'p2', 'p3', k2 = 'k2', extra = 'kx1') (tenga en cuenta que k1 se está pasando posicionalmente como p3), entonces ...

>>> call_args = getcallargs(f, 'p1', 'p2', 'p3', k2='k2', extra='kx1')
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'kwargs': {'extra': 'kx1'}}

Si sabe que la función decorada no usará ** kwargs , esa tecla no aparecerá en el dict, y ya está (y supongo que no hay * args , ya que eso rompería el requisito de que todo tenga un nombre). Si do tiene ** kwargs , como tengo en este ejemplo, y desea incluirlos con el resto de los argumentos nombrados, se necesita una línea más:

>>> call_args.update(call_args.pop('kwargs'))
>>> call_args
{'p2': 'p2', 'k2': 'k2', 'k1': 'p3', 'p1': 'p1', 'extra': 'kx1'}

Actualización: para Python > = 3.3, consulte < code> inspect.Signature.bind () y el inspect.signature function para una funcionalidad similar a (pero más robusta que) inspect.getcallargs () .

Bueno, esto puede ser excesivo. Lo escribí para el paquete dectools (en PyPi), para que pueda obtener actualizaciones allí. Devuelve el diccionario teniendo en cuenta los argumentos posicionales, de palabras clave y predeterminados. Hay un conjunto de pruebas en el paquete (test_dict_as_called.py):

 def _dict_as_called(function, args, kwargs):
""" return a dict of all the args and kwargs as the keywords they would
be received in a real function call.  It does not call function.
"""

names, args_name, kwargs_name, defaults = inspect.getargspec(function)

# assign basic args
params = {}
if args_name:
    basic_arg_count = len(names)
    params.update(zip(names[:], args))  # zip stops at shorter sequence
    params[args_name] = args[basic_arg_count:]
else:
    params.update(zip(names, args))    

# assign kwargs given
if kwargs_name:
    params[kwargs_name] = {}
    for kw, value in kwargs.iteritems():
        if kw in names:
            params[kw] = value
        else:
            params[kwargs_name][kw] = value
else:
    params.update(kwargs)

# assign defaults
if defaults:
    for pos, value in enumerate(defaults):
        if names[-len(defaults) + pos] not in params:
            params[names[-len(defaults) + pos]] = value

# check we did it correctly.  Each param and only params are set
assert set(params.iterkeys()) == (set(names)|set([args_name])|set([kwargs_name])
                                  )-set([None])

return params

Aquí hay un método más nuevo para resolver esto usando inspect.signature (para Python 3.3+). Daré un ejemplo que se puede ejecutar / probar primero y luego mostrar cómo modificar el código original con él.

Aquí hay una función de prueba que simplemente resume cualquier args / kwargs que se le haya dado; se requiere al menos un argumento ( a ) y hay un argumento de solo palabras clave con un valor predeterminado ( b ), solo para probar diferentes aspectos de las firmas de funciones.

def silly_sum(a, *args, b=1, **kwargs):
    return a + b + sum(args) + sum(kwargs.values())

Ahora hagamos un contenedor para silly_sum que se puede llamar de la misma manera que silly_sum (con una excepción que veremos) pero que solo pasa kwargs al silly_sum envuelto.

def wrapper(f):
    sig = inspect.signature(f)
    def wrapped(*args, **kwargs):
        bound_args = sig.bind(*args, **kwargs)
        bound_args.apply_defaults()
        print(bound_args) # just for testing

        all_kwargs = bound_args.arguments
        assert len(all_kwargs.pop("args")) == 0
        all_kwargs.update(all_kwargs.pop("kwargs"))
        return f(**all_kwargs)
    return wrapped

sig.bind devuelve un objeto BoundArguments , pero esto no tiene en cuenta los valores predeterminados a menos que llame explícitamente a apply_defaults . Hacerlo también generará una tupla vacía para args y un dict vacío para kwargs si no se dieron * args / ** kwargs .

sum_wrapped = wrapper(silly_sum)
sum_wrapped(1, c=9, d=11)
# prints <BoundArguments (a=1, args=(), b=1, kwargs={'c': 9, 'd': 11})>
# returns 22

Luego, solo obtenemos el diccionario de argumentos y agregamos cualquier ** kwargs . La excepción al uso de este contenedor es que * args no se puede pasar al función. Esto se debe a que no hay nombres para estos, por lo que no podemos convertirlos en kwargs. Si pasarlos como un kwarg llamado args es aceptable, eso podría hacerse en su lugar.


Aquí es cómo se puede aplicar esto al código original:

import inspect


class mydec(object):
    def __init__(self, f, *args, **kwargs):
        self.f = f
        self._f_sig = inspect.signature(f)

    def __call__(self, *args, **kwargs):
        bound_args = self._f_sig.bind(*args, **kwargs)
        bound_args.apply_defaults()
        all_kwargs = bound_args.arguments
        assert len(all_kwargs.pop("args")) == 0
        all_kwargs.update(all_kwargs.pop("kwargs"))
        hozer(**all_kwargs)
        self.f(*args, **kwargs)
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top