Object Oriented Programming in Python

In [1]:
import addutils.toc ; addutils.toc.js(ipy_notebook=True)
Out[1]:

Object Oriented (OOP) is a programming paradigm that allows abstraction through the concept of interacting entities. This is usally opposed to a more conventional model, called procedural, in which programs are organized as a sequence of commands (statements) to perform. We can think an object as an entity that resides in memory, has a states and it's able to perform some actions.

More formally objects are entities that represent instances of a general abstract concept called class. In Python the variables defining an object state are called attributes and the possible actions are called methods.

In Python everything is an object also classes and functions.

In [2]:
from addutils import css_notebook
css_notebook()
Out[2]:

1 How to define classes

1.1 Creating a class

Suppose we want to create a class, named Person, as a prototype, a sort of template for any number of 'Person' objects (instances).

The python syntax to define a class is the following:

class ClassName(base_classes):
    statements

Class names should always be uppercase (it's a naming convention).

Say we need to model a Person as:

  • Name
  • Surname
  • Age
In [3]:
class Person:
    pass

john_doe = Person()
john_doe.name = "Alec"
john_doe.surname = "Baldwin"
john_doe.year_of_birth = 1958


print(john_doe)
print("%s %s was born in %d." %
      (john_doe.name, john_doe.surname, john_doe.year_of_birth))
<__main__.Person object at 0x7fb64bb540b8>
Alec Baldwin was born in 1958.

The preceding example defines an empty class (i.e. the class doesn't have a state) called Person then creates a Person instance called _johndoe and adds three attributes to _johndoe. We see that we can access objects attributes using the . dot operator.

This isn't a raccomended style because classes should describe omogeneous entities. A way to do so is the following:

In [4]:
class Person:
    def __init__(self, name, surname, year_of_birth):
        self.name = name
        self.surname = surname
        self.year_of_birth = year_of_birth
__init__(self, ...)

Is a special Python method that is automatically called after an object construction, its purpose is to initialize every object state. The first argument (by convention) self is automatically passed either and refers to the object itself.

In the preceding example __init__ adds three attributes to every object that is instantiated. So the class is actually describing each object's state.

A class isn't something which we can directly manipulate, we need to create an instance of the class:

In [5]:
alec = Person("Alec", "Baldwin", 1958)
print(alec)
print("%s %s was born in %d." % 
      (alec.name, alec.surname, alec.year_of_birth))
<__main__.Person object at 0x7fb64bb54ef0>
Alec Baldwin was born in 1958.

We have just created an instance of the Person class, bound to the variable alec.

1.2 Methods

In [6]:
class Person:
    def __init__(self, name, surname, year_of_birth):
        self.name = name
        self.surname = surname
        self.year_of_birth = year_of_birth
    
    def age(self, current_year):
        return current_year - self.year_of_birth
    
    def __str__(self):
        return "%s %s was born in %d ." % (self.name, self.surname, self.year_of_birth)
    
alec = Person("Alec", "Baldwin", 1958)
print(alec)
print(alec.age(2014))
Alec Baldwin was born in 1958 .
56

We defined two more methods age and __str__. The latter is once again a special method that is called by Python when the object has to be represented as a string (e.g. when has to be printed). If the __str__ method isn't defined the print command shows the type of object and its address in memory. We can see that in order to call a method we use the same syntax for attributes (instance_name.instance _method).

1.3 Bad practice

It is possible to create a class without the __init__ method, but this isn't a raccomended style because classes should describe omogeneous entities.

In [7]:
class Person:
  
    def set_name(self, name):
        self.name = name
        
    def set_surname(self, surname):
        self.surname = surname
        
    def set_year_of_birth(self, year_of_birth):
        self.year_of_birth = year_of_birth
        
    def age(self, current_year):
        return current_year - self.year_of_birth
    
    def __str__(self):
        return "%s %s was born in %d ." \
                % (self.name, self.surname, self.year_of_birth)
    

In this case an empty instance of the class Person is created, and no attributes have been initialized while instantiating:

In [8]:
president = Person()
In [10]:
# This code will raise an attribute error:
print(president.name)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-10-004adac081fd> in <module>()
      1 # This code will raise an attribute error:
----> 2 print(president.name)

AttributeError: 'Person' object has no attribute 'name'

This raises an Attribute Error... We need to set the attributes:

In [11]:
president.set_name('John')
president.set_surname('Doe')
president.set_year_of_birth(1940)
In [12]:
print('Mr', president.name, president.surname,
      'is the president, and he is very old. He is',
      president.age(2014))
Mr John Doe is the president, and he is very old. He is 74

1.4 Protect your abstraction

Since objects are a powerfull mean of abstraction they shouldn't reveal internal implementation detail, hence instance attributes shouldn't be accessible by the end user of an object. Python doesn't have a strict mechanism to protect objects attributes, but official guidelines suggest that a variable that has an underscore _ prefix should be treated as private. Moreover prepending two underscores to a variable name makes the intepreter mangle a little the variable name.

In [13]:
class Person:
    def __init__(self, name, surname, year_of_birth):
        self._name = name
        self._surname = surname
        self._year_of_birth = year_of_birth
    
    def age(self, current_year):
        return current_year - self._year_of_birth
    
    def __str__(self):
        return "%s %s and was born %d." \
                % (self._name, self._surname, self._year_of_birth)
    
alec = Person("Alec", "Baldwin", 1958)
print(alec)
print(alec.age(2014))
Alec Baldwin and was born 1958.
56
In [14]:
class Person:
    def __init__(self, name, surname, year_of_birth):
        self.__name = name
        self.__surname = surname
        self.__year_of_birth = year_of_birth
    
    def age(self, current_year):
        return current_year - self.__year_of_birth
    
    def __str__(self):
        return "%s %s and was born %d." \
                % (self.__name, self.__surname, self.__year_of_birth)
    
alec = Person("Alec", "Baldwin", 1958)
print(alec.__dict__.keys())
dict_keys(['_Person__name', '_Person__surname', '_Person__year_of_birth'])

__dict__ is a special attribute that is a dictionary containing each attribute of an object. We can see that prepending two underscores every key has _ClassName__ prepended.

2 Inheritance

Once a class is defined it models a concept. Often is usefull to extend a class behavior to model a less general concept. Say we need to model a Student, but we know that every student is also a Person so we shouldn't model the Person again but inherit from it instead.

In [15]:
class Student(Person):
    def __init__(self, student_id, *args, **kwargs):
        super(Student, self).__init__(*args, **kwargs)
        self._student_id = student_id
        
charlie = Student(1, 'Charlie', 'Brown', 2006)
print(charlie)
print(type(charlie))
print(isinstance(charlie, Person))
print(isinstance(charlie, object))
Charlie Brown and was born 2006.
<class '__main__.Student'>
True
True

Charlie now has the same behavior of a Person, but his state has also a student ID. Person is one of the base classes of Student and Student is one of the sub classes of Person. Be aware that a subclass knows about its superclasses but the converse isn't true.

A sub class doesn't only inherits from its base classes, but from its base classes base classes too, forming an inheritance tree that starts from object (every class base class).

super(Class, instance)

is a function that returns a proxy-object that delegates method calls to a parent or sibling class of type. So we used it to access Person's __init__.

2.1 Overriding methods

Inheritance allows to add new methods to a subclass, but often is usefull to change the behavior of a method defined in the superclass. To override a method just define it again.

In [16]:
class Student(Person):
    def __init__(self, student_id, *args, **kwargs):
        super(Student, self).__init__(*args, **kwargs)
        self._student_id = student_id
        
    def __str__(self):
        return super(Student, self).__str__() + " And has ID: %d" % self._student_id
        
charlie = Student(1, 'Charlie', 'Brown', 2006)
print(charlie)
Charlie Brown and was born 2006. And has ID: 1

We defined __str__ again overriding the one wrote in Person, but we wanted to extend it, so we used super to achieve our goal.

3 Encapsulation

Another powerful way to extend a class is called encapsulation and consists on wrapping an object with a second one. There are two main reasons to use encapsulation:

  • Composition
  • Dynamic Extension

3.1 Composition

The abstraction process relies on creating a simplified model that remove useless details from a concept. In order to be simplified a model should be described in terms of other simpler concepts. For example we can say that a car is composed by:

  • Tyres
  • Engine
  • Body

And break down each one of this elements in simpler parts until we reach primitive data.

In [17]:
class Tyres:
    def __init__(self, branch, belted_bias, opt_pressure):
        self.branch = branch
        self.belted_bias = belted_bias
        self.opt_pressure = opt_pressure
        
    def __str__(self):
        return ("Tyres: \n \tBranch: " + self.branch +
               "\n \tBelted-bias: " + str(self.belted_bias) + 
               "\n \tOptimal pressure: " + str(self.opt_pressure))
        
class Engine:
    def __init__(self, fuel_type, noise_level):
        self.fuel_type = fuel_type
        self.noise_level = noise_level
        
    def __str__(self):
        return ("Engine: \n \tFuel type: " + self.fuel_type +
                "\n \tNoise level:" + str(self.noise_level))
        
class Body:
    def __init__(self, size):
        self.size = size
        
    def __str__(self):
        return "Body:\n \tSize: " + self.size
        
class Car:
    def __init__(self, tyres, engine, body):
        self.tyres = tyres
        self.engine = engine
        self.body = body
        
    def __str__(self):
        return str(self.tyres) + "\n" + str(self.engine) + "\n" + str(self.body)

        
t = Tyres('Pirelli', True, 2.0)
e = Engine('Diesel', 3)
b = Body('Medium')
c = Car(t, e, b)
print(c)
Tyres: 
 	Branch: Pirelli
 	Belted-bias: True
 	Optimal pressure: 2.0
Engine: 
 	Fuel type: Diesel
 	Noise level:3
Body:
 	Size: Medium

3.2 Dynamic Extension

Sometimes it's necessary to model a concept that may be a subclass of another one, but it isn't possible to know which class should be its superclass until runtime.

3.2.1 Example

Suppose we want to model a simple dog school that trains instructors too. It will be nice to re-use Person and Student but students can be dogs or peoples. So we can remodel it this way:

In [18]:
class Dog:
    def __init__(self, name, year_of_birth, breed):
        self._name = name
        self._year_of_birth = year_of_birth
        self._breed = breed

    def __str__(self):
        return "%s is a %s born in %d." % (self._name, self._breed, self._year_of_birth)

kudrjavka = Dog("Kudrjavka", 1954, "Laika")
print(kudrjavka)
Kudrjavka is a Laika born in 1954.
In [19]:
class Student:
    def __init__(self, anagraphic, student_id):
        self._anagraphic = anagraphic
        self._student_id = student_id
    def __str__(self):
        return str(self._anagraphic) + " Student ID: %d" % self._student_id


alec_student = Student(alec, 1)
kudrjavka_student = Student(kudrjavka, 2)

print(alec_student)
print(kudrjavka_student)
Alec Baldwin and was born 1958. Student ID: 1
Kudrjavka is a Laika born in 1954. Student ID: 2

4 Polymorphism and DuckTyping

Python uses dynamic typing (also called duck typing). If an object implements a method you can use it, no matter what the type is. This is different from statically typed languages, where the type of a construct need to be explicitly declared. Polymorphism is the ability to use the same syntax for objects of different types:

In [20]:
def summer(a, b):
    return a + b

print(summer(1, 1))
print(summer(["a", "b", "c"], ["d", "e"]))
print(summer("abra", "cadabra"))
2
['a', 'b', 'c', 'd', 'e']
abracadabra

5 How long does a class should be?

There is an Object Oriented Programming principle called SRP single responsability principle and it states: "A class should have one single responsability" or "A class should have only one reason to change". If you come up with a class that doesn't follow this principle you should consider to split it. You will be grateful to SRP during your software manteinance.


Visit www.add-for.com for more tutorials and updates.

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.