Week 6: 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 digit_sum(x, y, z):
    return 100 * x + 10 * y + z

We may call it in any of the following ways:

>>> digit_sum(3, 4, 5)
345
>>> digit_sum(x = 3, y = 4, z = 5)
345
>>> digit_sum(z = 5, y = 4, x = 3)
345
>>> digit_sum(3, y = 4, z = 5)
345
>>> digit_sum(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:

>>> digit_sum(y = 4, z = 5, 3)
  File "<stdin>", line 1
    digit_sum(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 want to provide arguments for only some parameters. We can use names to indicate which argument(s) we are providing:

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

>>> digit_sum2(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.

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 add(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:

>>> add(10, 20, 30, 40)
110
>>> l = [10, 20, 30, 40]
>>> add(*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 avg() function we defined above:

>>> l = [3.0, 4.0]
>>> avg(*l)
3.5

Objects and classes

In Python and many other object-oriented languages, we may define our own data types, which are called classes. After we define a class, we may create instances of the class, which are called objects. An object has a set of attributes, which are data that belongs to the object. A class defines methods which may run on instances of the class.

As a first example, let's create a class Point. This is the smallest possible class definition in Python:

class Point:
    pass

In Python, the 'pass' statement does nothing. It's needed here, since a class definition may not be completely empty.

We may now create objects which are instances of class Point, and assign attributes to them:

>>> p = Point()
>>> p.x = 3
>>> p.y = 4
>>> q = Point()
>>> q.x = 10
>>> q.y = 20
>>> p.x
3
>>> q.y
20

Above, the function Point() is a constructor function that we can call to create a Point object.

However, typically when we write any class we write an initializer method that takes the arguments (if any) that are passed to the constructor, and uses them to initialize the object, typically by creating attributes. For example:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

In Python, the name __init__ is special: it means that this method is an initializer, and will run automatically when a new instance of this class is created.

Every method has a parameter list that begins with a parameter that receives the object on which the method was invoked. Traditionally in Python this parameter is called 'self' (though actually it may have any name). In an initializer method, this parameter receives the object that is being created.

Let's create a couple of Point objects using this new initializer:

>>> p = Point(3, 4)
>>> q = Point(10, 20)
>>> p.x
3
>>> q.y
20

Notice in this example that when we call the constructor, we pass only two arguments, but the parameter list in __init__ has three parameters. That's because 'self' is an extra parameter that is passed automatically.

Let's add a few methods to the Point class. It will now look like this:

from math import sqrt

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # Return the distance from this point to the origin.
    def from_origin(self):
        return sqrt(self.x ** 2 + self.y ** 2)

    # Return true if this point is the origin (0, 0).
    def is_origin(self):
        d = self.from_origin()
        return d == 0

    # Return the distance between this point and another point q.
    def distance(self, q):
        return sqrt((self.x - q.x) ** 2 + (self.y - q.y) ** 2)

Notice that

Here are two more classes that we wrote in the lecture:

class Line:
    def __init__(self, p, q):     # create a line from p to q
        self.p = p
        self.q = q

    def length(self):
        return self.p.distance(self.q)


# a vector of any dimension
class Vector:
    def __init__(self, *args):
        self.a = args

    def dims(self):
        return len(self.a)

    def length(self):
        s = 0
        for x in self.a:
            s += x * x
        return sqrt(s)

    # Return the dot product of self and w.
    def dot(self, w):
        assert len(self.a) == len(w.a)

        s = 0
        for i in range(len(self.a)):
            s += self.a[i] * w.a[i]
        
        return s

The __repr__ method

Above, we learned about __init__, which is a method with a special name that Python recognizes and which affects an object's behavior in a certain way. Python actually recognizes many different special method names, all of which begin and end with two underscores. Methods with these names are often called magic methods.

Another magic method in Python is called __repr__. If this method is defined in a class, Python calls it automatically whenever it needs to generate a string representation of an object. For example, this happens when you print out an object in the interactive Python console. By default, the string representation is just a blob of text with an ugly hexadecimal number:

>>> p = Point(3, 4)
>>> q = Point(10, 20)
>>> l = Line(p, q)
>>> p
<__main__.Point object at 0x7f173b1aa8b0>
>>> l
<__main__.Line object at 0x7f173b1aa7c0>

Let's add __repr__ methods to the Point and Line class to define a nicer string representation for these classes:

    class Point:
        ...
        def __repr__(self):
            return f'({self.x}, {self.y})'

    class Line
        ...
        def __repr__(self):
            return f'{self.p} – {self.q}'

Now Point and Line objects will print more nicely:

>>> p = Point(3, 4)
>>> q = Point(10, 20)
>>> l = Line(p, q)
>>> p
(3, 4)
>>> l
(3, 4) - (10, 20)

Notice that in our __repr__ method in the Line class, we wrote 'self.p' and 'self.q' in curly braces. In this situation, Python needs to convert self.p and self.q to strings, so it will call the __repr__ method of the Point class to perform that task.

Assertions

In Python, the assert statement checks that a given condition is true. If it is false, the program will fail with an AssertionError. For example:

assert 0.0 <= prob <= 1.0

You may add an optional string which will be printed if the assertion fails:

assert 0.0 <= prob <= 1.0, 'probability must be between 0.0 and 1.0'

You can use assertions to verify conditions that should always be true unless there is a bug in the code. If such a condition is false, it is best to find out about it right away, rather than continuing to run and producing an incorrect result later, which may be difficult to debug.

You may also wish to use assertions to verify that arguments passed to a function are valid. For example:

# Compute the average value of numbers in list a

def avg(a):
    assert len(a) > 0, 'list must be non-empty'
    return sum(a) / len(a)