Programming 1, 2021-2
Week 7: Notes

Some of today's topics are covered in these sections of Think Python:

Here are some additional notes.

default parameter values

When we define a function in Python, we can specify default parameter values that are used if the caller doesn't specify values for these parameters. For example:

def foo(x, y = 4, z = 6):
    return x + y + z

>>> foo(1, 2, 3)
6
>>> foo(1, 2)
9
>>> foo(1)
11

In a function declaration, parameters with default values must appear at the end of the parameter list.

Many Python functions in the standard library have parameters with default values. For example, the pop(i) method removes a value at a given index i in a list, and returns the value. If the parameter 'i' is not specified, it defaults to -1, meaning that it will remove the value at the end of the list:

>>> l = [22, 44, 66, 88, 110]
>>> l.pop(3)
88
>>> l
[22, 44, 66, 110]
>>> l.pop()
110

You should be aware of one danger in declaring default parameter values. When Python sees a function declaration with default values, it evaluates each of those values to an object which is reused on all invocations of the function. That leads to this surprising behavior:

def add(x, y, l = []):    # l defaults to an empty list
    l.append(x)
    l.append(y)
    return l

>>> add(3, 5, [7, 8])
[7, 8, 3, 5]
>>> add(3, 5)
[3, 5]
>>> add(10, 11)
[3, 5, 10, 11]         # unexpected: 3 and 5 are present in the list!

You can avoid that behavior by using an immutable value such as None as the default:

def add(x, y, l = None):
    if l == None:
        l = []
    l.append(x)
    l.append(y)
    return l

Now the function behaves as you might expect:

>>> add(3, 5)
[3, 5]
>>> add(10, 11)
[10, 11]

keyword arguments

When you call any function in Python, you may optionally specify parameter names when you provide arguments. An argument with a name is called a keyword argument.

For example, consider this function:

def foo(x, y, z):
    return 100 * x + 10 * y + z

We may call it in any of the following ways:

>>> foo(3, 4, 5)
345
>>> foo(x = 3, y = 4, z = 5)
345
>>> foo(z = 5, y = 4, x = 3)
345
>>> foo(3, y = 4, z = 5)
345
>>> foo(3, z = 5, y = 4)
345

Notice that keyword arguments may appear in any order in a function call. However, they must appear after any arguments without names:

>>> foo(y = 4, z = 5, 3)
  File "<stdin>", line 1
    foo(y = 4, z = 5, 3)
                       ^
SyntaxError: positional argument follows keyword argument

If a function's parameters have default values, when we call the function we may to provide arguments for only some parameters. We can use names to indicate which argument(s) we are providing:

def foo(x = 8, y = 8, z = 8):
    return 100 * x + 10 * y + z

>>> foo(y = 2)
828

More generally, sometime it's good practice to specify parameter names in a function call to make it more readable, so that the meaning of each argument is clear.

variable numbers of arguments

We may sometimes wish to write a function that can take a variable number of arguments. For example, we may wish to write a function that returns the average of all its arguments, no matter how many there are:

>>> avg(1.0, 3.0, 8.0)
4.0
>>> avg(2.0, 4.0, 6.0, 8.0, 10.0)
6.0

We may write this function using a parameter preceded by the character '*', which means that the parameter should gather all of its arguments into a tuple:

def avg(*args):
    return sum(args) / len(args)

When we make the call 'avg(1.0, 3.0, 8.0)', inside the function the variable 'args' has the value (1.0, 3.0, 8.0), which is a tuple of 3 values. (Recall that the sum() and len() functions work on tuples just as they do on lists and other sequences).

A parameter preceded by '*' can have any name, but the name 'args' is conventional in Python.

As another example, here's a function that returns a length of a vector, whose components are specified as individual arguments to the function:

def vec_len(*args):
    return math.sqrt(sum([x * x for x in args]))

>>> vec_len(3.0, 4.0)
5.0

Conversely, when we call a function we may sometimes have a list (or other sequence) that contains the arguments we'd like to pass. In this case, we can specify '*' before an argument to specify that Python should explode its values into separate arguments. For example, consider this function:

def foo(x, y, z, q):
    return x + y + z + q + 10

When we call it, we may specify its arguments individually, or by exploding them from a list:

>>> foo(10, 20, 30, 40)
110
>>> l = [10, 20, 30, 40]
>>> foo(*l)
110

More commonly, this situation occurs when a function accepts a variable number of arguments, and we want to pass arguments from a list. For example, calling the vec_len() function we defined above:

>>> l = [3.0, 4.0]
>>> vec_len(*l)
5.0

sets

A set in Python is a useful built-in data structure that represents a mutable set of values. For example, here is a set that initially contains three integers:

>>> s = {3, 5, 10}

We may add a value to a set using the add() method. If the value is already present, the call does nothing:

>>> s.add(4)
>>> s
{10, 3, 4, 5}
>>> s.add(4)
>>> s
{10, 3, 4, 5}

The remove() method removes a value from a set. If it's not present, it will report an error:

>>> s.remove(5)
>>> s
{10, 3, 4}
>>> s.remove(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 5

discard() is similar, but will simply do nothing it a value is not present:

>>> s = {10, 3, 4}
>>> s.discard(3)
>>> s
{10, 4}
>>> s.discard(3)

The 'in' operator tests whether a value is present in a set:

>>> s = { 'red', 'green', 'yellow', 'turquoise' }
>>> 'yellow' in s
True
>>> 'purple' in s
False

All of these operations are efficient: add(), remove(), discard(), and 'in' will run in O(1) on average. For this reason, a set is sometimes a better choice of data structure than a list, especially in situations when we want to be able to quickly test whether an element is present.

However, be aware that sets are unordered. In other words, the values in a set are not in any particular order, and you cannot access values by index:

>>> s = { 'red', 'green', 'yellow', 'turquoise' }
>>> s[2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'set' object is not subscriptable

Python will print a set's values in an arbitrary order, unrelated to the order in which values were added:

>>> s = set()
>>> s.add(5)
>>> s.add(2)
>>> s.add(10)
>>> s
{2, 10, 5}

As you can see above, the function set() called with no arguments creates an empty set. (Do not attempt to create an empty set by writing '{}'. That is an empty dictionary, not a set. We will discuss dictionaries below.)

Sets are iterable, meaning that you can use a 'for' statement to loop over a set's values (in some arbitrary order):

>>> s = { 'red', 'green', 'yellow', 'turquoise' }
>>> for c in s:
...   print(c)
... 
green
turquoise
red
yellow

Note that every value in a set must be immutable. So you cannot create a set of lists, for example:

>>> s = { [1, 2, 3], [6, 5, 8], [10, 11, 12] }
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

However, a set of tuples will work fine, since tuples are immutable:

>>> s = { (1, 2, 3), (6, 5, 8), (10, 11, 12) }
>>> s
{(6, 5, 8), (1, 2, 3), (10, 11, 12)}
>>> 

Python provides handy operators for performing Boolean operations on sets. The '&', '|', and '-' operators compute the intersection, union, or difference of two sets:

>>> s = { 5, 8, 2, 10, 3, 6, 12 }
>>> t = { 8, 77, 3, 14, 12 }
>>> s & t
{8, 3, 12}
>>> s | t
{2, 3, 5, 6, 8, 10, 12, 77, 14}
>>> s - t
{2, 10, 5, 6}

The set operators 's <= t' and 's >= t' test whether s is a subset or superset of t, respectively:

>>> s = { 2, 4, 6 }
>>> t = { 1, 2, 3, 4, 5, 6, 8 }
>>> s <= t
True
>>> t <= s
False

For more information about built-in operators and functions that work on sets, see our quick library reference.

set comprehensions

A set comprehension is similar to a list comprehension, but collects values into a set, not a list.

For example, suppose that we have a set s of integers. We'd like to add one to each integer, and collect the resulting values into a new set t. We can perform this easily using a set comprehension:

>>> s = {7, 9, 100}
>>> t = {x + 1 for x in s}
>>> t
{8, 10, 101}

dictionaries

A dictionary is a built-in Python data type that represents a mutable map from keys to values. Dictionaries are quite convenient, and we will use them very often.

Here's a dictionary that maps the names of some animals in English to their Czech equivalents:

>>> d = {'cat' : 'kočka', 'dog' : 'pes', 'guinea pig' : 'morče'}

The keys in this dictionary are the strings 'cat', 'dog' and 'guinea pig'. The corresponding values are 'kočka', 'pes' and 'morče'. Notice that each key and its value are separated by a colon (':') in the declaration above.

We may look up the value for any key:

>>> d['dog']
'pes'
>>> d['guinea pig']
'morče'

We can use an assignment to add a new key/value pair to the dictionary:

>>> d['cow'] = 'kráva'
>>> d
{'cat': 'kočka', 'dog': 'pes', 'guinea pig': 'morče', 'cow': 'kráva'}

We may use the same syntax to replace the value at any key:

>>> d['cat'] = 'gato'
>>> d
{'cat': 'gato', 'dog': 'pes', 'guinea pig': 'morče', 'cow': 'kráva'}

The 'del' statement will delete a key and its value from a dictionary:

>>> del d['dog']
>>> d
{'cat': 'gato', 'guinea pig': 'morče', 'cow': 'kráva'}

The 'in' operator tests whether a key is present in a dictionary:

>>> 'guinea pig' in d
True
>>> 'horse' in d
False

All of these operations are efficient. Looking up a key's value, replacing a key's value, deleting a key, and testing for a key will all run in O(1) on average.

Notice that all keys in a dictionary are unique. Values, however, may be repeated. Also note that dictionaries are indexed by key, but not by value. In other words, you can quickly look up the value for any key, but if you want to know whether a particular value is present in the dictionary, you must scan the entire dictionary, which takes O(N).

Python provides methods that let us conveniently access the keys and values in a dictionary. d.keys() returns an iterable object containing all keys in the dictionary. You can iterate over this object using 'for', or convert it to a list:

>>> d = {'cat' : 'kočka', 'dog' : 'pes', 'guinea pig' : 'morče'}
>>> for k in d.keys():
        print(k)
cat
dog
guinea pig
>>> list(d.keys())
['cat', 'dog', 'guinea pig']

Similarly, d.values() lists all values in a dictionary. Another helpful method is d.items(), which is an iterable of key-value pairs:

>>> list(d.items())
[('cat', 'kočka'), ('dog', 'pes'), ('guinea pig', 'morče')]

Recall that in a 'for' statement we may use pattern matching to assign to multiple variables on each loop iteration:

>>> for (x, y) in [(1, 2), (3, 4), (5, 6)]:
         print(x + y)
3
7
11

And so we may conveniently use pattern matching to loop over the keys and values of a dictionary simultaneously:

>>> d
{'cat': 'kočka', 'dog': 'pes', 'guinea pig': 'morče'}
>>> for key, value in d.items():
        print(f'key = {key}, value = {value}')
key = cat, value = kočka
key = dog, value = pes
key = guinea pig, value = morče

Note that dictionary keys, like values in a set, must be immutable. So you can't create a dictionary whose keys are lists:

>>> d = { [1, 2, 3]: 7, [5, 3, 2]: 8 }
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

However, tuples will work fine as dictionary keys:

>>> d = { (1, 2, 3): 7, (5, 3, 2): 8 }

See our quick library reference for more information about built-in operators and functions that work on dictionaries.