Programming 1, 2020-1
Week 8: Notes

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

And in these chapters of Introducing Python:

Note that the file input/output methods we discussed are listed on our library quick reference page.

Here are some additional notes.

magic methods

We have already seen how we can write classes in Python and given them named methods. Sometimes we'd like to customize how instances of our classes will behave in ways that go beyond just calling methods with names. For example, we may wish to specify how our classes work with operators such as +, -, or *, or how instances of our classes are represented as strings. Python provides a variety of so-called magic methods for this purpose.

Actually we've already seen one magic method, which is the __init__ method that initializes a new object. All other magic methods also have special names that begin and end with two underscores.

We can customize the behavior of operators using these magic methods:

Many more similar operators exist; you can read about them in the official Python reference. But the ones listed here are sufficient for our purposes for now.

To see how these are used, let's write a class Vector that can represent a vector with an arbitrary number of elements:

class Vector:
    def __init__(self, *a):
        self.a = a

We've already seen that a parameter such as "*a" allows a function or method to accept an arbitrary number of arguments, which are gathered into a single tuple. So our initializer sets the attribute 'a' to hold a tuple of values in the vector:

>>> v = Vector(2.0, 4.0, 5.0)
>>> v.a
(2.0, 4.0, 5.0)

We will now implement a magic method __add__ that allows the operator + to combine instances of our Vector class:

    def __add__(self, other):
        b = []
        assert len(self.a) == len(other.a), 'vectors must have same dimension'
        
        for i in range(len(self.a)):
            b.append(self.a[i] + other.a[i])
        
        return Vector(*b)
    

Now we can add Vector objects using +:

$ py -i vector.py 
>>> v = Vector(2.0, 4.0, 5.0)
>>> w = Vector(1.0, 2.0, 3.0)
>>> z = v + w
>>> z.a
(3.0, 6.0, 8.0)

Behind the scenes, the '+' operator just calls the magic method, and in fact we can call it directly if we like:

>>> v.__add__(w)
[3.0 6.0 8.0]

We could similarly implement a method __sub__ that will allow the – operator to subtract two Vectors, or a method __mul__ that causes the * operator to compute the dot product of two Vectors.

__repr__

Consider this simple Point class:

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

When we ask Python to print out an instance of a class such as this, by default we get a string representation that is not very informative:

>>> p = Point(22, 33)
>>> p
<__main__.Point object at 0x7f8609c3bd30>

Often when we write a class we'd like to customize how Python will represent our class instances as strings. The magic method __repr__ will let us do that.

Let's add a __repr__ method to the Point class to generate a nice string representation:

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

Now a Point will print nicely:

>>> p = Point(22, 33)
>>> p
(22, 33)

Let's now enhance the Vector class with a __repr__ method:

   def __repr__(self):
        vals = [str(x) for x in self.a]
        return '[' + ' '.join(vals) + ']'

Now Vectors will print nicely as well:

>>> v = Vector(2.0, 4.0, 5.0)
>>> v
[2.0 4.0 5.0]