Oct 23, 2014
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:
In [1]:
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:
In [1]:
def y(t):
g = 9.81
return v0*t - 0.5*g*t**2
Motivation: $y(t)$ is a function of $t$ only
With a class, y(t) can be a function of t only, but still have
v0 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
In [1]:
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:
In [1]:
y = Y(v0=3) # create instance (object)
v = y.value(0.1) # compute function value
In [1]:
y = Y(v0=3)
we create a new variable (instance) y of type Y.
Y(3) is a call to the constructor:
In [1]:
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 itThink 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
In [1]:
print y.v0
print y.g
In [1]:
def value(self, t):
return self.v0*t - 0.5*self.g*t**2
Example on a call:
In [1]:
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
In [1]:
Y.value(y, t=0.1)
Inside value things "appear" as
In [1]:
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
In [1]:
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
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)$
In [1]:
class MyFunc:
def __init__(self, p0, p1, p2, ..., pn):
self.p0 = p0
self.p1 = p1
...
self.pn = pn
def value(self, x):
return ...
In [1]:
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)
In [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
In [1]:
y = Y(3)
can be thought of as
In [1]:
Y.__init__(y, 3) # class prefix Y. is like a module prefix
Then
In [1]:
self.v0 = v0
is actually
In [1]:
y.v0 = 3
In [1]:
v = y.value(2)
can alternatively be written as
In [1]:
v = Y.value(y, 2)
In [1]:
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
In [1]:
s1 = SelfExplorer(1)
id(s1)
s2 = SelfExplorer(2)
id(s2)
s1.value(4)
SelfExplorer.value(s1, 4)
s2.value(5)
SelfExplorer.value(s2, 5)
In [1]:
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
In [1]:
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()
In [1]:
a1.name = 'Some other name'
a1.balance = 100000
a1.no = '19371564768'
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.
In [1]:
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
In [1]:
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
In [1]:
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
In [1]:
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:
In [1]:
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()
In [1]:
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
In [1]:
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())
In [1]:
def test_Circle():
R = 2.5
c = Circle(7.4, -8.1, R)
from math import pi
exact_area = pi*R**2
computed_area = c.area()
diff = abs(exact_area - computed_area)
tol = 1E-14
assert diff < tol, 'bug in Circle.area, diff=%s' % diff
exact_circumference = 2*pi*R
computed_circumference = c.circumference()
diff = abs(exact_circumference - computed_circumference)
assert diff < tol, 'bug in Circle.circumference, diff=%s' % diff
In [1]:
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
In [1]:
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)
In [1]:
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
In [1]:
y = Y(3)
v = y(0.1) # same as v = y.__call__(0.1) or Y.__call__(y, 0.1)
it is wise to represent f by a class where $p_0,\ldots,p_n$
are attributes and __call__(x) computes $f(x)$
In [1]:
class MyFunc:
def __init__(self, p0, p1, p2, ..., pn):
self.p0 = p0
self.p1 = p1
...
self.pn = pn
def __call__(self, x):
return ...
In [1]:
def f(x):
return x**3
can we make a class Derivative and write
In [1]:
dfdx = Derivative(f)
so that dfdx behaves as a function that computes the derivative of f(x)?
In [1]:
print dfdx(2) # computes 3*x**2 for x=2
for a small (yet moderate) $h$, say $h=10^{-5}$
Implementation.
In [1]:
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
In [1]:
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)
In [1]:
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:
In [1]:
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)
In [1]:
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
In [1]:
f = lambda x: a*x + b
is equivalent to
In [1]:
def f(x):
return a*x + b
Lambda functions are convenient for producing quick, short code
Use of closure:
In [1]:
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:
In [1]:
>>> 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
In [1]:
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)
In [1]:
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
Technique: Trapezoidal rule
Desired application code:
In [1]:
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)
In [1]:
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:
In [1]:
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)
In [1]:
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:
In [1]:
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:
In [1]:
y = Y(1.5)
y(0.2)
print y
and the coefficients can be stored as [1, 0, -1, 2]
Desired application code:
In [1]:
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
In [1]:
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
In [1]:
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:
In [1]:
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:
In [1]:
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
In [1]:
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
and their sum
In [1]:
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.
In [1]:
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)
In [1]:
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)
Desired application code:
In [1]:
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
In [1]:
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__
In [1]:
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)
In [1]:
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__()
In [1]:
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
In [1]:
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
In [1]:
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)
In [1]:
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')
In [1]:
u = Complex(1, 2)
w = u + 4.5
Problem: we have assumed that other is Complex.
Remedy:
In [1]:
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)
In [1]:
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
In [1]:
class Complex:
...
def __radd__(self, other):
"""Rturn other + self."""
# other + self = self + other:
return self.__add__(other)
In [1]:
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)
In [1]:
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)
In [1]:
a = A([1,2])
print a.__dict__ # all attributes
dir(a) # what's in object a?
a.__doc__ # programmer's documentation of A
In [1]:
a.myvar = 10 # add new attribute (!)
a.__dict__
dir(a)
b = A(-1)
b.__dict__ # b has no myvar attribute
dir(b)
In [1]:
%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)
In [1]:
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!
In [1]:
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)
In [1]:
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:
In [1]:
a+b = [1, 3]
a-b = [-8, -6]
a*b = [-15, -8]
a/b = [-0.75, -0.4]
In [1]:
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)
In [1]:
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))
In [1]:
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')
In [1]:
a = IntervalMath(5,7)
float(a)
is achieved by
In [1]:
class IntervalArithmetics:
...
def __float__(self):
return 0.5*(self.lo + self.up)
In [1]:
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)
In [1]:
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
In [1]:
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$