04 OOP Kindergarten
04 OOP Kindergarten
04 OOP Kindergarten
1 OOP kindergarten
• A quick resume
• Classes and objects
• Iterators and generators
2 A quick resume
2.1 Data structures summary
-- Lists, sets and dictionaries are extensible and mutable.
-- Tuples, on the other hand:
-- Not extensible
1
In [4]: print(myset)
{1, 2, 3}
<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
In [7]: p1.x = 2.
p1.y = 1.
p1.x, p1.y
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:
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:
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
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:
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:
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
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)
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
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.
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):
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
• 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:
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 [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....)
10
In [33]: fib = pi_series()
i=0
while i<1000:
a = fib.__next__()
i += 1
print(a)
3.140592653839794
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:
[5, 9, 6] 3
11
In [38]: #it has no length
try: len(filtered_gen)
except TypeError as e: print(e)
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)
3.140592653839794
5 The End!
12