Introduction to classes

Our primary goal for the first half of the course is to introduce you to all of the core elements of the Python language. We have one last major element of the language to cover in that survey - classes. These lecture notes will serve as your first introduction to this concept.

In Python a class is a structure composed of both member variables and methods. A member variable is simply a variable embedded in the class structure. A method is a function embedded in the class structure. Methods are typically designed to work with the member variables.

You have already worked with classes and methods

Many of the built-in data types in Python are classes. Examples include the String class and List class. We have already seen many examples of methods being used with these classes. You can tell that you are using a method when you see code that looks like

<object variable>.<method name>(<parameters>)

Here are some examples:

# Using the append method on a list object
myList = []
myList.append(2)

# Using the split method on a string object
text = "12 34"
parts = text.split()
tuple = (int(parts[0]),int(parts[1]))

In the rest of these notes you will see how to construct your own classes and methods.

Making a random number generator

A psuedo-random number sequence is a sequence of numbers that appears random to the casual observer, but is generated by a deterministic, repeatable algorithm. One of the simplest algorithms used to make a pseudo-random number sequence is the linear congruential random number generator. This is a simple mathematical formula that generates a sequence of integers. To form the sequence we pick an arbitrary integer as the seed for the sequence. We then substitute that starting value into a simple linear function to generate the next number in the sequence. Most often that simple linear function uses two large prime numbers as its coefficients.

x1 = 12553 x0 + 15401

We repeat that process to make a whole sequence of numbers. If we did just that, the sequence would typically march off toward positive or negative infinity. To prevent that, we mod each number by a large prime integer b on each round.

x1 = (12553 x0 + 15401) mod 6133

This guarantees that the numbers stay trapped in the range from 0 to b-1. Often, the sequence of integers generated by this process is not immediately useful to us, because, say, we may be interested in generating a sequence of numbers in the range from 0 to c-1 for some other integer c. That is easy to fix. If c < b, we can compute x % c for each number we generate and save the list of x % c values as we generate them.

Here is a simple example program in Python that uses this process to generate and print a list of integers in the range from 0-99:

x = 52 # The seed value for the pseudo-random sequence

for n in range(100):
    print(x % 100)
    x = (12553*x+15401)%6133

Next, we would like some way to encapsulate the random number generation process so we can start and stop the process on demand. The key to doing this is to place the current value of x in a safe location so we can access it on demand to generate the next x in the sequence. This is a very good application for the class concept in Python. Python classes store state information in the form of member variables. In this case, we are going to make a random sequence class that stores the current value of the x variable as a member variable. Next, we will also need to equip our class with at least one method that clients can use to fetch a new number from the sequence.

Here is the code for a Python class that can do these things.

class RndSeq():
    def __init__(self,seed = 0):
      self.x = seed

    def nextInt(self,N):
        """Return a random integer in the range from 0 to N-1."""
        if N > 5000:
            print("RndSeq.nextInt requires its argument to be less than or equal to 5000.")
            return 0
        self.x = (12553*self.x+15401)%6133
        return self.x % N

This code shows the bare minimum set of things needed to make a useful class. The class contains an __init__ method that is used to initialize objects. In this case, we need to initialize the member variable x with a seed value. Since classes store useful information in member variables, we also need member functions to access and modify those values. In this case, we only need one member function, nextInt(), that will compute and store the next x in the sequence and return a result based on the new value of the x hidden in the object.

This code is stored in a file named 'rndseq.py'. We can import and use this class in a simple test program.

from rndseq import RndSeq

seed = input("Enter a seed value for the sequence: ")
s = RndSeq(int(seed))
for n in range(100):
    print(s.nextInt(100))

Here are some things to note about the code in the RndSeq class and the example program:

  1. A class is a container for a set of methods.
  2. Methods work with member variables. To work with a member variable you use the self.<variable> syntax.
  3. Every method in a class takes self as its first parameter. self refers to the object that the method is working on.
  4. You call a method by using the <object>.method() syntax. For example, in the code above we invoked the nextInt() method on the RndSeq object s. Python will automatically translate s.nextInt(100) to nextInt(s,100). In the nextInt() method s gets assigned to the parameter self and 100 gets assigned to the parameter N.
  5. To create an object we use the syntax <class name>(<parameters). For example, in the example code above I created a RndSeq object by calling RndSeq(int(seed)). When you create an object in this way Python will pass any parameters you provided to the class's __init__() method. The __init__() method in turn will initialize the member variables in the newly created object.

Guessing game example

Objects store state information in their member variables. Methods manipulate those member variables.

In the next example we will construct a class that plays a simple guessing game. When you create a game object the __init__() method of our class will start by picking a random integer in the range from 1 to 100. The game object will also keep track of the number of guesses you have made. To play the game you will pass a guess to the guess() method. That method will compare your guess to the secret number and tell you whether you are too low, too high, or just right. Every time you call the guess() method the game will increment its count of the number of guesses you have made. If you have made too many guesses without getting the right answer, the game object will not allow you make any more guesses.

import random

class GuessingGame():
    def __init__(self):
        self.secret = random.randint(1,100)
        self.guesses = 0

    def isGameOver(self):
        if guesses < 5:
            return False
        else:
            return True

    def guess(self,n):
        if self.guesses >= 5:
            print("You have used up all of your guesses.")
        elif n < self.secret:
            self.guesses += 1
            print("Your guess is too low.")
        elif n > self.secret:
            self.guesses += 1
            print("Your guess is too high.")
        else:
            self.guesses = 5
            print("Correct! You win.")

Here now is some code to play the guessing game.

game = GuessingGame()
while game.isGameOver() == False:
    n = int(input("Guess a number:"))
    game.guess(n)

A complex number class

For our next example of a Python class I am going to construct a class that represents complex numbers. This is a very simple class with only two data members, the real and imaginary parts of the number. The class contains methods to read those data members and also compute and return the modulus of the complex number.

Because we will want to be able to do arithmetic with our complex number objects, I have also supplied this class with a number of operator overloads to implement the various operations.

When you try to do an operation with a couple of data items, such as adding two Complex objects, Python will try to make sense of the operation by converting the code into something that looks like a method call. Here is an example. Suppose we write the code

a = Complex(2,3)
b = Complex(3,4)
c = a*b

When the Python interpret reaches the last statement it try to make sense of the code there by first converting the code into an alternative form:

c = a.__mul__(b)

This has the form of a method call being applied to the object a with a parameter of b. To make this work, Python will inspect the object stored in a to see if it has a method with that name. If it does, it will call that method.

Here is the code for the __mul__ method in the Complex class.

def __mul__(self,other):
  r = self.real*other.getReal() - self.imag*other.getImag()
  i = self.real*other.getImag() + self.imag*other.getReal()
  return Complex(r,i)

This method implements complex multiplication using the standard mathematical rule for that operation. After computing the real and imaginary parts of the product, the function constructs and returns a Complex object with those parts.

Another handy method to provide in a class is the __str__ method. This method will get invoked when you try to use str() to convert an object to text. Here is an example.

x = Complex(2,1)
print(str(x))

When the Python interpreter encounters the second statement it will attempt to rewrite it as

print(x.__str__())

This will succeed if x is an object and the class that x belongs to implements the __str__ method.

Here is the code for the __str__ method for the Complex class.

def __str__(self):
    if self.imag < 0:
        return str(self.real)+"-"+str(-self.imag)+"i"
    elif self.imag > 0:
        return str(self.real)+"+"+str(self.imag)+"i"
    else:
        return str(self.real)

Code for the class

Here now is the complete source code for the Complex class. This code lives in a separate source file named 'complex.py'

import math

class Complex():
    def __init__(self,real,imag = 0):
        self.real = real
        self.imag = imag

    def getAbs(self):
        return math.sqrt(self.real*self.real+self.imag*self.imag)

    def __add__(self,other):
        return Complex(self.real+other.real,self.imag+other.imag)

    def __sub__(self,other):
        return Complex(self.real-other.real,self.imag-other.imag)

    def __mul__(self,other):
        r = self.real*other.real - self.imag*other.imag
        i = self.real*other.imag + self.imag*other.real
        return Complex(r,i)

    def __truediv__(self,other):
        d = other.real*other.real + other.imag*other.imag
        r = (self.real*other.real + self.imag*other.imag) / d
        i = (self.imag*other.real - self.real*other.imag) / d
        return Complex(r,i)

    def __str__(self):
        if self.imag < 0:
            return str(self.real)+"-"+str(-self.imag)+"i"
        elif self.imag > 0:
            return str(self.real)+"+"+str(self.imag)+"i"
        else:
            return str(self.real)

Application: Newton's method with complex numbers

Here now is an application program that makes use of the complex number class. This program is an implementation of Newton's method that takes advantage of the fact that Newton's method works just fine in the presence of complex numbers.

This program attempts to find the roots of the polynomial

x4 + 2 x3 - 6 x2 + 8 x + 80

This polynomial has no real roots: all four of its roots lie in the complex plane. If you run this program with a complex number as the starting guess it will converge to one of those four complex roots. If you start the program with a real number as the starting guess, Newton's method falls into an infinite loop and does not converge. When that happens you will have to stop the program by choosing the stop command in your IDE or by hitting the control-C key combination in the terminal.

# Program to find the roots of f(x) = x^4 + 2 x^3 - 6 x^2 + 8 x + 80
# by Newton's method. This polynomial has four complex roots.
from complex import Complex

# Define the function and its derivative
def f(x):
    return (((x+Complex(2))*x+Complex(-6))*x+Complex(8))*x + Complex(80)

def fp(x):
    return ((Complex(4)*x+Complex(6))*x+Complex(-12))*x+Complex(8)

r = input("Enter the real part for your starting guess: ")
i = input("Enter the imaginary part for your starting guess: ")
x = Complex(float(r),float(i))

y = f(x)
tolerance = 10e-6

while y.getAbs() > tolerance:
    # Compute f'(x)
    yp = fp(x)
    # Do one round of Newton's method
    x = x - y/yp
    y = f(x)

print("The root estimate is " + str(x))

An important thing to note here is that all of the numbers that appear in the definitions for f(x) and its derivative have to be complex numbers. We need to do this because the operator functions that allow us to do arithmetic with complex number objects require that both operands in the operation by complex number objects.

Programming assignment

Construct a class to represent rational numbers. A rational number contains a numerator and a denominator, both of which are integers. To make your rational number class useful, provide it with operator overloads for addition, subtraction, multiplication, and division. Also provide a __str__() method to convert a rational number to a string. Finally, add a __float__() method to the class so you can convert a rational number to a floating point number on demand.

To check whether or not your rational arithmetic functions are working correctly, write a program to compute two rational approximations for π. One way to estimate the value of π is to use this formula, which was discovered by Leibniz:

Another method is due to Wallis:

As you increase the number of terms in each of these approximations you will end up generating a sequence of rational numbers that approach π. Write a program that uses these two methods to construct two sequences of rational approximations for π. Each time you add another term to the approximation, use the float() function to convert your rational number to a floating point number and print that number.