Object Oriented Programming II.

OOP II.

Encapsulation
In procedural programming, the data and the functions are disjoint. With classes the data and the corresponding functions are in one class. The data and the functionality of the class are accessible via the interface: the methods of the class.
Class
The type of an object, contains every attribute of the object.
Attribute
Attribute can be a member: data (variable) or function (method). There are class attributes and object (instance) attributes. They can be accessed with the dot notation (dot and name).
Instance
An concrete object of a class, like 5 is an instance of int.
Instantiation
When you create a new object (__init___)
Instance variable
a variable of self, it is defined inside a method. It is the property of an instance, different instances can have different values.
Class variable
variable defined in a class, outside of any methods. It is a property of the class, every instance shares it.
Method
a function inside a class, its first parameter is (typically but not always) self.
Inheritance
a way to make a class on top of an other, existing one. The new class (child) is derived from the old class (parent). The child class will have every attribute that the parent have had.
Polymorphism
Different classes with similar functionality (interface). Heterogeneous objects can be used for the same thing, although behaving differently.

Inheritance

Let's write a python class Person which stores a name and a title (Mr, Ms, Dr ...). Let's also write a __str__ method which prints the name nicely.

After that we make a Knight class inheritted from the Person. The knights are just like the persons but their title is "Sir".

See how the child class calles the parent's constructor (super).

In [20]:
class Person(object):
    def __init__(self, name, title):
        self.name = name
        self.title = title
    def __str__(self):
        return self.title + " " + self.name

class Knight(Person):
    def __init__(self, name):
        super(Knight, self).__init__(name, 'Sir')

smith = Person('Smith', 'Mr')
launcelot = Knight('Launcelot')
print smith
print launcelot
Mr Smith
Sir Launcelot

You can also define members in the child class, but they are only present in the child, not in the parent.

For example a Knight can have an optional epithet!

In [21]:
class Person(object):
    def __init__(self, name, title):
        self.name = name
        self.title = title
    def __str__(self):
        return self.title + " " + self.name

class Knight(Person):
    def __init__(self, name, epithet=""):
        super(Knight, self).__init__(name, 'Sir')
        self.epithet = epithet
         
launcelot = Knight('Launcelot', 'the brave')
print launcelot
Sir Launcelot

Now we override the inherited __str__ method with a new one, a same method but in the child class. If you don't write a new method, the parent (inherited) method is used instead.

In [22]:
class Person(object):
    def __init__(self, name, title):
        self.name = name
        self.title = title

    def __str__(self):
        return self.title + " " + self.name

class Knight(Person):
    def __init__(self, name, epithet=""):
        super(Knight, self).__init__(name, 'Sir')
        self.epithet = epithet

    def __str__(self):
        if len(self.epithet) > 0:
            return self.title + " " + self.name + ", " + self.epithet
        else:
            return super(Knight, self).__str__()
         
launcelot = Knight('Launcelot', 'the brave')
black = Knight('Black')
robin = Knight('Robin', 'the Not-quite-so-brave-as-Sir-Launcelot')
print launcelot
print black
print robin
Sir Launcelot, the brave
Sir Black
Sir Robin, the Not-quite-so-brave-as-Sir-Launcelot

Instance vs. class members

One can define a variable in a class but outside a method. This is a class member, it is the same for all of the instances.

For example the "Sir" title of knights is the same for all knights. Let's call this special_title, not to interfere with the Persons title member.

The Knight.special_title will be the same as any instance's .special_title.

In [23]:
class Person(object):
    def __init__(self, name, title):
        self.name = name
        self.title = title
    def __str__(self):
        return self.title + " " + self.name

class Knight(Person):
    special_title = 'Sir'
    def __init__(self, name, epithet=""):
        super(Knight, self).__init__(name, "")
        self.epithet = epithet
    def __str__(self):
        return Knight.special_title + " " + self.name + ", " + self.epithet
         
launcelot = Knight('Launcelot', 'the brave')
robin = Knight('Robin', 'the Not-quite-so-brave-as-Sir-Launcelot')
print launcelot
print robin
Sir Launcelot, the brave
Sir Robin, the Not-quite-so-brave-as-Sir-Launcelot
In [24]:
print robin.special_title, launcelot.special_title, Knight.special_title
Sir Sir Sir

The child class can be a parent of a third class, the inheritence structure can be deeper and more complicated.

Exceptions

There are cases when your code does something wrong or encounters some invalid value. In this case an object of type Exception is raised (or thrown).

What happens:

  • the code stops its proper control flow
  • every function stops immediately
  • if the exception happend outside of a function, then the code stops at that point.

Unless

  • The code handles (catches) the exception with the following block:
    try:
        ...
    except ... :
        ...

An exception can be handled at any place with this block. If it is not handled, then the exception can break out from any functions.

For example let's raise an exception:

In [25]:
try:
    raise Exception('spam', 'eggs')
except Exception as inst:
    print type(inst)    
    print inst.args      
    print inst           
    x, y = inst.args
    print 'x =', x
    print 'y =', y
<type 'exceptions.Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

Now handle an other exception:

In [26]:
try:
    str(5) + 5
except TypeError as inst:
    print type(inst)
    print inst.args     
    print inst           
<type 'exceptions.TypeError'>
("cannot concatenate 'str' and 'int' objects",)
cannot concatenate 'str' and 'int' objects

It is a natural way to handle exceptions in order to correct mistakes.

For example read an integer from the user, but prepare that the user might not enter a proper integer.

In [27]:
while True:
    try:
        x = float(raw_input("Please enter a real number: "))
        break
    except ValueError:
        print "Oops!  That was not a real number. Try again..."
print 2*x
Please enter a real number: 3,14
Oops!  That was not a real number. Try again...
Please enter a real number: 3.14
6.28

Exception across functions

Look where the exception happens and where is it handled.

In [28]:
def f(x):
    return x + 5 # not good if x is a string

def g(x):
    return x + x # good for integers and strings too

x = "5"
y = g(x)
z = f(y)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-28-ec3d5081c890> in <module>()
      7 x = "5"
      8 y = g(x)
----> 9 z = f(y)

<ipython-input-28-ec3d5081c890> in f(x)
      1 def f(x):
----> 2     return x + 5 # not good if x is a string
      3 
      4 def g(x):
      5     return x + x # good for integers and strings too

TypeError: cannot concatenate 'str' and 'int' objects
In [29]:
def read(n):
    maximum = float("-inf")
    for i in range(n):
        x = float(raw_input())
        if x > maximum:
            maximum = x
    return maximum

try:
    y = read(3)
except ValueError as e:
    print e
1
2
x
could not convert string to float: x

Custom exceptions

You can write your own exception class. You can use it to mark that the error happened in one of your own code.

All you have to do is to inherit from the built-in Exception class. Optionally, you can have other functions or members in the exception class.

In [30]:
class KnightException(Exception):
    pass

lancelot = Person("Lancelot", "Mr")
x = str(lancelot)
if x[:3] != "Sir":
    raise KnightException("a", "b")
---------------------------------------------------------------------------
KnightException                           Traceback (most recent call last)
<ipython-input-30-d2e26e0132e9> in <module>()
      5 x = str(lancelot)
      6 if x[:3] != "Sir":
----> 7     raise KnightException("a", "b")

KnightException: ('a', 'b')

Iterable objects

As we have seen, the for loop can work on several data types:

for i in L:

L can be list, tuple, dict. What type of objects can follow for ... in ?

What happens in a for loop?

  • for calls the iter() function, which returns an iterable object.
    • This marks the beginning of the loop.
  • The iterable object has a next() function which returns the next elements.
    • This goes through the elements
  • or raises a StopIteration exception if it ran out of elements.
    • This marks the end of the loop.

An object is iterable if it has a next method. Any object with an __iter__ method can stand after for if it returns an iterable object.

These are special methods!

In [31]:
r = range(3)
it = iter(r)
next(it)
Out[31]:
0
In [32]:
print next(it)
print next(it)
1
2
In [33]:
next(it)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-33-bc1ab118995a> in <module>()
----> 1 next(it)

StopIteration: 

You can write your own iterable and tell python how to iterate over it.

First you need an __iter__ method, the returned object can be the self or a built-in iterable (list, set, tuple). The iterable object have to define a next() method which returns the elements one after the other.

Raise StopIteration when you want to stop.

In [34]:
class Group(object):
    def __init__(self, name, persons):
        self.persons = persons
        self.name = name
        
    def __iter__(self):
        self.index = 0
        return self
    
    def next(self):
        if self.index >= len(self.persons):
            raise StopIteration     # tell to stop
        self.index += 1
        return self.persons[self.index - 1]
        
kotrt = Group('Knights of The Round Table', 
              [Knight('Launcelot', 'the brave'), 
               Knight('Galahad', 'the pure'),
               Knight('Bedevere', 'the wise'), 
               Knight('Robin', 'the Not-quite-so-brave-as-Sir-Launcelot')])
for knight in kotrt:
    print knight
Sir Launcelot, the brave
Sir Galahad, the pure
Sir Bedevere, the wise
Sir Robin, the Not-quite-so-brave-as-Sir-Launcelot

Mind that you have to re-start the index in the __iter__ otherwise you could iterate only once over the object.

In this case you could solve this easier, since a list is already iterable:

In [35]:
class Group:
    def __init__(self, name, persons):
        self.persons = persons
        self.name = name
    def __iter__(self):
        return iter(self.persons)
        
kotrt = Group('Knights of The Round Table', 
              [Knight('Launcelot', 'the brave'), 
               Knight('Galahad', 'the pure'),
               Knight('Bedevere', 'the wise'), 
               Knight('Robin', 'the Not-quite-so-brave-as-Sir-Launcelot')])
for knight in kotrt:
    print knight
Sir Launcelot, the brave
Sir Galahad, the pure
Sir Bedevere, the wise
Sir Robin, the Not-quite-so-brave-as-Sir-Launcelot
In [ ]: