¿Cómo llamar a una función con un diccionario que contiene más elementos que los parámetros de la función?


Estoy buscando la mejor manera de combinar una función con un diccionario que contiene más elementos que las entradas de la función

Basic * * kwarg desempaquetado falla en este caso:

def foo(a,b):
    return a + b

d = {'a':1,
     'b':2,
     'c':3}

foo(**d)
--> TypeError: foo() got an unexpected keyword argument 'c'

Después de algunas investigaciones, se me ocurrió el siguiente enfoque:

import inspect

# utilities
def get_input_names(function):
    '''get arguments names from function'''
    return inspect.getargspec(function)[0]

def filter_dict(dict_,keys):
    return {k:dict_[k] for k in keys}

def combine(function,dict_):
    '''combine a function with a dictionary that may contain more items than the function's inputs '''
    filtered_dict = filter_dict(dict_,get_input_names(function))
    return function(**filtered_dict)

# examples
def foo(a,b):
    return a + b

d = {'a':1,
     'b':2,
     'c':3}

print combine(foo,d)
--> 3

Mi pregunta es: ¿es esta una buena manera de tratar este problema, o hay una mejor práctica o hay un mecanismo en el lenguaje que me estoy perdiendo tal vez?

Author: Bergi, 2016-03-19

6 answers

¿Qué tal hacer un decorador eso filtraría solo los argumentos de palabras clave permitidas :

import inspect


def get_input_names(function):
    '''get arguments names from function'''
    return inspect.getargspec(function)[0]


def filter_dict(dict_,keys):
    return {k:dict_[k] for k in keys}


def filter_kwargs(func):
   def func_wrapper(**kwargs):
       return func(**filter_dict(kwargs, get_input_names(func)))
   return func_wrapper


@filter_kwargs
def foo(a,b):
    return a + b


d = {'a':1,
     'b':2,
     'c':3}

print(foo(**d))

Lo bueno de este decorador es que es genérico y reutilizable. Y no tendría que cambiar la forma en que llama y usa sus funciones de destino.

 23
Author: alecxe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-19 13:25:32

Todas estas respuestas son erróneas.

No es posible hacer lo que estás pidiendo, porque la función podría declararse así:

def foo(**kwargs):
    a = kwargs.pop('a')
    b = kwargs.pop('b')
    if kwargs:
        raise TypeError('Unexpected arguments: %r' % kwargs)

Ahora, ¿por qué diablos alguien escribiría eso?

Porque no conocen todos los argumentos de antemano. He aquí un caso más realista:

def __init__(self, **kwargs):
    for name in self.known_arguments():
        value = kwargs.pop(name, default)
        self.do_something(name, value)
    super().__init__(**kwargs)  # The superclass does not take any arguments

Y aquí hay algún código del mundo real que realmente hace esto.

Usted podría preguntar por qué necesitamos la última línea. Por qué pasar argumentos a una superclase que no tomar cualquier? Herencia múltiple cooperativa. Si mi clase obtiene un argumento que no reconoce, no debe tragarse ese argumento, ni debe equivocarse. Debería pasar el argumento por la cadena para que otra clase que no conozco pueda manejarlo. Y si nadie lo maneja, entonces object.__init__() proporcionará un mensaje de error apropiado. Desafortunadamente, las otras respuestas no manejarán eso con gracia. Ellos verán **kwargs y o bien no pasar argumentos o pasar todos ellos, que ambos son incorrectos.

La conclusión: No hay una manera general de descubrir si una llamada a una función es legal sin realmente hacer esa llamada a la función. inspect es una aproximación cruda, y se desmorona completamente frente a funciones variádicas. Variadic no significa "pase lo que quiera"; significa "las reglas son demasiado complejas para expresarlas en una firma."Como resultado, si bien puede ser posible en muchos casos hacer lo que está tratando de hacer, siempre habrá situaciones donde no hay una respuesta correcta.

 14
Author: Kevin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-19 17:45:43

Su problema radica en la forma en que definió su función, debería definirse así -

def foo(**kwargs):

Y luego dentro de la función puede iterar sobre el número de argumentos enviados a la función como so -

if kwargs is not None:
        for key, value in kwargs.iteritems():
                do something

Puedes encontrar más información sobre el uso de **kwargs en este post - http://pythontips.com/2013/08/04/args-and-kwargs-in-python-explained /

 11
Author: Yaron Idan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-19 13:11:05

También puede usar una función decoradora para filtrar aquellos argumentos de palabras clave que no están permitidos en su función. De usted utiliza el signature función nuevo en 3.3 para devolver su función Signature

from inspect import signature
from functools import wraps


def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        sig = signature(func)
        result = func(*[kwargs[param] for param in sig.parameters])
        return result
    return wrapper

Desde Python 3.0 puedes usar getargspec que es obsoleto desde la versión 3.0

import inspect


def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        argspec = inspect.getargspec(func).args
        result = func(*[kwargs[param] for param in argspec])
            return result
    return wrapper

Para aplicar su decorar una función existente que necesita para pasar su función como argumento a su decorador:

Demo:

>>> def foo(a, b):
...     return a + b
... 
>>> foo = decorator(foo)
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> foo(**d)
3

Para aplicar su decorador a una nueva función simplemente use @

>>> @decorator
... def foo(a, b):
...     return a + b
... 
>>> foo(**d)
3

También puede definir su función usando argumentos de palabras clave arbitrarios **kwargs.

>>> def foo(**kwargs):
...     if 'a' in kwargs and 'b' in kwargs:
...         return kwargs['a'] + kwargs['b']
... 
>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> foo(**d)
3
 8
Author: styvane,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-20 11:31:22

Yo haría algo como esto:

def combine(function, dictionary):
    return function(**{key:value for key, value in dictionary.items()
                    if key in inspect.getargspec(function)[0]}
    )

Uso:

>>> def this(a, b, c=5):
...     print(a, b, c)
...
>>> combine(this, {'a': 4, 'b': 6, 'c': 6, 'd': 8})
4 6 6
>>> combine(this, {'a': 6, 'b': 5, 'd': 8})
6 5 5
 3
Author: zondo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-19 13:11:26

Esto todavía está modificando la función original, pero puede crear un bitbucket kwargs al final de la lista de argumentos:

def foo(a, b, **kwargs):
    return a + b

foo(**{
    'a': 5,
    'b': 8,
    'c': ''
}) # 13
 2
Author: Christian Mann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-20 05:25:55