Hugo Sereno Ferreira
Hugo Sereno Ferreira

Reputation: 8631

Initialize default keyword args list in python

How can one define the initial contents of the keyword arguments dictionary? Here's an ugly solution:

def f(**kwargs):
    ps = {'c': 2}
    ps.update(kwargs)
    print(str(ps))

Expected behaviour:

f(a=1, b=2) => {'c': 2, 'a': 1, 'b': 2}
f(a=1, b=2, c=3) => {'c': 3, 'a': 1, 'b': 2}

Yet, I would like to do something a little bit more in the lines of:

def f(**kwargs = {'c': 2}):
    print(str(kwargs))

Or even:

def f(c=2, **kwargs):
    print(str(???))

Ideas?

Upvotes: 2

Views: 476

Answers (3)

Labrys Knossos
Labrys Knossos

Reputation: 393

First to address the issues with your current solution:

def f(**kwargs):
    ps = {'c': 2}
    ps.update(kwargs)
    print(str(ps))

This creates a new dictionary and then has to take the time to update it with all the values from kwargs. If kwargs is large that can be fairly inefficient and as you pointed out is a bit ugly.

Obviously your second isn't valid.

As for the third option, an implementation of that was already given by Austin Hastings

If you are using kwargs and not keyword arguments for default values there's probably a reason (or at least there should be; for example an interface that defines c without explicitly requiring a and b might not be desired even though the implementation may require a value for c).

A simple implementation would take advantage of dict.setdefault to update the values of kwargs if and only if the key is not already present:

def f(**kwargs):
    kwargs.setdefault('c', 2)
    print(str(kwargs))

Now as mentioned by the OP in a previous comment, the list of default values may be quite long, in that case you can have a loop set the default values:

def f(**kwargs):
    defaults = {
        'c': 2,
         ...
    }
    for k, v in defaults.items():
        kwargs.setdefault(k, v)
    print(str(kwargs))

A couple of performance issues here as well. First the defaults dict literal gets created on every call of the function. This can be improved upon by moving the defaults outside of the function like so:

DEFAULTS = {
    'c': 2,
     ...
}
def f(**kwargs):
    for k, v in DEFAULTS.items():
        kwargs.setdefault(k, v)
    print(str(kwargs))

Secondly in Python 2, dict.items returns a copy of the (key, value) pairs so instead dict.iteritems or dict.viewitems allows you to iterate over the contents and thus is more efficient. In Python 3, 'dict.items` is a view so there's no issue there.

DEFAULTS = {
    'c': 2,
     ...
}
def f(**kwargs):
    for k, v in DEFAULTS.iteritems():  # Python 2 optimization
        kwargs.setdefault(k, v)
    print(str(kwargs))

If efficiency and compatibility are both concerns, you can use the six library for compatibility as follows:

from six import iteritems

DEFAULTS = {
    'c': 2,
     ...
}
def f(**kwargs):
    for k, v in iteritems(DEFAULTS):
        kwargs.setdefault(k, v)
    print(str(kwargs))

Additionally, on every iteration of the for loop, a lookup of the setdefault method of kwargs needs to be performed. If you truly have a really large number of default values a micro-optimization is to assign the method to a variable to avoid repeated lookup:

from six import iteritems

DEFAULTS = {
    'c': 2,
     ...
}
def f(**kwargs):
    setdefault = kwargs.setdefault
    for k, v in iteritems(DEFAULTS):
        setdefault(k, v)
    print(str(kwargs))

Lastly if the number of default values is instead expected to be larger than the number of kwargs, it would likely be more efficient to update the default with the kwargs. To do this, you can't use the global default or it would update the defaults with every run of the function, so the defaults need to be moved back into the function. This would leave us with the following:

def f(**kwargs):
    defaults = {
        'c': 2,
         ...
    }
    defaults.update(kwargs)
    print(str(defaults))

Enjoy :D

Upvotes: 3

aghast
aghast

Reputation: 15310

Have you tried:

def f(c=2, **kwargs):
    kwargs['c'] = c
    print(kwargs)

Update

Barring that, you can use inspect to access the code object, and get the keyword-only args from that, or even all the args:

import inspect

def f(a,b,c=2,*,d=1,**kwargs):
    code_obj = inspect.currentframe().f_code
    nposargs = code_obj.co_argcount
    nkwargs = code_obj.co_kwonlyargcount
    localvars = locals()
    kwargs.update({k:localvars[k] for k in code_obj.co_varnames[:nposargs+nkwargs]})

    print(kwargs)

g=f

g('a', 'b')

Upvotes: 0

ShadowRanger
ShadowRanger

Reputation: 155438

A variation possible on your first approach on Python 3.5+ is to define the default and expand the provided arguments on a single line, which also lets you replace kwargs on the same line, e.g.:

def f(**kwargs):
    # Start with defaults, then expand kwargs which will overwrite defaults if
    # it has them
    kwargs = {'c': 2, **kwargs}
    print(str(kwargs))

Another approach (that won't produce an identical string) that creates a mapping that behaves the same way using collections.ChainMap (3.3+):

from collections import ChainMap

def f(**kwargs):
    # Chain overrides over defaults
    # {'c': 2} could be defined outside f to avoid recreating it each call
    ps = ChainMap(kwargs, {'c': 2})
    print(str(ps))

Like I said, that won't produce the same string output, but unlike the other solutions, it won't become more and more costly as the number of passed keyword arguments increases (it doesn't have to copy them at all).

Upvotes: 1

Related Questions