$$ \newcommand{\tp}{\thinspace .} $$

 

 

 

This chapter is taken from the book A Primer on Scientific Programming with Python by H. P. Langtangen, 5th edition, Springer, 2016.

More examples on classes

The use of classes to solve problems from mathematical and physical sciences may not be so obvious. On the other hand, in many administrative programs for managing interactions between objects in the real world the objects themselves are natural candidates for being modeled by classes. Below we give some examples on what classes can be used to model.

Bank accounts

The concept of a bank account in a program is a good candidate for a class. The account has some data, typically the name of the account holder, the account number, and the current balance. Three things we can do with an account is withdraw money, put money into the account, and print out the data of the account. These actions are modeled by methods. With a class we can pack the data and actions together into a new data type so that one account corresponds to one variable in a program.

Class Account can be implemented as follows:

class Account(object):
    def __init__(self, name, account_number, initial_amount):
        self.name = name
        self.no = account_number
        self.balance = initial_amount

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        self.balance -= amount

    def dump(self):
        s = '%s, %s, balance: %s' % \ 
            (self.name, self.no, self.balance)
        print s

Here is a simple test of how class Account can be used:

>>> from classes import Account
>>> a1 = Account('John Olsson', '19371554951', 20000)
>>> a2 = Account('Liz Olsson',  '19371564761', 20000)
>>> a1.deposit(1000)
>>> a1.withdraw(4000)
>>> a2.withdraw(10500)
>>> a1.withdraw(3500)
>>> print "a1's balance:", a1.balance
a1's balance: 13500
>>> a1.dump()
John Olsson, 19371554951, balance: 13500
>>> a2.dump()
Liz Olsson, 19371564761, balance: 9500

The author of this class does not want users of the class to operate on the attributes directly and thereby change the name, the account number, or the balance. The intention is that users of the class should only call the constructor, the deposit, withdraw, and dump methods, and (if desired) inspect the balance attribute, but never change it. Other languages with class support usually have special keywords that can restrict access to attributes, but Python does not. Either the author of a Python class has to rely on correct usage, or a special convention can be used: any name starting with an underscore represents an attribute that should never be touched. One refers to names starting with an underscore as protected names. These can be freely used inside methods in the class, but not outside.

In class Account, it is natural to protect access to the name, no, and balance attributes by prefixing these names by an underscore. For reading only of the balance attribute, we provide a new method get_balance. The user of the class should now only call the methods in the class and not access any data attributes directly.

The new "protected" version of class Account, called AccountP, reads

class AccountP(object):
    def __init__(self, name, account_number, initial_amount):
        self._name = name
        self._no = account_number
        self._balance = initial_amount

    def deposit(self, amount):
        self._balance += amount

    def withdraw(self, amount):
        self._balance -= amount

    def get_balance(self):
        return self._balance

    def dump(self):
        s = '%s, %s, balance: %s' % \ 
            (self._name, self._no, self._balance)
        print s

We can technically access the data attributes, but we then break the convention that names starting with an underscore should never be touched outside the class. Here is class AccountP in action:

>>> a1 = AccountP('John Olsson', '19371554951', 20000)
>>> a1.deposit(1000)
>>> a1.withdraw(4000)
>>> a1.withdraw(3500)
>>> a1.dump()
John Olsson, 19371554951, balance: 13500
>>> print a1._balance       # it works, but a convention is broken
13500
print a1.get_balance()      # correct way of viewing the balance
13500
>>> a1._no = '19371554955'  # this is a "serious crime"

Python has a special construct, called properties, that can be used to protect data attributes from being changed. This is very useful, but the author considers properties a bit too complicated for this introductory book.

Phone book

You are probably familiar with the phone book on your mobile phone. The phone book contains a list of persons. For each person you can record the name, telephone numbers, email address, and perhaps other relevant data. A natural way of representing such personal data in a program is to create a class, say class Person. The data attributes of the class hold information like the name, mobile phone number, office phone number, private phone number, and email address. The constructor may initialize some of the data about a person. Additional data can be specified later by calling methods in the class. One method can print the data. Other methods can register additional telephone numbers and an email address. In addition we initialize some of the data attributes in a constructor method. The attributes that are not initialized when constructing a Person instance can be added later by calling appropriate methods. For example, adding an office number is done by calling add_office_number.

Class Person may look as

class Person(object):
    def __init__(self, name,
                 mobile_phone=None, office_phone=None,
                 private_phone=None, email=None):
        self.name = name
        self.mobile = mobile_phone
        self.office = office_phone
        self.private = private_phone
        self.email = email

    def add_mobile_phone(self, number):
        self.mobile = number

    def add_office_phone(self, number):
        self.office = number

    def add_private_phone(self, number):
        self.private = number

    def add_email(self, address):
        self.email = address

Note the use of None as default value for various data attributes: the object None is commonly used to indicate that a variable or attribute is defined, but yet not with a sensible value.

A quick demo session of class Person may go as follows:

>>> p1 = Person('Hans Hanson',
...             office_phone='767828283', email='h@hanshanson.com')
>>> p2 = Person('Ole Olsen', office_phone='767828292')
>>> p2.add_email('olsen@somemail.net')
>>> phone_book = [p1, p2]

It can be handy to add a method for printing the contents of a Person instance in a nice fashion:

class Person(object):
    ...
    def dump(self):
        s = self.name + '\n'
        if self.mobile is not None:
            s += 'mobile phone:   %s\n' % self.mobile
        if self.office is not None:
            s += 'office phone:   %s\n' % self.office
        if self.private is not None:
            s += 'private phone:  %s\n' % self.private
        if self.email is not None:
            s += 'email address:  %s\n' % self.email
        print s

With this method we can easily print the phone book:

>>> for person in phone_book:
...     person.dump()
...
Hans Hanson
office phone:   767828283
email address:  h@hanshanson.com

Ole Olsen
office phone:   767828292
email address:  olsen@somemail.net

A phone book can be a list of Person instances, as indicated in the examples above. However, if we quickly want to look up the phone numbers or email address for a given name, it would be more convenient to store the Person instances in a dictionary with the name as key:

>>> phone_book = {'Hanson': p1, 'Olsen': p2}
>>> for person in sorted(phone_book):  # alphabetic order
...     phone_book[person].dump()

The current example of Person objects is extended in the section Example: Phone book with special methods.

A circle

Geometric figures, such as a circle, are other candidates for classes in a program. A circle is uniquely defined by its center point \( (x_0,y_0) \) and its radius \( R \). We can collect these three numbers as data attributes in a class. The values of \( x_0 \), \( y_0 \), and \( R \) are naturally initialized in the constructor. Other methods can be area and circumference for calculating the area \( \pi R^2 \) and the circumference \( 2\pi R \):

class Circle(object):
    def __init__(self, x0, y0, R):
        self.x0, self.y0, self.R = x0, y0, R

    def area(self):
        return pi*self.R**2

    def circumference(self):
        return 2*pi*self.R

An example of using class Circle goes as follows:

>>> c = Circle(2, -1, 5)
>>> print 'A circle with radius %g at (%g, %g) has area %g' % \ 
...       (c.R, c.x0, c.y0, c.area())
A circle with radius 5 at (2, -1) has area 78.5398

The ideas of class Circle can be applied to other geometric objects as well: rectangles, triangles, ellipses, boxes, spheres, etc. Exercise 4: Make classes for a rectangle and a triangle tests if you are able to adapt class Circle to a rectangle and a triangle.

Verification

We should include a test function for checking that the implementation of class Circle is correct:

def test_Circle():
    R = 2.5
    c = Circle(7.4, -8.1, R)

    from math import pi
    expected_area = pi*R**2
    computed_area = c.area()
    diff = abs(expected_area - computed_area)
    tol = 1E-14
    assert diff < tol, 'bug in Circle.area, diff=%s' % diff

    expected_circumference = 2*pi*R
    computed_circumference = c.circumference()
    diff = abs(expected_circumference - computed_circumference)
    assert diff < tol, 'bug in Circle.circumference, diff=%s' % diff

The test_Circle function is written in a way that it can be used in a pytest or nose testing framework. The necessary conventions are that the function name starts with test_, the function takes no arguments, and all tests are of the form assert success or assert success, msg where success is a boolean condition for the test and msg is an optional message to be written if the test fails (success is False). It is a good habit to write such test functions to verify the implementation of classes.

Remark

There are usually many solutions to a programming problem. Representing a circle is no exception. Instead of using a class, we could collect \( x_0 \), \( y_0 \), and \( R \) in a list and create global functions area and circumference that take such a list as argument:

x0, y0, R = 2, -1, 5
circle = [x0, y0, R]

def area(c):
    R = c[2]
    return pi*R**2

def circumference(c):
    R = c[2]
    return 2*pi*R

Alternatively, the circle could be represented by a dictionary with keys 'center' and 'radius':

circle = {'center': (2, -1), 'radius': 5}

def area(c):
    R = c['radius']
    return pi*R**2

def circumference(c):
    R = c['radius']
    return 2*pi*R