Learning objectives:
A class packs together data (a collection of variables) and functions into one single unit. As a programmer you can create a new class and thereby a new object type (similar to those you have already encountered - float, string, list, file, etc.). Once you have created a class you can create many objects of that type as you wish, just as you can have many int or float objects.
Modern programming makes heavy use of classes therefore it is an important concept to understand. We will spend this lecture focusing on examples.
Consider a function of $t$ with a parameter $v_0$: $$ y(t; v_0)=v_0t - {1\over2}gt^2 $$
We need both $v_0$ and $t$ to evaluate $y$ (and $g=9.81$). How should we implement this?
def y(t, v0):
g = 9.81
return v0*t - 0.5*g*t**2
# or define v0 as a global?
def y(t):
g = 9.81
return v0*t - 0.5*g*t**2
It is best to have y
as function of t
only (y(t)
, see the book for a thorough discussion). There are two ways this can be implemented - either define v0
as global variable (bad solution!) or y
as a class (good solution!)
A class has variables and functions. For this example class Y
for $y(t;v_0)$ has variables v0
and g
and a function value(t)
for computing $y(t;v_0)$. Classes in general should have the special function __init__
for initialising class variables. While we will not cover it in detail here, it is worth noting that professional developers often use UML (Unified Modeling Language) to illustrate the design of a class. Here is a UML diagram for this example:
Here is the implementation of this class:
class Y:
def __init__(self, v0):
self.v0 = v0
self.g = 9.81
def value(self, t):
return self.v0*t - 0.5*self.g*t**2
An example of its usage:
y = Y(v0=3) # Create instance
v = y.value(0.1) # Compute function value
print(v)
0.25095
When we write y = Y(v0=3)
we create a new variable (instance) y
of type Y
.
Y(3)
is a call to the constructor:
Think of self
as y
, i.e., the new variable to be created. self.v0
means that we attach a variable v0
to self (y
).
self
is always the first argument/parameter in a function, but never inserted in the call! After y=Y(3)
, y
has two variables v0
and g
, and we can take a look at these:
print(y.v0)
print(y.g)
3 9.81
Functions in classes are called methods. Variables in classes are called attributes. Therefore, in the above example the value
method was
Example on a call:
self
is left out in the call (as discussed above), but Python automatically inserts y
as the self
argument inside the value
method. Inside the value
method things appear as
The method value
has, through self
, access to the attributes. Attributes are like global variables in the class, and any method gets a self
parameter as its first argument. The method can then access the attributes of the class through self
.
In summary, class Y
collects the attributes v0
and g
and the method value
together as a single unit. value(t)
is function of t
only, but has access to the class attributes v0
and g
.
The great feature of Python is that we can send y.value
as an ordinary function of t
to any other function that expects a function f(t)
:
from math import pi
from pylab import *
def table(f, tstop, n):
"""Make a table of t, f(t) values."""
for t in linspace(0, tstop, n):
print(t, f(t))
def g(t):
return sin(t)*exp(-t)
table(g, 2*pi, 11) # send ordinary function
y = Y(6.5)
table(y.value, 2*pi, 11) # send class method
0.0 0.0 0.628318530718 0.313576432217 1.25663706144 0.27067976079 1.88495559215 0.144404428881 2.51327412287 0.0476121290679 3.14159265359 5.29217866803e-18 3.76991118431 -0.0135508663113 4.39822971503 -0.0116971330584 5.02654824574 -0.00624028118657 5.65486677646 -0.0020575066539 6.28318530718 -4.57391552795e-19 0.0 0.0 0.628318530718 2.14765406617 1.25663706144 0.422475365359 1.88495559215 -5.17553610244 2.51327412287 -14.6463803372 3.14159265359 -27.990057339 3.76991118431 -45.2065671078 4.39822971503 -66.2959096435 5.02654824574 -91.2580849463 5.65486677646 -120.093093016 6.28318530718 -152.800933853
Make a class called F that implements the function
$$f(x; a, w) = \exp(−ax)\sin(wx).$$A value(x) method computes values of f, while a and w are class attributes.
Test the class with the following main program:
from math import *
f = F(a=1.0, w=0.1)
print(f.value(x=pi))
f.a = 2
print(f.value(pi))
Make a class called Simple with one attribute i, one method double which replaces the value of i by i+i, and a constructor that initializes the attribute.
Try out the following code for testing the class (but before you run this code, convince yourself what the output of the print statements will be):
s1 = Simple(4)
for i in range(4):
s1.double()
print(s1.i)
s2 = Simple('Hello')
s2.double(); s2.double()
print(s2.i)
s2.i = 100
print(s2.i)
class Account:
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)
UML diagram of class 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("%s’s balance: %.2f"%(a1.name, a1.balance))
John Olsson’s balance: 13500.00
a1.dump()
John Olsson, 19371554951, balance: 13500
a2.dump()
Liz Olsson, 19371564761, balance: 9500
Add an attribute called transactions to the Account class given above. The new attribute counts the number of transactions done in the deposit and withdraw methods. The total number of transactions should be printed in the dump method. Write a simple test program to demonstrate that transaction gets the right value after some calls to deposit and withdraw.
It is not possible in Python to explicitly protect attributes from being overwritten by the calling function, i.e. the following is possible but not intended:
a1.name = 'Some other name'
a1.balance = 100000
a1.no = '19371564768'
The assumptions on correct usage:
balance
attribute can be viewed.balance
is done through with the methods draw
and deposit
.Remedy - Attributes and methods not intended for use outside the class can be marked as protected by prefixing the name with an underscore (e.g., _name
). This is just a convention to warn you to stay away from messing with the attribute directly. There is no technical way of stopping attributes and methods from being accessed directly from outside the class.
We rewrite the account class using this convention:
class AccountP:
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): # NEW - read balance value
return self._balance
def dump(self):
s = '%s, %s, balance: %s' %(self._name, self._no, self._balance)
print(s)
a1 = AccountP('John Olsson', '19371554951', 20000)
a1.withdraw(4000)
print(a1._balance) # it works, but a convention is broken
16000
print(a1.get_balance()) # correct way of viewing the balance
16000
a1._no = '19371554955' # if you did this you'd probably lose your job! Don't mess with the convention.
A phone book is a list of data about persons. Typical data includes: name, mobile phone, office phone, private phone, email. This data about a person can be collected in a class as attributes. Think about what kinds of methods make sense for this class, e.g.:
class Person:
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
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)
p1 = Person('Gerard Gorman', email='g.gorman@imperial.ac.uk')
p1.add_office_phone('49985')
p2 = Person('ICT Service Desk', office_phone='49000')
p2.add_email('service.desk@imperial.ac.uk')
phone_book = {'Gorman': p1, 'ICT': p2}
for p in phone_book:
phone_book[p].dump()
Gerard Gorman office phone: 49985 email address: g.gorman@imperial.ac.uk ICT Service Desk office phone: 49000 email address: service.desk@imperial.ac.uk
A circle is defined by its center point $x0, y0$ and its radius $R$. These data can be attributes in a class. Possible methods in the class are area and circumference. The constructor initializes $x0$, $y0$ and $R$.
class Circle:
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
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
Make a class called Line whose constructor takes two points $p_1$ and $p_2$ (2-tuples or 2-lists) as input. The line goes through these two points (see function line defined below for the relevant formula of the line). A value(x) method computes a value on the line at the point x.
def line(x0, y0, x1, y1):
"""
Compute the coefficients a and b in the mathematical
expression for a straight line y = a*x + b that goes
through two points (x0, y0) and (x1, y1).
x0, y0: a point on the line (floats).
x1, y1: another point on the line (floats).
return: coefficients a, b (floats) for the line (y=a*x+b).
"""
a = (y1 - y0)/float(x1 - x0)
b = y0 - a*x0
return a, b
Consider a quadratic function $f(x; a, b, c) = ax^2 + bx + c$. Make a class called Quadratic for representing f, where a, b, and c are attributes, and the methods are:
interval [L, R], 3. roots for computing the two roots.
Some class methods have leading and trailing double underscores, e.g.,
These are special methods and allow for special syntax. Recall for example the constructor, we write
and not (the more logical)
__call__
special method we can make the class instance behave and look as a function.__add__
special method we can add two class instances with our own tailored rule for addition.__call__
special method¶Let us replace the value
method in class Y
by a __call__
special method:
class Y:
def __init__(self, v0):
self.v0 = v0
self.g = 9.81
def __call__(self, t):
return self.v0*t - 0.5*self.g*t**2
Now we can write:
y = Y(3)
v = y(0.1) # same as v = y.__call__(0.1)
The instance $y$ behaves/looks as a function! The value(t)
method does the same, but the special method __call__
allows nicer syntax for computing function values.
__str__
special method (for printing)¶In Python, we can usually print an object a
by
This works for built-in types (strings, lists, floats, ...). However, if we have made a new type through a class, Python does not know how to print objects of this type. However, if the class has defined a method __str__
, Python will use this method to convert the object to a string.
class Y:
def __init__(self, v0):
self.v0 = v0
self.g = 9.81
def __call__(self, t):
return self.v0*t - 0.5*self.g*t**2
def __str__(self):
return 'v0*t - 0.5*g*t**2; v0=%g' % self.v0
y = Y(1.5)
y(0.2)
print(y)
v0*t - 0.5*g*t**2; v0=1.5
Make a class that can only do one thing: print a writes "Hello, World!" to the screen, where a is an instance of the class.
Modify the class from the first exercise such that the following code works:
f = F2(1.0, 0.1)
print(f(pi))
f.a = 2
print(f(pi))
print(f)