PeanutsMonkey
PeanutsMonkey

Reputation: 7105

Why does the Python function not print a default value?

I am new to Python and am attempting to print a default value set for a parameter. My code is as follows;

# Functions

def shoppingcart(item='computer', *price):
    print item
    for i in price:
        print i

shoppingcart(100,200,300)

When I run the code, all I get are the values 100,200,300. I was expecting computer,100,200,300. What am I doing wrong?

EDIT

How do I achieve this in Python 2.7?

EDIT

I updated my code to

def shoppingcart(item='computer', *price):
    print item
    for i in price:
        print i

shoppingcart(item='laptop', 100,200,300)

however get the error

enter image description here

Upvotes: 2

Views: 427

Answers (3)

abarnert
abarnert

Reputation: 366223

What you want to do is impossible. You can never write this in Python:

shoppingcart(item='laptop', 100,200,300)

In either 2.x or 3.x, you will get an error:

SyntaxError: non-keyword arg after keyword arg

First, a little background:

A "keyword" argument is something like item='laptop', where you specify the name and value. A "positional", or "non-keyword" argument, is something like 100, where you just specify the value. You have to put the keyword args after all of the positional args whenever you call a function. So, you can write shoppingcart(100, 200, 300, item='laptop'), but you can't write shoppingcart(item='laptop', 100, 200, 300).

This makes a little more sense when you understand the way keyword arguments are handled. Let's take a very simple case, with a fixed set of parameters and no default values, to make everything easier:

def shoppingcart(item, price):
    print item, price

Whether I call this with shoppingcart('laptop', 200), shoppingcart(price=200, item='laptop'), or any other possibility, you can figure out how the arguments arrive in item and price in the function body. Except for shoppingcart(price=200, 'laptop'), Python can't tell what laptop is supposed to be—it's not the first argument, it's not given the explicit keyword item, so it can't be the item. But it also can't be the price, because I've already got that. And if you think it through, it's going to have the same problem any time I try to pass a keyword argument before a positional argument. In more complex cases, it's harder to see why, but it doesn't even work in the simplest case.

So, Python will raise a SyntaxError: non-keyword arg after keyword arg, without even looking at how your function was defined. So there's no way to change your function definition to make this work; it's just not possible.

But if you moved item to the end of the parameter list, then I could call shoppingcart(100, 200, 300, item='computer') and everything would be fine, right? Unfortunately, no, because you're using *args to soak up the 100, 200, 300, and you can't put any explicit parameters after a *args. As it turns out, there's no principled reason for this restriction, so they did away with it in Python 3—but if you're still using 2.7, you have to live with it. But at least there is a workaround.

The way around this is to use **kwargs to soak up all of the keyword arguments and parse them yourself, the same way you used *args to soak up all the positional arguments. Just as *args gets you a list with the positional arguments, **kwargs gets you a dict with each keyword argument's keyword mapped to its value.

So:

>>> def shoppingcart(*args, **kwargs):
...    print args, kwargs
>>> shoppingcart(100, 200, 300, item='laptop')
[100, 200, 300], {'item': 'laptop'}

If you want item to be mandatory, you just access it like this:

item = kwargs['item']

If you want it to be optional, with a default value, you do this:

item = kwargs.get('item', 'computer')

But there's a problem here: You only wanted to accept an optional item argument, but now you're actually accepting any keyword argument. If you try this with a simple function, you get an error:

>>> def simplefunc(a, b, c):
...     print a, b, c
>>> simplefunc(1, 2, 3, 4)
TypeError: simplefunc() takes exactly 3 arguments (4 given)
>>> simplefunc(a=1, b=2, c=3, d=4, e=5)
TypeError: simplefunc() got an unexpected keyword argument 'd'

There's a very easy way to simulate this with **kwargs. Remember that it's just a normal dict, so if you take out the expected keywords as you parse them, there should be nothing left over—if there is, the caller passed an unexpected keyword argument. And if you don't want that, you can raise an error. You could write that like this:

def shoppingcart(*prices, **kwargs):
    for key in kwargs:
        if key != 'item':
            raise TypeError("shoppingcart() got unexpected keyword argument(s) '%s' % kwargs.keys()[0]`)
    item = kwargs.get('item', 'computer')
    print item, prices

However, there's a handy shortcut. The dict.pop method acts just like dict.get, but in addition to returning the value to you, it removes it from the dictionary. And after you've removed item (if it's present), if there's anything left, it's an error. So, you can just do this:

def shoppingcart(*args, **kwargs):
    item = kwargs.pop('item', 'computer')
    if kwargs: # Something else was in there besides item!
        raise TypeError("shoppingcart() got unexpected keyword argument(s) '%s' % kwargs.keys()[0]`)

Unless you're building a reusable library, you can get away with something simpler than that whole if/raise thing and just do assert not kwargs; nobody will care that you get an AssertionError instead of a TypeError. That's what lqc's solution is doing.

To recap, it doesn't matter how you write your function; it can never be called the way you want. But if you're willing to move item to the end, you can both write it and call it in a reasonable way (even if it's not as simple or readable as the Python 3 version).

For some rare cases, there are other ways to go. For example, if prices always have to be numbers, and items are always strings, you can change your code to do something like this:

def shoppingcart(*prices):
    if prices and isinstance(prices[0], basestring):
        item = prices.pop(0)
    else:
        item = 'computer'
    print item
    for price in prices:
        print price

Now, you can do this:

>>> shoppingcart(100, 200, 300)
computer
100
200
300
>>> shoppingcart('laptop', 100, 200, 300) # notice no `item=` here
laptop
100
200
300

This is basically the kind of trick Python uses to implement slice(start=None, stop, step=None), and it is very occasionally useful in your code. However, it makes your implementation much more fragile, makes the behavior of your function much less obvious to the reader, and generally just doesn't feel Pythonic. So, it's almost always best to avoid this and just make the caller use real keyword arguments if that's what you want, with the restriction that they have to come at the end.

Upvotes: 2

Jochen Ritzel
Jochen Ritzel

Reputation: 107786

Simply put, default values are only used if there is no other way. Here you have item=100 and the other 2 values go into *price.

In Python3 you can write:

def shoppingcart(*prices, item='computer'): 
    print(item)
    print(prices)

shoppingcart(100,200,300)
shoppingcart(100,200,300, item='moon')

In Python2 you cannot have default arguments after *args so you have to find another way to call your functions.

Upvotes: 6

lqc
lqc

Reputation: 7348

For Python 2.x you'll have to use the ** syntax:

def shoppingcart(*prices, **kwargs):
   item = kwargs.pop("item", "computer")
   assert not kwargs, "This function only expects 'item' as keyword argument".
   print(item)
   print(prices)

Then you can pass it keyword arguments:

shoppingcart(100,200,300, item='moon')

(They always need to be after 'positional' arguments)

Upvotes: 3

Related Questions