Argumentos predeterminados con * args y ** kwargs

84

En Python 2.x (yo uso 2.7), ¿cuál es la forma correcta de usar los argumentos predeterminados con *argsy **kwargs?
Encontré una pregunta sobre SO relacionada con este tema, pero esa es para Python 3 :
llamar a una función de Python con * args, ** kwargs y argumentos opcionales / predeterminados

Allí, dicen que este método funciona:

def func(arg1, arg2, *args, opt_arg='def_val', **kwargs):
    #...

En 2.7, resulta en un SyntaxError. ¿Existe alguna forma recomendada de definir dicha función?
Lo hice funcionar de esta manera, pero supongo que hay una mejor solución.

def func(arg1, arg2, *args, **kwargs):
    opt_arg ='def_val'
    if kwargs.__contains__('opt_arg'):
        opt_arg = kwargs['opt_arg']
    #...
codeforester
fuente
Sigue siendo la explicación más sucinta que he encontrado: saltycrane.com/blog/2008/01/…
verbsintransit
4
Nunca llame explícitamente __contains__. Utilice siempre in: 'opt_arg' in kwargs. (Aún mejor: kwargs.get('opt_arg', 'def_val')como en la respuesta de mgilson).
nneonneo

Respuestas:

80

Simplemente coloque los argumentos predeterminados antes de *args:

def foo(a, b=3, *args, **kwargs):

Ahora, bse establecerá explícitamente si lo pasa como un argumento de palabra clave o el segundo argumento posicional.

Ejemplos:

foo(x) # a=x, b=3, args=(), kwargs={}
foo(x, y) # a=x, b=y, args=(), kwargs={}
foo(x, b=y) # a=x, b=y, args=(), kwargs={}
foo(x, y, z, k) # a=x, b=y, args=(z, k), kwargs={}
foo(x, c=y, d=k) # a=x, b=3, args=(), kwargs={'c': y, 'd': k}
foo(x, c=y, b=z, d=k) # a=x, b=z, args=(), kwargs={'c': y, 'd': k}

Tenga en cuenta que, en particular, foo(x, y, b=z)no funciona porque bse asigna por puesto en ese caso.


Este código también funciona en Python 3. Poner el argumento predeterminado después *args en Python 3 lo convierte en un argumento "solo de palabra clave" que solo se puede especificar por nombre, no por posición. Si desea un argumento de solo palabras clave en Python 2, puede usar la solución de @ mgilson.

neonneo
fuente
1
Sí, eso se debe a la ambigüedad de *args. Tu camino es correcto si quieres un argumento de solo palabras clave.
nneonneo
Lo acabo de responder en los comentarios anteriores. (¿Qué pasa si quiero llamar a la función con una b diferente, y también quiero agregar * args?)
De todos modos, antes de preguntar, probé esta solución, pero también encontré que solo funcionaba si después de definir opt_arg solo uso kwargs.
@nneonneo: Tengo tus ejemplos, pero de esta manera todavía no nos da la libertad de especificar argumentos predeterminados y * args al mismo tiempo, como lo permite Python 3.x, como se explica en el enlace; ¿lo hace?
5
No puede dejar un argumento predeterminado solo mientras también lo completa *args, no. Esta es la razón por la que se agregó la funcionalidad a Python 3. Normalmente, los argumentos predeterminados en Python 2 se especifican como algo obvio como 0 o Nonepara que puedan pasarse explícitamente.
nneonneo
55

La sintaxis en la otra pregunta es solo python3.x y especifica solo argumentos de palabras clave. No funciona en python2.x.

Para python2.x, lo haría popsin kwargs:

def func(arg1, arg2, *args, **kwargs):
    opt_arg = kwargs.pop('opt_arg', 'def_val')
mgilson
fuente
También especifica * args, antes del argumento predeterminado.
4

También podrías usar un decorador como este:

import functools
def default_kwargs(**defaultKwargs):
    def actual_decorator(fn):
        @functools.wraps(fn)
        def g(*args, **kwargs):
            defaultKwargs.update(kwargs)
            return fn(*args, **defaultKwargs)
        return g
    return actual_decorator

Entonces solo haz:

@default_kwargs(defaultVar1 = defaultValue 1, ...)
def foo(*args, **kwargs):
    # Anything in here

Por ejemplo:

@default_kwargs(a=1)
def f(*args, **kwargs):
    print(kwargs['a']+ 1)

f() # Returns 2
f(3) # Returns 4
Daniel Américo
fuente
1

Siguiendo bastante cerca de su enfoque de solución mientras intenta hacerlo más genérico y más compacto, sugeriría considerar algo como esto:

>>> def func(arg1, arg2, *args, **kwargs):
...     kwargs_with_defaults = dict({'opt_arg': 'def_val', 'opt_arg2': 'default2'}, **kwargs)
...     #...
...     return arg1, arg2, args, kwargs_with_defaults

>>> func('a1', 'a2', 'a3', 'a5', x='foo', y='bar')
('a1', 'a2', ('a3', 'a5'), {'opt_arg2': 'default2', 'opt_arg': 'def_val', 'y': 'bar', 'x': 'foo'})

>>> func('a1', 'a2', 'a3', 'a5', opt_arg='explicit_value', x='foo', y='bar')
('a1', 'a2', ('a3', 'a5'), {'opt_arg2': 'default2', 'opt_arg': 'explicit_value', 'y': 'bar', 'x': 'foo'})
yaccob
fuente
0

Otra forma de manejar Python 2.x:

def foo(*args, **kwargs):
    if 'kwarg-name' not in kwargs.keys():
        kwargs['kwarg-name'] = 'kwarg-name-default-value'
    return bar(*args, **kwargs)

Esto maneja el paso arbitrario *argsa la llamada subyacente a diferencia de la respuesta de @ nneonneo.

conner.xyz
fuente
1
Esto sería más claro si usara un código válido, es decir, en 'opt_arg'lugar de < kwarg-name >y en 'def_val'lugar de< kwarg-name-default-value >
wjandrea
0

Esta respuesta es una extensión de lo que sugirió Daniel Américo .

Este decorador asigna valores kwarg predeterminados si no están estrictamente definidos.

from functools import wraps

def force_kwargs(**defaultKwargs):
    def decorator(f):
        @wraps(f)
        def g(*args, **kwargs):
            new_args = {}
            new_kwargs = defaultKwargs
            varnames = f.__code__.co_varnames
            new_kwargs.update(kwargs)
            for k, v in defaultKwargs.items():
                if k in varnames:
                    i = varnames.index(k)
                    new_args[(i, k)] = new_kwargs.pop(k)
            # Insert new_args into the correct position of the args.
            full_args = list(args)
            for i, k in sorted(new_args.keys()):
                if i <= len(full_args):
                    full_args.insert(i, new_args.pop((i, k)))
                else:
                    break
            # re-insert the value as a key-value pair
            for (i, k), val in new_args.items():
                new_kwargs[k] = val
            return f(*tuple(full_args), **new_kwargs)
        return g
    return decorator

Resultado

@force_kwargs(c=7)
def f(a, b='B', c='C', d='D', *args, **kw):
    return a, b, c, d, args, kw
#                               a    b  c    d  args      kwargs
f('r')                      # 'r', 'B', 7, 'D',   (),         {}
f(1,2,3)                    #   1,   2, 7,   3,   (),         {}
f(1, 2, 3, b=3, c=9, f='F') #   1,   3, 9,   2, (3,), {'f': 'F'}

Variante

Si desea utilizar los valores predeterminados tal como están escritos en la definición de la función, puede acceder a los valores predeterminados del argumento utilizando f.func_defaults, que enumera los valores predeterminados. Tendría que hacerlo zipcon el final de f.__code__.varnamespara hacer coincidir estos valores predeterminados con los nombres de las variables.

AlexLoss
fuente