Note: Click on "Kernel" > "Restart Kernel and Run All" in JupyterLab after finishing the exercises to ensure that your solution runs top to bottom without any errors. If you cannot run this file on your machine, you may want to open it in the cloud .
The exercises below assume that you have read the third part of Chapter 7.
The ...
's in the code cells indicate where you need to fill in code snippets. The number of ...
's within a code cell give you a rough idea of how many lines of code are needed to solve the task. You should not need to create any additional code cells for your final solution. However, you may want to use temporary code cells to try out some ideas.
In the "Function Definitions & Calls" section in Chapter 7 , we define the following function
product()
. In this exercise, you will improve it by making it more "user-friendly."
def product(*args):
"""Multiply all arguments."""
result = args[0]
for arg in args[1:]:
result *= arg
return result
The *
in the function's header line packs all positional arguments passed to product()
into one iterable called args
.
Q1: What is the data type of args
within the function's body?
< your answer >
Because of the packing, we may call product()
with an abitrary number of positional arguments: The product of just 42
remains 42
, while 2
, 5
, and 10
multiplied together result in 100
.
product(42)
product(2, 5, 10)
However, "abitrary" does not mean that we can pass no argument. If we do so, we get an IndexError
.
product()
Q2: What line in the body of product()
causes this exception? What is the exact problem?
< your answer >
In Chapter 7 , we also pass a
list
object, like one_hundred
, to product()
, and no exception is raised.
one_hundred = [2, 5, 10]
product(one_hundred)
Q3: What is wrong with that? What kind of error (cf., Chapter 1 ) is that conceptually? Describe precisely what happens to the passed in
one_hundred
in every line within product()
!
< your answer >
Of course, one solution is to unpack one_hundred
with the *
symbol. We look at another solution further below.
product(*one_hundred)
Let's continue with the issue when calling product()
without any argument.
This revised version of product()
avoids the IndexError
from before.
def product(*args):
"""Multiply all arguments."""
result = None
for arg in args:
result *= arg
return result
product()
Q4: Describe why no error occurs by going over every line in product()
!
< your answer >
Unfortunately, the new version cannot process any arguments we pass in any more.
product(42)
product(2, 5, 10)
Q5: What line causes troubles now? What is the exact problem?
< your answer >
Q6: Replace the None
in product()
above with something reasonable that does not cause exceptions! Ensure that product(42)
and product(2, 5, 10)
return a correct result.
Hints: It is ok if product()
returns a result different from the None
above. Look at the documentation of the built-in sum() function for some inspiration.
def product(*args):
"""Multiply all arguments."""
result = ...
for arg in args:
result *= arg
return result
product(42)
product(2, 5, 10)
Now, calling product()
without any arguments returns what we would best describe as a default or start value. To be "philosophical," what is the product of no numbers? We know that the product of one number is just the number itself, but what could be a reasonable result when multiplying no numbers? The answer is what you use as the initial value of result
above, and there is only one way to make product(42)
and product(2, 5, 10)
work.
product()
Q7: Rewrite product()
so that it takes a keyword-only argument start
, defaulting to the above default or start value, and use start
internally instead of result
!
Hint: Remember that a keyword-only argument is any parameter specified in a function's header line after the first and only *
(cf., Chapter 2 ).
def product(*args, ...):
"""Multiply all arguments."""
...
...
return ...
Now, we can call product()
with a truly arbitrary number of positional arguments.
product(42)
product(2, 5, 10)
product()
Without any positional arguments but only the keyword argument start
, for example, start=0
, we can adjust the answer to the "philosophical" problem of multiplying no numbers. Because of the keyword-only syntax, there is no way to pass in a start
number without naming it.
product(start=0)
We could use start
to inject a multiplier, for example, to double the outcomes.
product(42, start=2)
product(2, 5, 10, start=2)
There is still one issue left: Because of the function's name, a user of product()
may assume that it is ok to pass a collection of numbers, like one_hundred
, which are then multiplied.
product(one_hundred)
Q8: What is a collection? How is that different from a sequence?
< your answer >
Q9: Rewrite the latest version of product()
to check if the only positional argument is a collection type! If so, its elements are multiplied together. Otherwise, the logic remains the same.
Hints: Use the built-in len() and isinstance()
functions to check if there is only one positional argument and if it is a collection type. Use the abstract base class
Collection
from the collections.abc module in the standard library
. You may want to re-assign
args
inside the body.
import collections.abc as abc
def product(*args, ...):
"""Multiply all arguments."""
...
...
...
...
return ...
All five code cells below now return correct results. We may unpack one_hundred
or not.
product(42)
product(2, 5, 10)
product()
product(one_hundred)
product(*one_hundred)
Side Note: Above, we make product()
work with a single collection type argument instead of a sequence type to keep it more generic: For example, we can pass in a set
object, like {2, 5, 10}
below, and product()
continues to work correctly. The set
type is introducted in Chapter 9 , and one essential difference to the
list
type is that objects of type set
have no order regarding their elements. So, even though [2, 5, 10]
and {2, 5, 10}
look almost the same, the order implied in the literal notation gets lost in memory!
product([2, 5, 10]) # the argument is a collection that is also a sequence
product({2, 5, 10}) # the argument is a collection that is NOT a sequence
isinstance({2, 5, 10}, abc.Sequence) # sets are NO sequences
Let's continue to improve product()
and make it more Pythonic. It is always a good idea to mimic the behavior of built-ins when writing our own functions. And, sum() , for example, raises a
TypeError
if called without any arguments. It does not return the "philosophical" answer to adding no numbers, which would be 0
.
sum()
Q10: Adapt the latest version of product()
to also raise a TypeError
if called without any positional arguments!
def product(*args, ...):
"""Multiply all arguments."""
...
...
...
...
...
...
return ...
product()