Date: Aug 15, 2015
A class packs together data (a collection of variables) and functions as one single unit
As a programmer you can create a new class and thereby a new object type (like float
, list
, file
, ...)
A class is much like a module: a collection of "global" variables and functions that belong together
There is only one instance of a module while a class can have many instances (copies)
Modern programming applies classes to a large extent
It will take some time to master the class concept
Let's learn by doing!
Consider a function of $t$ with a parameter $v_0$:
We need both $v_0$ and $t$ to evaluate $y$ (and $g=9.81$), but how should we implement this?
Having $t$ and $v_0$ as arguments:
def y(t, v0):
g = 9.81
return v0*t - 0.5*g*t**2
Having $t$ as argument and $v_0$ as global variable:
def y(t):
g = 9.81
return v0*t - 0.5*g*t**2
Motivation: $y(t)$ is a function of $t$ only
y(t)
can be a function of t
only, but still havev0
and g
as parameters with given values.
y(t)
and data (v0
, g
)We make a class Y
for $y(t;v_0)$ with variables v0
and g
and a function value(t)
for computing $y(t;v_0)$
Any class should also have a function __init__
for initialization of the variables
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
Usage:
y = Y(v0=3) # create instance (object)
v = y.value(0.1) # compute function value
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:
def __init__(self, v0):
self.v0 = v0
self.g = 9.81
self
variable? Stay cool - it will be understood later as you get used to it¶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
).
Y(3)
means Y.__init__(y, 3)
, i.e., set self=y
, v0=3
Remember: self
is always first parameter in a function, but never inserted in the call!
After y = Y(3)
, y
has two variables v0
and g
print y.v0
print y.g
In mathematics you don't understand things. You just get used to them. John von Neumann, mathematician, 1903-1957.
Functions in classes are called methods
Variables in classes are called attributes
Here is the value
method:
def value(self, t):
return self.v0*t - 0.5*self.g*t**2
Example on a call:
v = y.value(t=0.1)
self
is left out in the call, but Python automatically inserts y
as the self
argument inside the value
method. Think of the call as
Y.value(y, t=0.1)
Inside value
things "appear" as
return y.v0*t - 0.5*y.g*t**2
self
gives access to "global variables" in the class object.
Class Y
collects the attributes v0
and g
and the method value
as one unit
value(t)
is function of t
only, but has automatically access to the parameters v0
and g
as self.v0
and self.g
The great advantage: we can send y.value
as an ordinary function of t
to any other function that expects a function f(t)
of one variable
def make_table(f, tstop, n):
for t in linspace(0, tstop, n):
print t, f(t)
def g(t):
return sin(t)*exp(-t)
table(g, 2*pi, 101) # send ordinary function
y = Y(6.5)
table(y.value, 2*pi, 101) # send class method
Given a function with $n+1$ parameters and one independent variable,
it is wise to represent f
by a class where
$p_0,\ldots,p_n$ are attributes and where there is a method, say value(self, x)
, for computing $f(x)$
class MyFunc:
def __init__(self, p0, p1, p2, ..., pn):
self.p0 = p0
self.p1 = p1
...
self.pn = pn
def value(self, x):
return ...
class VelocityProfile:
def __init__(self, beta, mu0, n, R):
self.beta, self.mu0, self.n, self.R = \
beta, mu0, n, R
def value(self, r):
beta, mu0, n, R = \
self.beta, self.mu0, self.n, self.R
n = float(n) # ensure float divisions
v = (beta/(2.0*mu0))**(1/n)*(n/(n+1))*\
(R**(1+1/n) - r**(1+1/n))
return v
v = VelocityProfile(R=1, beta=0.06, mu0=0.02, n=0.1)
print v.value(r=0.1)
class MyClass:
def __init__(self, p1, p2):
self.attr1 = p1
self.attr2 = p2
def method1(self, arg):
# can init new attribute outside constructor:
self.attr3 = arg
return self.attr1 + self.attr2 + self.attr3
def method2(self):
print 'Hello!'
m = MyClass(4, 10)
print m.method1(-2)
m.method2()
It is common to have a constructor where attributes are initialized, but this is not a requirement - attributes can be defined whenever desired
The book features a section on a different version of class Y
where there is no constructor (which is possible)
The book also features a section on how to implement classes without using classes
These sections may be clarifying - or confusing
Warning.
You have two choices:
follow the detailed explanations of what self
really is
postpone understanding self
until you have much more
experience with class programming (suddenly self
becomes clear!)
The syntax
y = Y(3)
can be thought of as
Y.__init__(y, 3) # class prefix Y. is like a module prefix
Then
self.v0 = v0
is actually
y.v0 = 3
self
works in the value
method¶v = y.value(2)
can alternatively be written as
v = Y.value(y, 2)
So, when we do instance.method(arg1, arg2)
, self
becomes
instance
inside method
.
self
¶id(obj)
: print unique Python identifier of an object
class SelfExplorer:
"""Class for computing a*x."""
def __init__(self, a):
self.a = a
print 'init: a=%g, id(self)=%d' % (self.a, id(self))
def value(self, x):
print 'value: a=%g, id(self)=%d' % (self.a, id(self))
return self.a*x
s1 = SelfExplorer(1)
id(s1)
s2 = SelfExplorer(2)
id(s2)
s1.value(4)
SelfExplorer.value(s1, 4)
s2.value(5)
SelfExplorer.value(s2, 5)
Warning.
You have two choices:
follow the detailed explanations of what self
really is
postpone understanding self
until you have much more
experience with class programming (suddenly self
becomes clear!)
The syntax
y = Y(3)
can be thought of as
Y.__init__(y, 3) # class prefix Y. is like a module prefix
Then
self.v0 = v0
is actually
y.v0 = 3
self
works in the value
method¶v = y.value(2)
can alternatively be written as
v = Y.value(y, 2)
So, when we do instance.method(arg1, arg2)
, self
becomes
instance
inside method
.
self
¶id(obj)
: print unique Python identifier of an object
class SelfExplorer:
"""Class for computing a*x."""
def __init__(self, a):
self.a = a
print 'init: a=%g, id(self)=%d' % (self.a, id(self))
def value(self, x):
print 'value: a=%g, id(self)=%d' % (self.a, id(self))
return self.a*x
s1 = SelfExplorer(1)
id(s1)
s2 = SelfExplorer(2)
id(s2)
s1.value(4)
SelfExplorer.value(s1, 4)
s2.value(5)
SelfExplorer.value(s2, 5)
Attributes: name of owner, account number, balance
Methods: deposit, withdraw, pretty print
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
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.dump()
a2.dump()
Possible, but not intended use:
The assumptions on correct usage:
The attributes should not be changed!
The balance
attribute can be viewed
Changing balance
is done through withdraw
or 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 - and no technical way of avoiding attributes and methods to be accessed.
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
print a1.get_balance() # correct way of viewing the balance
a1._no = '19371554955' # this is a "serious crime"!
A phone book is a list of data about persons
Data about a person: name, mobile phone, office phone, private phone, email
Let us create a class for data about a person!
Methods:
Constructor for initializing name, plus one or more other data
Add new mobile number
Add new office number
Add new private number
Add new email
Write out person data
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
class Person:
...
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
Usage:
p1 = Person('Hans Petter Langtangen', email='hpl@simula.no')
p1.add_office_phone('67828283'),
p2 = Person('Aslak Tveito', office_phone='67828282')
p2.add_email('aslak@simula.no')
phone_book = [p1, p2] # list
phone_book = {'Langtangen': p1, 'Tveito': p2} # better
for p in phone_book:
p.dump()
A circle is defined by its center point $x_0$, $y_0$ and its radius $R$
These data can be attributes in a class
Possible methods in the class: area
, circumference
The constructor initializes $x_0$, $y_0$ 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())
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
class MyClass:
def __init__(self, a, b):
...
p1 = MyClass(2, 5)
p2 = MyClass(-1, 10)
p3 = p1 + p2
p4 = p1 - p2
p5 = p1*p2
p6 = p1**7 + 4*p3
def __init__(self, ...)
def __call__(self, ...)
def __add__(self, other)
# Python syntax
y = Y(4)
print y(2)
z = Y(6)
print y + z
# What's actually going on
Y.__init__(y, 4)
print Y.__call__(y, 2)
Y.__init__(z, 6)
print Y.__add__(y, z)
We shall learn about many more such special methods
Replace the value
method 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) or Y.__call__(y, 0.1)
Note:
The instance y
behaves and looks as a function!
The value(t)
method does the same, but __call__
allows nicer syntax for computing function values
Given a function with $n+1$ parameters and one independent variable,
it is wise to represent f
by a class where $p_0,\ldots,p_n$
are attributes and __call__(x)
computes $f(x)$
class MyFunc:
def __init__(self, p0, p1, p2, ..., pn):
self.p0 = p0
self.p1 = p1
...
self.pn = pn
def __call__(self, x):
return ...
Given some mathematical function in Python, say
def f(x):
return x**3
can we make a class Derivative
and write
dfdx = Derivative(f)
so that dfdx
behaves as a function that computes the derivative of f(x)
?
print dfdx(2) # computes 3*x**2 for x=2
for a small (yet moderate) $h$, say $h=10^{-5}$
Implementation.
class Derivative:
def __init__(self, f, h=1E-5):
self.f = f
self.h = float(h)
def __call__(self, x):
f, h = self.f, self.h # make short forms
return (f(x+h) - f(x))/h
from math import *
df = Derivative(sin)
x = pi
df(x)
cos(x) # exact
def g(t):
return t**3
dg = Derivative(g)
t = 1
dg(t) # compare with 3 (exact)
Newton's method solves nonlinear equations $f(x)=0$, but the method requires $f'(x)$
def Newton(f, xstart, dfdx, epsilon=1E-6):
...
return x, no_of_iterations, f(x)
Suppose $f'(x)$ requires boring/lengthy derivation, then class Derivative
is handy:
def f(x):
return 100000*(x - 0.9)**2 * (x - 1.1)**3
df = Derivative(f)
xstart = 1.01
Newton(f, xstart, df, epsilon=1E-5)
How can we test class Derivative
?
Method 1: compute $(f(x+h)-f(x))/h$ by hand for some $f$ and $h$
Method 2: utilize that linear functions are differentiated exactly by our numerical formula, regardless of $h$
Test function based on method 2:
def test_Derivative():
# The formula is exact for linear functions, regardless of h
f = lambda x: a*x + b
a = 3.5; b = 8
dfdx = Derivative(f, h=0.5)
diff = abs(dfdx(4.5) - a)
assert diff < 1E-14, 'bug in class Derivative, diff=%s' % diff
Use of lambda functions:
f = lambda x: a*x + b
is equivalent to
def f(x):
return a*x + b
Lambda functions are convenient for producing quick, short code
Use of closure:
f = lambda x: a*x + b
a = 3.5; b = 8
dfdx = Derivative(f, h=0.5)
dfdx(4.5)
Looks straightforward...but
How can Derivative.__call__
know a
and b
when it calls
our f(x)
function?
Local functions inside functions remember (have access to) all local variables in the function they are defined (!)
f
can access a
and b
in test_Derivative
even when
called from __call__
in class `Derivative
f
is known as a closure in computer science
SymPy can perform exact, symbolic differentiation:
>>> from sympy import *
>>> def g(t):
... return t**3
...
>>> t = Symbol('t')
>>> dgdt = diff(g(t), t) # compute g'(t)
>>> dgdt
3*t**2
>>> # Turn sympy expression dgdt into Python function dg(t)
>>> dg = lambdify([t], dgdt)
>>> dg(1)
3
import sympy as sp
class Derivative_sympy:
def __init__(self, f):
# f: Python f(x)
x = sp.Symbol('x')
sympy_f = f(x)
sympy_dfdx = sp.diff(sympy_f, x)
self.__call__ = sp.lambdify([x], sympy_dfdx)
def g(t):
return t**3
def h(y):
return sp.sin(y)
dg = Derivative_sympy(g)
dh = Derivative_sympy(h)
dg(1) # 3*1**2 = 3
from math import pi
dh(pi) # cos(pi) = -1
Given a function $f(x)$, we want to compute
Technique: Midpoint rule or Trapezoidal rule, here the latter:
Desired application code:
def f(x):
return exp(-x**2)*sin(10*x)
a = 0; n = 200
F = Integral(f, a, n)
x = 1.2
print F(x)
def trapezoidal(f, a, x, n):
h = (x-a)/float(n)
I = 0.5*f(a)
for i in range(1, n):
I += f(a + i*h)
I += 0.5*f(x)
I *= h
return I
Class Integral
holds f
, a
and n
as attributes and has a call special method for computing the integral:
class Integral:
def __init__(self, f, a, n=100):
self.f, self.a, self.n = f, a, n
def __call__(self, x):
return trapezoidal(self.f, self.a, x, self.n)
How can we test class Integral
?
Method 1: compute by hand for some $f$ and small $n$
Method 2: utilize that linear functions are integrated exactly by our numerical formula, regardless of $n$
Test function based on method 2:
def test_Integral():
f = lambda x: 2*x + 5
F = lambda x: x**2 + 5*x - (a**2 + 5*a)
a = 2
dfdx = Integralf, a, n=4)
x = 6
diff = abs(I(x) - (F(x) - F(a)))
assert diff < 1E-15, 'bug in class Integral, diff=%s' % diff
In Python, we can usually print an object a
by print a
,
works for built-in types (strings, lists, floats, ...)
Python does not know how to print objects of a user-defined class,
but if the class defines a method __str__
, Python will use this method to convert an object to a string
Example:
class Y:
...
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
Demo:
y = Y(1.5)
y(0.2)
print y
A polynomial can be specified by a list of its coefficients. For example, $1 - x^2 + 2x^3$ is
and the coefficients can be stored as [1, 0, -1, 2]
Desired application code:
p1 = Polynomial([1, -1])
print p1
p2 = Polynomial([0, 1, 0, 0, -6, -1])
p3 = p1 + p2
print p3.coeff
print p3
p2.differentiate()
print p2
How can we make class Polynomial
?
class Polynomial:
def __init__(self, coefficients):
self.coeff = coefficients
def __call__(self, x):
s = 0
for i in range(len(self.coeff)):
s += self.coeff[i]*x**i
return s
class Polynomial:
...
def __add__(self, other):
# return self + other
# start with the longest list and add in the other:
if len(self.coeff) > len(other.coeff):
coeffsum = self.coeff[:] # copy!
for i in range(len(other.coeff)):
coeffsum[i] += other.coeff[i]
else:
coeffsum = other.coeff[:] # copy!
for i in range(len(self.coeff)):
coeffsum[i] += self.coeff[i]
return Polynomial(coeffsum)
The coeff. corresponding to power $i+j$ is $c_i\cdot d_j$. The
list r
of coefficients of the result:
r[i+j] = c[i]*d[j]
(i
and j
running from
0 to $M$ and $N$, resp.)
Implementation:
class Polynomial:
...
def __mul__(self, other):
M = len(self.coeff) - 1
N = len(other.coeff) - 1
coeff = [0]*(M+N+1) # or zeros(M+N+1)
for i in range(0, M+1):
for j in range(0, N+1):
coeff[i+j] += self.coeff[i]*other.coeff[j]
return Polynomial(coeff)
If c
is the list of coefficients, the derivative has a list
of coefficients, dc
, where dc[i-1] = i*c[i]
for
i
running from 1 to the largest index in c
.
Note that dc
has one element less than c
.
Implementation:
class Polynomial:
...
def differentiate(self): # change self
for i in range(1, len(self.coeff)):
self.coeff[i-1] = i*self.coeff[i]
del self.coeff[-1]
def derivative(self): # return new polynomial
dpdx = Polynomial(self.coeff[:]) # copy
dpdx.differentiate()
return dpdx
class Polynomial:
...
def __str__(self):
s = ''
for i in range(0, len(self.coeff)):
if self.coeff[i] != 0:
s += ' + %g*x^%d' % (self.coeff[i], i)
# fix layout (lots of special cases):
s = s.replace('+ -', '- ')
s = s.replace(' 1*', ' ')
s = s.replace('x^0', '1')
s = s.replace('x^1 ', 'x ')
s = s.replace('x^1', 'x')
if s[0:3] == ' + ': # remove initial +
s = s[3:]
if s[0:3] == ' - ': # fix spaces for initial -
s = '-' + s[3:]
return s
Consider
and their sum
p1 = Polynomial([1, -1])
print p1
p2 = Polynomial([0, 1, 0, 0, -6, -1])
p3 = p1 + p2
print p3.coeff
p2.differentiate()
print p2
How should, e.g., __add__(self, other)
be defined? This is completely up to the programmer, depending on what is meaningful by object1 + object2
.
An anthropologist was asking a primitive tribesman about arithmetic. When the anthropologist asked, What does two and two make? the tribesman replied, Five. Asked to explain, the tribesman said, If I have a rope with two knots, and another rope with two knots, and I join the ropes together, then I have five knots.
c = a + b # c = a.__add__(b)
c = a - b # c = a.__sub__(b)
c = a*b # c = a.__mul__(b)
c = a/b # c = a.__div__(b)
c = a**e # c = a.__pow__(e)
a == b # a.__eq__(b)
a != b # a.__ne__(b)
a < b # a.__lt__(b)
a <= b # a.__le__(b)
a > b # a.__gt__(b)
a >= b # a.__ge__(b)
Mathematical operations for vectors in the plane:
Desired application code:
u = Vec2D(0,1)
v = Vec2D(1,0)
print u + v
a = u + v
w = Vec2D(1,1)
a == w
print u - v
print u*v
class Vec2D:
def __init__(self, x, y):
self.x = x; self.y = y
def __add__(self, other):
return Vec2D(self.x+other.x, self.y+other.y)
def __sub__(self, other):
return Vec2D(self.x-other.x, self.y-other.y)
def __mul__(self, other):
return self.x*other.x + self.y*other.y
def __abs__(self):
return math.sqrt(self.x**2 + self.y**2)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __str__(self):
return '(%g, %g)' % (self.x, self.y)
def __ne__(self, other):
return not self.__eq__(other) # reuse __eq__
eval(repr(p))
creates p
¶class MyClass:
def __init__(self, a, b):
self.a, self.b = a, b
def __str__(self):
"""Return string with pretty print."""
return 'a=%s, b=%s' % (self.a, self.b)
def __repr__(self):
"""Return string such that eval(s) recreates self."""
return 'MyClass(%s, %s)' % (self.a, self.b)
m = MyClass(1, 5)
print m # calls m.__str__()
str(m) # calls m.__str__()
s = repr(m) # calls m.__repr__()
s
m2 = eval(s) # same as m2 = MyClass(1, 5)
m2 # calls m.__repr__()
class Y:
"""Class for function y(t; v0, g) = v0*t - 0.5*g*t**2."""
def __init__(self, v0):
"""Store parameters."""
self.v0 = v0
self.g = 9.81
def __call__(self, t):
"""Evaluate function."""
return self.v0*t - 0.5*self.g*t**2
def __str__(self):
"""Pretty print."""
return 'v0*t - 0.5*g*t**2; v0=%g' % self.v0
def __repr__(self):
"""Print code for regenerating this instance."""
return 'Y(%s)' % self.v0
Python already has a class complex
for complex numbers, but implementing such a class is a good pedagogical example on class programming (especially with special methods).
Usage:
u = Complex(2,-1)
v = Complex(1) # zero imaginary part
w = u + v
print w
w != u
u*v
u < v
print w + 4
print 4 - w
class Complex:
def __init__(self, real, imag=0.0):
self.real = real
self.imag = 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):
return Complex(self.real*other.real - self.imag*other.imag,
self.imag*other.real + self.real*other.imag)
def __div__(self, other):
ar, ai, br, bi = self.real, self.imag, \
other.real, other.imag # short forms
r = float(br**2 + bi**2)
return Complex((ar*br+ai*bi)/r, (ai*br-ar*bi)/r)
def __abs__(self):
return sqrt(self.real**2 + self.imag**2)
def __neg__(self): # defines -c (c is Complex)
return Complex(-self.real, -self.imag)
def __eq__(self, other):
return self.real == other.real and \
self.imag == other.imag
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
return '(%g, %g)' % (self.real, self.imag)
def __repr__(self):
return 'Complex' + str(self)
def __pow__(self, power):
raise NotImplementedError(
'self**power is not yet impl. for Complex')
Can we add a real number to a complex number?
u = Complex(1, 2)
w = u + 4.5
Problem: we have assumed that other
is Complex
.
Remedy:
class Complex:
...
def __add__(self, other):
if isinstance(other, (float,int)):
other = Complex(other)
return Complex(self.real + other.real,
self.imag + other.imag)
# or
def __add__(self, other):
if isinstance(other, (float,int)):
return Complex(self.real + other, self.imag)
else:
return Complex(self.real + other.real,
self.imag + other.imag)
What if we try this:
u = Complex(1, 2)
w = 4.5 + u
Problem: Python's float
objects cannot add a Complex
.
Remedy: if a class has an __radd__(self, other)
special method, Python applies this for other + self
class Complex:
...
def __radd__(self, other):
"""Rturn other + self."""
# other + self = self + other:
return self.__add__(other)
Right operands for subtraction is a bit more complicated since $a-b \neq b-a$:
class Complex:
...
def __sub__(self, other):
if isinstance(other, (float,int)):
other = Complex(other)
return Complex(self.real - other.real,
self.imag - other.imag)
def __rsub__(self, other):
if isinstance(other, (float,int)):
other = Complex(other)
return other.__sub__(self)
class A:
"""A class for demo purposes."""
def __init__(self, value):
self.v = value
Any instance holds its attributes in the self.__dict__
dictionary (Python automatically creates this dict)
a = A([1,2])
print a.__dict__ # all attributes
dir(a) # what's in object a?
a.__doc__ # programmer's documentation of A
a.myvar = 10 # add new attribute (!)
a.__dict__
dir(a)
b = A(-1)
b.__dict__ # b has no myvar attribute
dir(b)
Example on a defining a class with attributes and methods:
%matplotlib inline
class Gravity:
"""Gravity force between two objects."""
def __init__(self, m, M):
self.m = m
self.M = M
self.G = 6.67428E-11 # gravity constant
def force(self, r):
G, m, M = self.G, self.m, self.M
return G*m*M/r**2
def visualize(self, r_start, r_stop, n=100):
from scitools.std import plot, linspace
r = linspace(r_start, r_stop, n)
g = self.force(r)
title='m=%g, M=%g' % (self.m, self.M)
plot(r, g, title=title)
Example on using the class:
mass_moon = 7.35E+22
mass_earth = 5.97E+24
# make instance of class Gravity:
gravity = Gravity(mass_moon, mass_earth)
r = 3.85E+8 # earth-moon distance in meters
Fg = gravity.force(r) # call class method
c = a + b
implies c = a.__add__(b)
There are special methods for a+b
, a-b
, a*b
, a/b
, a**b
, -a
, if a:
, len(a)
, str(a)
(pretty print), repr(a)
(recreate a
with eval
), etc.
With special methods we can create new mathematical objects like vectors, polynomials and complex numbers and write "mathematical code" (arithmetics)
The call special method is particularly handy: v = c(5)
means v = c.__call__(5)
Functions with parameters should be represented by a class with the parameters as attributes and with a call special method for evaluating the function
Uncertainty quantification:
Consider measuring gravity $g$ by dropping a ball from $y=y_0$ to $y=0$ in time $T$:
What if $y_0$ and $T$ are uncertain? Say $y_0\in [0.99,1.01]$ m and $T\in [0.43, 0.47]$ s. What is the uncertainty in $g$?
Interval arithmetics.
Rules for computing with intervals, $p=[a,b]$ and $q=[c,d]$:
$p+q = [a + c, b + d]$
$p-q = [a - d, b - c]$
$pq = [\min(ac, ad, bc, bd), \max(ac, ad, bc, bd)]$
$p/q = [\min(a/c, a/d, b/c, b/d), \max(a/c, a/d, b/c, b/d)]$ ($[c,d]$ cannot contain zero)
Obvious idea: make a class for interval arithmetics!
class IntervalMath:
def __init__(self, lower, upper):
self.lo = float(lower)
self.up = float(upper)
def __add__(self, other):
a, b, c, d = self.lo, self.up, other.lo, other.up
return IntervalMath(a + c, b + d)
def __sub__(self, other):
a, b, c, d = self.lo, self.up, other.lo, other.up
return IntervalMath(a - d, b - c)
def __mul__(self, other):
a, b, c, d = self.lo, self.up, other.lo, other.up
return IntervalMath(min(a*c, a*d, b*c, b*d),
max(a*c, a*d, b*c, b*d))
def __div__(self, other):
a, b, c, d = self.lo, self.up, other.lo, other.up
if c*d <= 0: return None
return IntervalMath(min(a/c, a/d, b/c, b/d),
max(a/c, a/d, b/c, b/d))
def __str__(self):
return '[%g, %g]' % (self.lo, self.up)
Code:
I = IntervalMath # abbreviate
a = I(-3,-2)
b = I(4,5)
expr = 'a+b', 'a-b', 'a*b', 'a/b' # test expressions
for e in expr:
print e, '=', eval(e)
Output:
a+b = [1, 3]
a-b = [-8, -6]
a*b = [-15, -8]
a/b = [-0.75, -0.4]
This code
a = I(4,5)
q = 2
b = a*q
leads to
File "IntervalMath.py", line 15, in __mul__
a, b, c, d = self.lo, self.up, other.lo, other.up
AttributeError: 'float' object has no attribute 'lo'
Problem: IntervalMath
times int
is not defined.
Remedy: (cf. class Complex
)
class IntervalArithmetics:
...
def __mul__(self, other):
if isinstance(other, (int, float)): # NEW
other = IntervalMath(other, other) # NEW
a, b, c, d = self.lo, self.up, other.lo, other.up
return IntervalMath(min(a*c, a*d, b*c, b*d),
max(a*c, a*d, b*c, b*d))
(with similar adjustments of other special methods)
Try to compute g = 2*y0*T**(-2)
:
multiplication of int
(2) and
IntervalMath
(y0
),
and power operation T**(-2)
are not defined
class IntervalArithmetics:
...
def __rmul__(self, other):
if isinstance(other, (int, float)):
other = IntervalMath(other, other)
return other*self
def __pow__(self, exponent):
if isinstance(exponent, int):
p = 1
if exponent > 0:
for i in range(exponent):
p = p*self
elif exponent < 0:
for i in range(-exponent):
p = p*self
p = 1/p
else: # exponent == 0
p = IntervalMath(1, 1)
return p
else:
raise TypeError('exponent must int')
"Rounding" to the midpoint value:
a = IntervalMath(5,7)
float(a)
is achieved by
class IntervalArithmetics:
...
def __float__(self):
return 0.5*(self.lo + self.up)
class IntervalArithmetics:
...
def __str__(self):
return '[%g, %g]' % (self.lo, self.up)
def __repr__(self):
return '%s(%g, %g)' % \
(self.__class__.__name__, self.lo, self.up)
g = 9.81
y_0 = I(0.99, 1.01)
Tm = 0.45 # mean T
T = I(Tm*0.95, Tm*1.05) # 10% uncertainty
print T
g = 2*y_0*T**(-2)
g
# computing with mean values:
T = float(T)
y = 1
g = 2*y_0*T**(-2)
print '%.2f' % g
R = I(6*0.9, 6*1.1) # 20 % error
V = (4./3)*pi*R**3
V
print V
print float(V)
# compute with mean values:
R = float(R)
V = (4./3)*pi*R**3
print V
20% uncertainty in $R$ gives almost 60% uncertainty in $V$