04 OOP Kindergarten

Download as pdf or txt
Download as pdf or txt
You are on page 1of 12

04_OOP_kindergarten

March 28, 2017

In [1]: from IPython.display import Image

1 OOP kindergarten
• A quick resume
• Classes and objects
• Iterators and generators

In [2]: import math


import numpy as np
import scipy as sp
import scipy.constants as CONST
PI = CONST.pi
import matplotlib.pyplot as plt
%matplotlib inline

2 A quick resume
2.1 Data structures summary
-- Lists, sets and dictionaries are extensible and mutable.
-- Tuples, on the other hand:

-- Not extensible

-- Values cannot be modified

-- Dictionaries and sets are unordered.


-- Elements of sets are not indexed.
Tuples require less storage and are treated faster by the interpreter.
Since sets have unique elements, the interpreter can optimize membership tests.

In [3]: mystr = "Python" #a string


mylist = ["king","soldatino","dartagnan"]
mytuple = (1,1,2,2,3,3)
mydict = {"A":"adenine","C":"cytosine","G":"guanosine","T":"thymine"}
myset = set(mytuple)

1
In [4]: print(myset)

{1, 2, 3}

3 Classes and objects


3.0.1 Creating new data types
You have heard that everything in Python is an object. Until now we have used Python data types.
But what if we have to define a new, composite datatype (C, not C++, programmers: typedef).
This is done by defining a new class which defines how to build a certain kind of object.
The class stores items (attributes) shared by all the instances of this class. A class is something
analogous to a C structure (a new structured data type) endowed with its own functions, i.e. its
methods.
A method is defined in a class by including function definitions within the scope of the class
block.
Suppose we want to represent a a point in a 2D space:

In [5]: class Point(object):


"""
A point in 2D space
"""
print(Point)

<class '__main__.Point'>

Point is derived from the basic Python data type which is a object: you have already heard
that everything in Python is an object.
Now we use this new type to create a variable, i.e. an instace of class Point

In [6]: p1 = Point()
type(p1),p1

Out[6]: (__main__.Point, <__main__.Point at 0x7f4a57082c88>)

p1 is an instance of type Point.


The properties of an object are called attributes. The most obvious attributes of a point are its
coordinates:

In [7]: p1.x = 2.
p1.y = 1.
p1.x, p1.y

Out[7]: (2.0, 1.0)

the built-in hasattr let’s you check if we have a point in 2D or 3D space:

In [8]: hasattr(p1,"x"), hasattr(p1,"z")

2
Out[8]: (True, False)

what properties can we desume from these basic attributes? one can be the norm of the vector
from the origin to our point:

In [9]: import math


class Point(object):
"""
A point in 2D space
"""
def norm(point,xo=.0,yo=.0):
"""
assume RS is (0.,0.) with keyword arguments
"""
return math.sqrt((point.x-xo)**2 + (point.y-yo)**2)
p1 = Point()
p1.x = 2.
p1.y = 1.
n1 = p1.norm()
n2 = Point.norm(p1)
print(n1,n2)

2.23606797749979 2.23606797749979

The function norm is associated to Point i.e. is a method. It can be invoked directly on the p1
instance or used as a function from the class Point. In the first case norm acts automatically on p1
which is called the subject of the method.
Since a method takes as its first argument the instance it is using, it is very common to use the
attribute self to encapsulate variables. Hence:

In [10]: class Point(object):


"""
A point in 2D space
"""
def __init__(self,x,y,ref=[0,0]):
self.x = float(x)
self.y = float(y)
self.ref = ref

def norm(self):
"""
assume RS is (0.,0.) with keyword arguments
"""
return math.sqrt((self.x-self.ref[0])**2 + (self.y-self.ref[1])**2)

p1 = Point(2,1)
p1.norm()

3
Out[10]: 2.23606797749979

The \_\_init\_\_ method is invoked whenever an instance of Point is created; \_\_init\_\_


is a special method. Note that this definition of Point by default uses itself for defining the origin
of axes; i.e. any instance of Point we create will include a reference to another one which is
embedded.
Being Python data types user defined classes have some default behaviour. For instance, they
are mutable:

In [11]: p1.x = 3; p1.x

Out[11]: 3

In [12]: p2 = p1
print(id(p1)-id(p2))
print(p2 == p1, p2 is p1)
p1.x = 2
p2.x

0
True True

Out[12]: 2

In [13]: p1 = Point(2,1)
print(p2 == p1, p2 is p1)

False False

The command p2 = p1 creates an alias of name p1. Note that the == operator in this case checks
the identity and not the value of p1 and p2 as it would do with integer and floats:

In [14]: n1 = 1.0
n2 = 1
print(n2 == n1, n2 is n1)

True False

As for other mutable data types copy or deepcopy from the copy module can be used:

In [15]: import copy


p2 = copy.copy(p1)
p3 = copy.deepcopy(p1)
print(p2.ref is p1.ref)
print(p3.ref is p1.ref)

4
True
False

i.e. a shallow copy creates a reference to embedded objects at variance with a deep copy
This way of creating types and "associating functions to data" is called Object oriented pro-
gramming (OOP). OOP is particularly suited for large projects involving multiple developers.

• In OOP, a code is divided in small blocks, which can be managed independently, without
blocking the development in other parts of the code.

• OOP revolves around the concept of object and an object can be composed of multiple ob-
jects.

3.0.2 Inheritance
A cool thing about object-oriented programming is inheritance, i.e. the possibility of defining new
classes importing methods and atributes from previous ones. The new class is called a subclass
while the older one is its parent or ancestor or superclass. Using the pass statement you can
simply create a subclass without adding modifications:

In [16]: import numpy

class legionary(object):
"""
A class to define an ancient warrior; enough with student or car examples
"""
def __init__(self,position="hastati"):
self.position = position
self.rank = None #officer or private?
self.status = 1 # dead or alive?
self.javelins = 2

def throw_javelin(self):
self.javelins -= 1
#throw the pilum a random number of meters away
return 20.*numpy.random.rand(1)[0]
L = legionary()
L

Out[16]: <__main__.legionary at 0x7f4a5709ef28>

In [17]: class centurion(legionary):


def promote(self):
self.rank = "centurion"
#let him be tougher
self.status = 2.

def __init__(self,position):

5
legionary.__init__(self,position)
self.promote()

def __str__(self):
if self.status > 0:
return self.rank + " is alive with " + str(self.javelins) + " pila"

julius = centurion("triarii")
print(julius)

centurion is alive with 2 pila

3.1 Numerical Integration


Suppose we want to integrate a function, i.e. we want to compute:
∫ x
F ( x; a) = f (t)dt
a

for which we have defined a range of values for t. A solution may be to use the Trapezoidal Rule
with n intervals and n+1 points:
∫ x
( )
n −1
h
f (t)dt = f ( a) + f ( x ) + ∑ 2 f ( a + ih)
a 2 i =1

where $ h=(x-a)/n $. Ideally, we would like to compute F ( x; a) like that:

myf = lambda x: sin(x)


a = 0; n = 100
F = Trap(myf,a,n)

according to the Section ?? a solution could be:

In [18]: def Trapezoidal(f,a,b,npoints):


"""
apply Trapezoidal rule to integrate f from a to x using n nodes
"""
h = (b-a)/npoints
F = 2*np.sum(np.asarray([f(a+i*h) for i in range(1,npoints)]))
return (h/2)*(F+f(a)+f(b))

Let’s try:

In [19]: x = PI*np.linspace(0,1)
myf = lambda x: np.sin(x)
F = Trapezoidal(myf,0.,PI,100)
F

Out[19]: 1.9998355038874436

6
However, a Integral class using the Trapezoidal method may be a more general solution. The
call special method allows to call an instance as function, creating a wrapper.

In [20]: class Integral(object):


def __init__(self,func,a,b,n=100):
"""
create integrator instance; see special methods
"""
self.func = func
self.a = a
self.b = b
self.n = n

def __call__(self):
"""
Integrate with Trapezoidal rule
"""
return Trapezoidal(self.func,self.a,self.b,self.n)

In [21]: F = Integral(myf,0,PI)
F()

Out[21]: 1.9998355038874436

3.2 Exercises
1. Modify the Point class so that even a shallow copy creates copies of all attributes
2. Extend the Point class to arbitrary number of coordinates and different norm
3. Create a Rectangle class using Point (in 2D) and side dimensions; add methods for perimeter
and area
4. Extend the Integrate class. Try numerical integration with the Section ??:
∫ 1
1 4 1
f ( x )dx ≈ f (−1) + f (0) + f (1)
−1 3 3 3

3.2.1 Hints
class Integral(object):
<snip>
def _call__(self):
pass

class Simpson(Integral):

In [22]: #Solution 2; test and complete; what is mnorm?


import math

class Point(object):

7
def __init__(self,coords,SR=None):
# a list of coordinates
self.coords = coords
self.dim = len(coords)
if bool(SR):
self.SR = SR
else:
self.SR = [.0 for i in range(self.dim)]

def enorm(self):
N = [(self.coords[i]-self.SR[i])**2 for i in range(self.dim)]
return math.sqrt(sum(N))

def mnorm(self):
M = [abs(self.coords[i]-self.SR[i]) for i in range(self.dim)]
return sum(M)

def norm(self,distance="euclidean"):
"""
assume RS is (0.,0.) with keyword arguments
"""
distance = distance.lower()
if distance == "euclidean":
return self.enorm()
elif distance == "m":
return self.mnorm()

apoint = Point([2,3,4])
apoint.dim; apoint.norm()

Out[22]: 5.385164807134504

In [23]: #Solution 3
class Rectangle(object):

def __init__(self,bottom_left,upper_right):
self.btl = bottom_left
self.upr = upper_right
self.width = upper_right.coords[0]-bottom_left.coords[0]
self.height = upper_right.coords[1]-bottom_left.coords[1]

def area(self):
return self.width*self.height

def perimeter(self):
return 2.*(self.width+self.height)

p1 = Point((1,1))

8
p2 = Point((3,3))

rect = Rectangle(p1,p2)

In [24]: rect.perimeter()

Out[24]: 8.0

In [25]: rect.area()

Out[25]: 4

4 Iterators and generators


4.1 Iterators
An iterable object is anything that can be viewed as a collection of other objects and can be used
in a for loop, including lists, dicts, files . . . .
All such objects have a __iter__ method that returns an iterator for that object. The iterators
runs over the “components” with one at time until it raises StopIteration.
An ITERABLE is:

• anything that can be looped over (i.e. you can loop over a string or file)
• anything that can appear on the right-side of a for-loop: for x in iterable: ...
• anything you can call with iter() have it return an ITERATOR: iter(obj)
• an object that defines __iter__ that returns a fresh ITERATOR, or it may have a __getitem__
method suitable for indexed lookup.

An ITERATOR is:

• an object with state that remembers its past state it is during iteration
• an object with a __next__ method that:
– returns the next value in the iteration
– updates the state
– signals when it is done by raising StopIteration
• an object that is self-iterable (meaning that it has an __iter__ method that returns self).

4.2 Generators
A generator is a function which can stop whatever it is doing at an arbitrary point in its body,
return a value back to the caller, and, later on, resume from the point it has stopped and proceed
as if nothing had happened. This magic is done by means of the yield statement:

In [26]: def yrange(n):


i = 0
while i < n:
yield i
i += 1

9
yield causes the interpreter to manage yrange in a special way; invoking yrange does not
execute the function. Instead, it prints that a yrange is

In [27]: myrange = yrange(3)


myrange

Out[27]: <generator object yrange at 0x7f4a57083258>

remember when we used list(range(n)) ...

In [28]: #myrange.__next__()
next(myrange)

Out[28]: 0

In [29]: #myrange.__next__()
next(myrange)

Out[29]: 1

In [30]: #myrange.__next__()
next(myrange)

Out[30]: 2

In [31]: #myrange.__next__()
try:
next(myrange)
except StopIteration as e:
print("I got a StopIteration")

I got a StopIteration

The performance improvement from the use of generators is the result of the lazy (on demand)
generation of values, which translates to lower memory usage.
Furthermore, we do not need to wait until all the elements have been generated before using
them. A generator will provide performance benefits only if we do not intend to use that set of
generated values more than once.
Since the memory used by a generator is constant and its status always defined it is possible
to use it to manage infinite sequences, such as a Fibonacci series:
π = 4 ∗ (1 − 1/3 + 1/5 − 1/7....)

In [32]: def pi_series():


total = 0
i = 1.0; j = 1
while(1): #always true
total = total + j/i
yield 4*total
i = i + 2; j = j * -1

10
In [33]: fib = pi_series()
i=0
while i<1000:
a = fib.__next__()
i += 1
print(a)

3.140592653839794

A generator can be used to yield another generator in a nested way:

In [34]: def firstn(g, n):


for i in range(n):
yield g.__next__()
print(firstn(pi_series(),10))

<generator object firstn at 0x7f4a57083c50>

In [35]: list(firstn(pi_series(),10))

Out[35]: [4.0,
2.666666666666667,
3.466666666666667,
2.8952380952380956,
3.3396825396825403,
2.9760461760461765,
3.2837384837384844,
3.017071817071818,
3.2523659347188767,
3.0418396189294032]

Note that fib() conserves its status so starts the second call starts from a different value.
Using list() on the generator object automatically calls next until the end
Generators can be used in generator comprehensions, analogous to list comprehensions:

In [36]: my_list = [1, 3, 5, 9, 2, 6]


filtered_list = [item for item in my_list if item > 3] # a list comprehension
print(filtered_list,len(filtered_list))

[5, 9, 6] 3

In [37]: filtered_gen = (item for item in my_list if item > 3)


print(filtered_gen) # notice it's a generator object

<generator object <genexpr> at 0x7f4a57083f10>

11
In [38]: #it has no length
try: len(filtered_gen)
except TypeError as e: print(e)

object of type 'generator' has no len()

In [39]: filtered_gen.__next__()

Out[39]: 5

4.3 Exercise
Use generators to implement the Euler accelerator on a series. If Sn is a converging sequence then
convergence may be speed up by:

( S n +1 − S n ) 2
Sn +1 −
Sn+1 − 2Sn + Sn−1

In [40]: #Solution
def euler_accelerator(series):
#initialization; g does not store values, as a list would do
s0 = series.next() # Sn-1
s1 = series.next() # Sn
s2 = series.next() # Sn+1
while 1: # Stop Iteration is given by series
yield s2 - ((s2 - s1)**2)/(s2 - 2.0*s1 + s0)
s0, s1, s2 = s1, s2, series.next() #wrap up

In [41]: euler_accelerator(pi_series)

Out[41]: <generator object euler_accelerator at 0x7f4a57083d58>

In [42]: while i<100:


a = euler_accelerator.__next__()
i += 1
print(a)

3.140592653839794

5 The End!

12

You might also like