Reputation: 7105
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
Upvotes: 2
Views: 427
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
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
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