This chapter is taken from the book A Primer on Scientific Programming with Python by H. P. Langtangen, 5th edition, Springer, 2016.
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.
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.
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.
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.
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.
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