Specifying Keyword Arguments with * args and ** kws

I found behavior in Python that puzzled and annoyed me, and I was wondering if I was wrong.

I have a function that needs to take an arbitrary number of arguments and keywords, but also needs to have some default keywords that make up its actual interface:

def foo(my_keyword0 = None, my_keyword1 = 'default', *args, **kws):
    for argument in args:
        print argument

      

The problem is, if I try to call foo(1, 2, 3)

, I only get a printout for 3, and the values ​​1 and 2 override the keyword arguments.

On the other hand, if I try to move my keywords after *args

or after **kws

, it will throw a syntax error. The only solution I found in this problem is to extract the keyword arguments from **kws

and set them to default values:

def foo(*args, **kws):
    my_keyword0 = None if 'my_keyword0' not in kws else kws.pop('my_keyword0')
    my_keyword0 = 'default' if 'my_keyword1' not in kws else kws.pop('my_keyword1')
    for argument in args:
        print argument

      

This is awful because it makes me add meaningless code and because the function signature becomes harder to understand - you should actually read the function code, not just look at its interface.

What am I missing? Isn't there a better way to do this?

+3


source to share


3 answers


Function arguments with default values ​​are still positional arguments, and therefore the result you see is correct. When you supply a default value for a parameter, you are not creating a keyword argument. The default values ​​are simply used when parameters are not supplied by the function call.

>>> def some_function(argument="Default"):
...     # argument can be set either using positional parameters or keywords
...     print argument
... 
>>> some_function()    # argument not provided -> uses default value
Default
>>> some_function(5)    # argument provided, uses it
5
>>> some_function(argument=5)    # equivalent to the above one
5
>>> def some_function(argument="Default", *args):
...     print (argument, args)
... 
>>> some_function()   #argument not provided, so uses the default and *args is empty
('Default', ())
>>> some_function(5)   # argument provided, and thus uses it. *args are empty
(5, ())
>>> some_function(5, 1, 2, 3)   # argument provided, and thus uses it. *args not empty
(5, (1, 2, 3))
>>> some_function(1, 2, 3, argument=5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: some_function() got multiple values for keyword argument 'argument'

      

Pay attention to the last error message: as you can see what is 1

assigned argument

and then python re-discovered keyword

by referencing argument

, and thus raised the error. *args

are assigned only after all possible positional arguments have been assigned.

In python2, it is not possible to define a meaning only for a keyword other than using**kwargs

. As a workaround, you can do something like:



def my_function(a,b,c,d,*args, **kwargs):
    default_dict = {
        'my_keyword1': TheDefaultValue,
        'my_keyword2': TheDefaultValue2,
    }
    default_dict.update(kwargs)    #overwrite defaults if user provided them
    if not (set(default_dict) <= set('all', 'the', 'possible', 'keywords')):
        # if you want to do error checking on kwargs, even though at that
        # point why use kwargs at all?
        raise TypeError('Invalid keywords')
    keyword1 = default_dict['keyword1']
    # etc.

      

In python3, you can define keyword-only arguments:

def my_function(a,b,c,*args, keyword, only=True): pass
    # ... 

      

Note that the keyword does not mean that it should have a default value.

+6


source


In Python, non-keyword arguments cannot appear after a keyword argument, so the signature of the function you are trying to use is not possible:

foo(my_keyword0="baa", my_keyword1="boo", 1, 2, 3, bar="baz", spam="eggs")  # won't work

      

If you think about it, there are very good reasons for this limitation (hint: keyword arguments can be presented in any order, and positional arguments ... well, positional ones)

From the documentation :

In a function call, keyword arguments must follow positional arguments. All keyword arguments passed must match one of the arguments accepted by the function (for example, actor is not a valid argument for the parrot function), and their order is not important. This also includes optional arguments

*args

and **kwargs

contain arguments that do not match the existing formal parameter, so once you have specified spam=None

in the argument list, it will no longer be passed **kwargs

:



When a final formal parameter of the form name is present , it receives a dictionary (see Collation Types - dict) containing all the ** keyword arguments except those matching the formal parameter. This can be combined with a formal parameter of the form * name (described in the next subsection), which receives a tuple containing positional arguments outside of the official parameter list. (* name must be specified before ** name.)

The closest approximation to your (impossible) syntax would be something like

def foo(my_keyword0=None, my_keyword1='default', numbers=None, **kwargs)

      

which you would use like

foo(my_keyword0="bar", my_keyword1="baz", numbers=(1,2,3), spam='eggs')
foo("bar", "baz", numbers=(1,2,3))
foo("bar", "baz", (1,2,3))
foo("bar", "baz", (1,2,3), spam="eggs")
foo(numbers=(1,2,3))
foo(spam="eggs")
etc.

      

which could possibly be more readable and less unexpected than your original idea.

+3


source


def foo(my_keyword0 = None, my_keyword1 = 'default', *args, **kws):
    all_args = [my_keyword0, my_keyword1] + args + kws.values()
    for argument in all_args:
        print argument

      

0


source







All Articles