Chp 10: Classes

Ask Yourself

We've got ints. We've got floats. We don't have fractions. Ask yourself how we could get them with the Python we know.

Well, what's a fraction? It's two integers - numerator and denominator. How should we store a fraction then? A two-member list perhaps? Or a two-member tuple?  Let's use tuples. Below are two examples:

f1 = (1, 2)

f2 = (2, 3)

The first - a fraction named f1 - has numerator 1 and denominator 2; that is, it's the fraction 1/2; and f2 is of course the fraction 2/3.

How do we store multiple fractions? We could simply do what we did above and create each new fraction on its own line. But what if wanted to automate fractions creation? We'd need a collection data type for that. A list perhaps? Or a dictionary? Let's try a dictionary in which the keys are fraction names and the values are tuples. Like this:

fracs = {"f1": (1, 2), "f2": (2, 3), "f3": (-12, 15), "f4": (0, 1)}

Fractions are no good to us if we can't work with them - add, subtract, multiple, divide, compare, etc. How would we do all of that? Functions, functions, functions! Here's one:

def add_fracs(fdct, f1, f2):

    # f1 and f2 are fracs.

    # Assume that a LCM helper function has been written.

    lcm = LCM(fdct[f1][1], fdct[f2][1])

    numer = fdct[f1][0] * (lcm // fdct[f1][1]) + fdct[f2][0] * (lcm // fdct[f2][1])

    return (numer, lcm)

Here's how we'd use that and place the result in the fracs dictionary.

f5 = add_fracs(fracs, "f1", "f2")

fracs["f5"] = (7, 6)

Well, okay then. We can do it. We can create fractions and store them away. We can add them and store the result. No doubt we could write functions for all the other operations too. But that code we wrote above, especially the add_fracs function ... that's some ugly stuff.

Let me show you a better way.

What is a Class?

Classes are the better way.

Let's start with a definition. Here's what the official docs say. Paraphrased a bit.

Classes bundle data and functionality.

When we create a class, we create a new type of object; and as with built-in types, we may then create specific instances of that type. Let us call these objects. Objects have attributes. The values of an object's attributes at a moment in time is its state.  Objects also have methods.  These are functions that modify the state of an object.

Before we start to build code, let's pause and reflect on this for a moment. It might seem unfamiliar, but it isn't really. Consider we human beings. We are all of the same type - that's our species.  I'm Homo sapiens. You're Homo sapiens. We're all Homo sapiens. But we're not the same objects; that is, we're not the same human being. I'm over here.  You're over there. I'm old(ish). You're (probably) young. I have an uncontrolled addiction to dark chocolate and black coffee. You (probably) do not. These qualities that make me the human being that I am, these properties of me that serve to distinguish me from you, are my attributes; and the set of my attributes at a moment in time is my state. But of course my state does not remain the same. It changes. I walk for a bit and now I'm over there. I sit in the sun and my bald head burns. You insult my favorite author and I cry.  You buy me a cup of coffee and I smile. All these ways that I or you can change my state, all these actions that I or you can perform that alter me in some way, these are my methods. Methods are actions that change the state of an object.

So you understand now that this talk of types, objects, attributes, state and methods is really just common sense. All that's new here is the language. 

Class Definition

So let's discuss the syntax of classes. After we'll construct the Human class.

Class definitions all share a certain form. I'll lay it out. My example will be simple and typical. Expect complexity to grow later. 

A class definition begins as:

class Name():

Name is your choice. Capitalize please; that's usually done for class definitions. Parentheses are optional. I include them, but here at TF! we won't ever actually place anything inside them. (If you're curious what could go in there, Google "Python inheritance". I prefer the parentheses because classes are function-like in a certain respect; that are function-like in that we will call them. More about this in a bit.)

 What do we put in the class? Definitions! Typically function definitions, though we could assign values to variables too. Here's a simple example:

class Spam:

  eggs = 12

  def bacon(sausage):

    return sausage

How would be access those definitions. By our old friend the dot. So if we ran the program above, we could do this in the console:

>>> Spam.eggs

12

>>> Spam.bacon(12)

12

So, if we use classes like this, they're just bags of definitions. (Note that a function inside a class is typically called a method. So Spam.bacon(12) called the bacon method in the Spam class and sent it the input 12.) That could be convenient, of course. But classes can do so much more. They give us the ability to create objects of a new type. Let's see how that's done.

The Constructor Method

Near the top of the definitions in a class (often first), comes a very special method, the class constructor. It begins def __init__(self, arg1, arg2, ...).  (Note that the method must be spelled just like that - double underscore "init" double underscore; only if its spelled like that will Python know that it's the class constructor.) A call to the constructor creates and returns a new instance of the class. self is a reference to the object under construction. You don't send a value for self. Its value is set for you; or better, the constructor creates self. You do send a values for arg1, arg2, etc. These values will (typically)  become the initial attributes of self. (Note: we could if we wanted use a name other than self. But no one does. Ever. Don't you dare use another name.)

The class constructor (typically) continues like this:

    self.attribute1 = arg1

    self.attribute2 = arg2

    . . .

On the left of = we create attribute names. These are attribute1, attribute2, etc. On the right are the expressions that give the newly-minted attributes their initial values. (As always, the right expressions can be complex. (arg1 ** 2) / arg2, arg1[1:], str(arg2), etc.)

Think of attributes as variables attached to objects in the class. Objects carry their attributes around with them. Note that the values of an object's attributes need not be the same as the the values of the attributes of another object in that class. (You might recall that in the Euclid module, pen-type objects had various attributes, color among them; and the color attributes of two pens didn't have to be the same.)

Under the class constructor comes a set of function definitions. These are the class methods. (As I said above, functions contained with a class definition are called "methods".) Typically the first parameter in a class method is self.  Like this:

    def method1(self, arg1, arg2, ...):

        ...


    def method2(self, arg1, arg2, ...):

        ...

self is the name of the object on which the method is called. For instance, logos.goto(12, 12) (an example from the Euclid module), calls the goto method in a pen-type object named logos, and in that method, the value of self would be logos.

(Bear with me here. I know this is a ton of information to take in. But I do need to talk through the "theory" of classes here at the start. I promise that, by the time we're done, you'll have seen many examples. Those examples, I expect, will make clear what all this "theory" means.)

Makes Objects

So, let's say we've begun to write a class, and that we've written its constructor method (which, one again, is the __init__ method). What do we do with it?

Well, typically we first create instances of the class; that is, we create objects of the new type that the class defines. How?  We do so with a call to the class. Like this:

object_name = Class_Name(arg1, arg2, ...)

On the right, we called the class; notice the parentheses after the class name, and recall that in Python a name followed immediately by parentheses is always a call. A call to the class triggers a call to the __init__ method; I mean, when you call a class, Python looks for a __init__ method within it, and if it finds it, it calls it. arg1, arg2, etc. become the values of that method's parameters after self. (As I said, self is the name of the object under construction.)

So, let's say that we've created some objects, and now wish to call methods on them. How? With the almighty dot.

object_name.method(arg1, arg2, ... )

The object named by object_name is passed to the method; it is, as is sometimes said, the primary argument. It becomes the value of self. arg1, arg2, etc. are additional arguments. Note that methods need not have any arguments in addition to self; and if a method (call it "self_only") has only self for a parameter, it can be called with object_name.self_only() - nothing's in the parentheses.

A final note. If we wanted the value of an attribute associated with an object, we must the dot but no parentheses. So for instance if object named obj has an attributed named attr, obj.attr is the value of the attribute.

Example

So far I've described classes abstractly. So let's get concrete. Indeed let's create a Human class! (I let my driver's license guide me.) Here and below, the comments are crucial; it's there that I explain in detail what's happened. Read them!

class Human(): # So begins every class definition.


    # The __init__ method is the class constructor.

    # It makes human beings!

    def __init__(self, name, age, sex, hght, wght, eyes, hair):

        # A call to the Human class triggers a call to its __init__ method.

        # self is the object created.

        # The values of the parameters name, age, etc. are used to set

        # initial values for the attributes of a Human object.

        # On the left, after self., is the attribute name.

        # After the equals sign is the initial value of that attribute.

        self.name = name

        self.age = age

        self.sex = sex

        self.height = hght  # height is the attribute name; hght is the value passed to __init__

        self.weight = wght

        self.eye_color = eyes

        self.hair_color = hair


    # After come the Human methods.

    def __repr__(self):

        # Overload the built-in __repr__ method,

# which creates a string-type representation of an object.

        # The value returned will be output when we print a human.

        str_repr = "A human named " + self.name + "."

        return str_repr


    def speak(self):

        # a fruitless method

        print("Where's my coffee?")


    def age_in_seconds(self):

        # a fruitful method

        return self.age * 365 * 24 * 60 * 60

    

    def eats(self, calories):

        # a fruitless mutator

        # The attribute weight is altered but no value is returned.

# Not biologically accurate, of course.

        self.weight += calories // 1200  # 1 kg = 1200 calories


    def propositions(self, second):

        # self is the first human; second is the second human.

        if self.sex == "male" and second.sex == "female":

            return self.name + " initially rebuffs " + second.name + "."

        else:

            return self.name + " mumbles incoherently at " + second.name + "."

You will have noticed that, of the methods after __init__, one is spelled unlike the others. That's the __repr__ method of course. You will have noticed too that it's spelled similarly to __init__. Both begin and end with a double underscore. What's up with that? We know that a call to __init__ is triggered by a call to the class; that is, since __init__ is spelled as it is, Python knows to call it when the class is called. The __repr__ method works in a similar fashion; it gets called when we call certain other functions. What's other functions? print is one (and any other that asks for a string representation of a Human.) A line of the form print(expression) has Python create the string representation of the object named by expression and then place that string in the console. Thus if we print a Human, Python will look for a __repr__ method in the Human class, and if it finds it, it will use the string returned by the __repr__.

We'll see lots of double-underscore methods. They all have this in common: they teach Python what to do when another method (or class) is called. A call to a class triggers a call to its __init__ method. A call to print triggers a call to its __repr__ method.

You might wonder what Python would do if we attempt to print a Human but have not created a __repr__ method. Well, it won't crash. But we get something most unhelpful if we haven't written a __repr__.  Assume that we've created a Human named "Eve". Like this:  Eve = Human("Eve", 36, "female", 1.6, 54, "brown", "brown"). If we don't have a __repr__ method, print(Eve) will output something like:

<__main__.Human object at 0x7fd5a84074c0>

That's a memory location. Yuck. If  on the other hand we do have the __repr__ method above, we get:

A human named Eve.

Much better!

Methods like __init__ and __repr__ are called "dunder" methods, which is short for "double underscore". Python has many. You'll learn about more in the Vector class below and in the Fraction and Complex Number projects.

Mutability

If you read the comments in the code above (I specifically asked you too!), you'll recall that one of the methods was called a mutator. It was the eats method. It alters one of the attributes of a Human. I don't have much to say about that. But I did want to emphasize that this is how user-defined classes work. The values of attributes of objects in those classes can be changed, and this is mutation.

Make Humans. Make Them Do.

Let's make a few more Humans (I made Eve in the section above) and then make them do as we command. (Do classes make you feel as if you have god-like powers? They do to me.)

First comes the creation of Humans. A statement of the form  name = Human(arg1, arg2, ...) will trigger a call to the __init__ method; and __init__ will then return an object of the Human type, and that new Human will be bound to name.

# Create a pair of humans

Eve = Human("Eve", 36, "female", 1.6, 54, "brown", "brown")

Adam = Human("Adam", 36, "male", 1.8, 68, "brown", "brown")

Let's now have a few a method calls. Again, methods are functions bound to an object in a class definition. They are called by use of the dot - object.method(). The object on which the method is called is automatically sent as an argument. It becomes the value of self.

# Print the string representation of a Human built in the __repr__ method

print(Eve)

print(Adam)


Adam.speak()


print(Adam.propositions(Eve))

print(Eve.propositions(Adam))


print(Adam.weight)

Adam.eats(3000) # mutate Adam

print(Adam.weight)

The output:

A human named Eve.

A human named Adam.

Where's my coffee?

Adam initially rebuffs Eve.

Eve mumbles incoherently at Adam.

68

70

What did we do? We made human beings! How? With a call to the Human class, which we know is a call to its constructor. What did we do with the human beings we made? We issued orders to them. How? Method calls.

The Vector Class

Let's have a second example, a mathematical example. Let's create and manipulate vectors.

I mean then to create a Vector class. We know that when we create a class, we create a new type of object; and objects of that new type have attributes and they have methods. The attributes of an object define it; they make it the object that it is. The methods in this case will be the vector operations - addition, multiplication and a few others.

So, what is a vector? What attributes must we set when we summon a vector into existence? A vector is an ordered sequence of numbers. Let us agree to place that sequence between square parentheses. So for example (3, 2, 1) is a vector whose first, second and third members respectively are 3, 2 and 1. (Of course vectors look like lists. But they're not. We'll define operations on vectors that have that have no list analogues.)

What are the vector methods? 

(You'll find a few other methods defined in the code, but you'll find them easy to understand when you read the code. Which of course you'll do.)

That's the mathematics. How will all this get implemented in code? You'll see an __init__ function of course. That's the class constructor. It constructs instances of the class and sets their initial attributes, which will be the elements in the vector and the vector's length. (That second attribute wasn't really necessary, but it is convenient.)

Note that the __init__ method checks whether it's been sent a numeric sequence (which of course is what it needs to build a vector).  I do these by means of three helper methods that precede the class: namely isNum, isSeq and isNumSeq.  To determine whether an the argument to isNum is a number, I try to do something to it that can only be done to numbers, namely divide it by some number. To determine whether the argument to isSeq is indeed a sequence, I try to make an iterable from it with iter(); if it's some kind of sequence (say a list or a tuple), that will work, and it's not, it won't. (You needn't really worry what an iterable is. But if you're curious, Google it. All you really need to know that a call to  iter() succeeds when and only when its argument is a sequence.) To determine whether the argument to isNumSeq is indeed a numeric sequence, I first determine whether it is a sequence, and if it is, I then determine whether each of its elements is a number.

In addition to the __init__ method, you'll also see a number of other dunder methods - for instance, __add__, __mul__ and __eq__. What are these? They're like the __init__ and __repr__ that you already know. What was  __repr__ for? Its the method Python uses when its asked to produce the string representation of an object, for instance when print is called.  __add__, __mul__ and the rest behave similarily; Python uses them when certain functions are called. What functions? The functions +, * and ==. I mean that with those dunder methods in place, we'll be able to put a +, * or == between a pair of vectors and thus add them, multiply them or compare them. Isn't that cool? Java won't let you do that. It defines * for certain built-in types; it won't let you extend that definition to cover user-created types. But like always, Python lets you do what you want to do. Want to use * to multiply objects in a class you created? Python's gotcha covered!

Here's a word for you: "overload". In Python, the operators +, * and == are overloaded. That means that they can be used with objects in many classes. For instance, the expressions 'a' + 'b' and 1 + 2 both make perfect sense to Python though the operands (first 'a' and 'b', then 1 and 2) are of different types. By means of the dunder methods __add__, __mul__ and __eq__ we can extend that overloading; that is, we can load more into +, * and ==. We can teach Python what they mean for a class we've created.

Before I ask you to read the code below, I have one final (somewhat subtle) point to make. You'll see in the code below a __mul__ method. That's the one that will be used when we multiply a vector by a vector; for instance, if Python encounter v1 * v2, where v1 and v2 are vectors, it will use the __mul__ method contained in the Vector class to compute its value. But you'll also see a __rmul__ method. That's short for "right multiplication". What's it for? Well, you must understand that when Python decides which method to use when it encounters an expression that contains *, it looks in the class in which the left operand falls. But the problem here is that it makes perfect sense to multiply a scalar by a vector; that is, s1 * v1, where s1 is a scalar and v1 is a vector, makes perfect mathematical sense. But since the left operand (the s1) is not a vector (let's say it's a float), it will look in the float class for a method to compute float times vector. That will be (provisionally) catastrophic! For of course the float class knows nothing about vectors!

I said this would be only provisionally catastrophic. Why? Well, Python will try something in the case of float times vector. Once it's checked the float class for a method that tells it how to multiply by a vector (and check that will come up empty, because the float class knows nothing about our vector class), it will return to the vector class in search of a __rmul__ method; and if it finds it, it will use it.

I have one last point to make, a point about the order in which the arguments will be sent to __rmul__.  As I said, we end up at __rmul__ when we multiply a scalar by a vector, in that order. But that order will be flipped when we arrive at __rmul__; that is, the first argument will be the vector and the second argument will be the scalar. Note in the code below how I choose to name those arguments:

def __rmul__ (second, first): 

I chose second for my first parameter to mark the fact that it is actually the second, i.e. the right, argument in the line of code that triggered a call to __rmul__. That is, when __rmul__ evaluates the expression:

s1 * v1 

second gets the second argument - the v1 - as its first argument.

The same point can be made about all the double-underscore r methods - __radd__, __rtruediv__, etc. You'll use all these when you write the Complex and  Fraction classes. (Those are the chapter projects.)

Your task now is to read the code below. Read it carefully. Slowly. If you don't understand it, you can't write your own numeric classes.

import math


class Vector():


    def __init__(self, elems):

        # the class constructor, called when the class is called

        # creates and returns an object in the Vector class

        self.elems = elems # left "elems" is the attribute name, right "elems" is from the argument list

        self.size = len(elems)  # an attribute value can be computed inside __init__


    def head(self):

        # return the first element of a vector

        # return type: scalar

        return self.elems[0]


    def tail(self):

        # returns a vector with its head removed

        # return type: vector

        # Notice the call to the vector class; this creates a vector, which is then returned.

        return Vector(self.elems[1:]) # a Vector type object is created and returned


    def magnitude(self):

        # return type: scalar

        sumSquares = 0

        for elem in self.elems:

            sumSquares += elem ** 2

        return math.sqrt(sumSquares)



    # Addition is defined for pairs of vectors.

    # The return value is a new vector whose first element

    # is the sum of the first elements of the input vectors,

    # whose second elements is the sum of the second elements

    # of the input vectors, etc.


    def __add__(first, second):

        # first and second are vectors

        # return type: vector

        sumElems = []

        for i in range(first.size):

            sumElems.append(first.elems[i] + second.elems[i])

        return Vector(sumElems)


    # Multiplication is defined both for vector times int or float

    # (this is vector times scalar) and vector times vector.

    # If we have vector times int or float, every element of the vector is multiplied

    # by the int or float, and a vector is returned.

    # If we have vector times vector, we return the sum of the products of the elements

    # in the same position; that is, first element * first element + 

    # second * second + ... .


    # The __mul__ function overloads the * operator.

    # Thus statements such as v1 * v2 or v1 * s, where v1 and v2 are vectors

    # and s is a scalar, will call the __mul__ method.


    # Note that __mul__ handles all cases where the left operand is a vector.


    def __mul__(first, second):

        # first is vector, second is vector or scalar

        # return type: scalar if first and second are vectors, vector is second is a scalar

        

        # vector * scalar

        if isinstance(second, int) or isinstance(second, float):

            prodElems = []

            for i in range(first.size):

                prodElems.append(second * first.elems[i])

            return Vector(prodElems)

        

        # vector * vector

        prod = 0

        for i in range(first.size):

            prod += first.elems[i] * second.elems[i]

        return prod


    # For each of the __r methods like __radd__ and __rmul__, the first parameter

    # (named "second" below) binds to the right operand and the second parameter

    # (named "first" below) binds to the first.


    # Note that __rmul__ handles the case when the right operand

    # is a vector but the left operand is not.


    def __rmul__ (second, first):

# second will be a vector, so second * first will return

# in a call to __mul__

        return second * first


    # __eq__ overloads the == check for equality.

    # Thus a state like v1 == v2, where v1 an v2 are vectors,

    # calls __eq__.


    def __eq__(first, second):

        if first.size != second.size:

            return False

        for i in range(first.size):

            if first.elems[i] != second.elems[i]:

                return False

        return True


    def __repr__(self):

        strRep = ''

        for i in range(self.size):

            if i < self.size - 1:

                strRep += str(self.elems[i]) + ', '

            else:

                strRep += str(self.elems[i])

        return '<' + strRep + '>'


# three vectors

v1 = Vector((1, 2, 3))

v2 = Vector((4, 5, 6))

v3 = Vector((1, 2, 3))


# two scalars

s1 = 12

s2 = -3.2


print(v1)  # The return value of the __repr__ method is used here.

print(v2)


print(v1 == v2, v1 == v3)


print(v1.head())

print(v1.tail())


print(v2.magnitude())


print(v1 + v2)  # vector addition, a vector is returned

print(v1 * v2)  # vector multiplication, a scalar is returned


# The left operand determines the method called.

# Below v1 is a vector, so __mul__ in the vector class is called.

print(v1 * s1)  


# Again the left operand determines the method called.

# Below the left operand is a float, so Python attempts to call

# __mul__ in the float class. But the float class doesn't know

# how to multiply by a vector. So Python then looks for __rmul__

# in the vector class and uses it if it's defined.

# Note that in this case v2 will bind to first and s2 will bind to second.

print(s2 * v2)

The  output:

[1, 2, 3]

[4, 5, 6]

False True

1

[2, 3]

8.774964387392123

[5, 7, 9]

32

[12, 24, 36]

[-12.8, -16.0, -19.200000000000003]