Python training UGA 2017

A training to acquire strong basis in Python to use it efficiently

Pierre Augier (LEGI), Cyrille Bonamy (LEGI), Eric Maldonado (Irstea), Franck Thollard (ISTerre), Christophe Picard (LJK), Loïc Huder (ISTerre)

Object-oriented programming: inheritance

Python is also a Object-oriented language. Object-oriented programming is very useful and used in many libraries so it is very useful to understand how the simple Object-oriented mechanisms work in Python.

For some problems, Object-oriented programming is a very efficient paradigm. Many libraries use it so it is necessary to understand how it works in Python to really use these libraries.

Concepts

Object

An object is an entity that has state and behavior. Objects are the basic elements of object oriented system.

Class

Classes are "families" of objects. A class describes how are organized its objects and how they work.

Example of problem: Simulate populations of honeybees

image bee

Hierarchy of honeybees

The "adult" bees can be:

  • queen
  • workers
  • fertile males

Each type of adult bee have different characteristics, behaviors, activities and tasks.

Class definition

A class is a logical entity which contains attributes and have some behavior. When a class is defined, we need to specify its name, its dependencies (the class inherits from at least one other class), its attributes and its methods.

In [1]:
class AdultBee(object):
    kind = None
    limit_age = 50.

    def __init__(self, mother, father, tag=None):
        self.mother = mother
        self.father = father
        
        if tag is None:
            self.tag = (self.mother.tag, self.father.tag)
        else:
            self.tag = tag
        
        # age in days
        self.age = 0.
        self.living = True
        
    def act_and_envolve(self, duration=1):
        """Time stepping method"""
        self.age += duration
        if self.age > self.limit_age:
            self.die()
    
    def die(self):
        self.living = False        

The first line states that instances of the class AdultBee will be Python objects. The class AdultBee inherits from the class object.

The first line could have been replaced by the less explicit class AdultBee:. Actually, in Python 3, the classes that do not inherit explicitly from another class automatically inherit from the class object.

 Instantiation of a class

We can create objects AdultBee. We say that we instantiate objects of the class AdultBee.

In [2]:
bee0 = AdultBee('mother0', 'father0', tag='0')
bee1 = AdultBee('mother1', 'father1', tag='1')

bee_second_generation0 = AdultBee(bee0, bee1)
bee_second_generation1 = AdultBee(bee0, bee1)

bee_third_generation = AdultBee(
    bee_second_generation0, bee_second_generation1)

bees = [bee0, bee1, bee_second_generation0, bee_second_generation1, bee_third_generation]

In this example, we manipulate the notions of class, object (instance), abstraction and encapsulation...

Syntax to create an object

In [3]:
bee2 = AdultBee('mother2', 'father2', tag='2')

What happens...

  • the Python object is first created
  • the object is initialized, i.e. the method __init__ is automatically called like this (for bee0):
AdultBee.__init__(bee0, 'mother0', 'father0', tag='0')

Special methods and attributes

In Python, methods or attributes that starts with __ are "special". Such methods and attributes are used internally by Python. They define how the class works internally.

For example the method __init__ is automatically called by Python during the instantiation of an object with the arguments used for the instantiation.

Protected methods and attributes (no notion of public, private, virtual as in C++)

Attributes and methods whose names start with _ are said to be "protected". It is just a name convention. It tells the users that they should not use these objects directly.

Warning for C++ users

__init__ is NOT the constructor. The real constructor is __new__. This method is called to really create the Python object and it really returns the object. Usually, we do not need to redefine it. Python __init__ and C++ constructor have to be used in very different ways. Only the __init__ method of the class is automatically called by Python during the instantiation. Nothing like the Russian dolls C++ mechanism.

Use the objects (instances)

In [4]:
print('second generation:', bee_second_generation0.tag)
print('third generation; ', bee_third_generation.tag)
print('warning: consanguinity...')
second generation: ('0', '1')
third generation;  (('0', '1'), ('0', '1'))
warning: consanguinity...
In [5]:
# 100 days
for i in range(100):
    for bee in bees:
        bee.act_and_envolve()
    bees = [bee for bee in bees if bee.living]
        
if len(bees) == 0:
    print('After 100 days, no more bees :-(')
After 100 days, no more bees :-(

Inheritance

To indicate the dependency to an other class, we put the parent class in parenthesis in the definition. The class QueenBee inherits from the class AdultBee

In [6]:
class QueenBee(AdultBee):
    kind = 'queen'
    limit_age = 4*365
    
    def act_and_envolve(self, duration=1):
        """Time stepping method"""
        super().act_and_envolve(duration)
        print('I am the Queen!')

class WorkerBee(AdultBee):
    kind = 'worker'
    # actually it depends on the season...
    limit_age = 6*7
    def dance(self):
        print('I communicate by dancing')
    def make_honey(self):
        print('I make honey')
  • The methods that are not rewritten are automatically inherited from the parent class.
  • The methods that are rewritten are completely replaced. To use the method of the parent class, it has to be called explicitly (for example with the super() function).

We see that we do not need to rewrite all methods. For example the method __init__ of the class QueenBee is the method __init__ of the class AdultBee.

The class AdultBee that we defined is also derived from a more generic class that is called object. Let's check the content of the class QueenBee.

In [7]:
queen = QueenBee('mother0', 'father0', tag='0')

print(dir(queen))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'act_and_envolve', 'age', 'die', 'father', 'kind', 'limit_age', 'living', 'mother', 'tag']

All the methods that star with the prefix __ are inherited from the class object. All classes in Python3 inherit from object.

In [8]:
queen.act_and_envolve()
I am the Queen!

super function

We have used the function super() like this to call a function of the parent class:

super().act_and_envolve(duration)

Remark: the python 2 syntax was more complicated. We would have to write:

super(QueenBee, self).act_and_envolve(duration)

Remark: we can also call the method explicitly:

AdultBee.act_and_envolve(self, duration)

Remark: the exceptions are classes...

We can define our own exceptions classes inheriting from an exception class.

In [9]:
class MyValueError(ValueError):
    pass

def myfunc():
    raise MyValueError('oops')
    
try:
    myfunc()
except OSError:
    print('An OSError was raised')
except ValueError:
    print('A ValueError was raised')
A ValueError was raised

Static methods and class methods (advanced)

"Class methods"

When we simply define a method in a class, it is a instance method, i.e. the first argument of the method (self) points toward the instance used to call the method. This is the normal and most common mechanism.

We could also define methods that work for the class using the decorator @classmethod:

In [12]:
class Person(object):
    def __init__(self):
        pass

class Student(Person):
    role = 'student'
    @classmethod
    def show_role(cls):
        print('The role for this class is ' +
              cls.role + '.')

Student.show_role()
The role for this class is student.

"Static methods"

For some situation we don't even need to explicitly use the class or an instance. We can use static methods.

In [13]:
class IdPerson(Person):
    count = 0
    def __init__(self, name):
        self.name = name
        self.id = IdPerson.count
        IdPerson.count += 1
        
    @staticmethod
    def show_nb_person():
        print('Number of persons created: ', IdPerson.count)
In [14]:
p1 = IdPerson('Pierre')
p2 = IdPerson('Cyrille')
p3 = IdPerson('Olivier')
p4 = IdPerson('Franck')

IdPerson.show_nb_person()
Number of persons created:  4

Do it yourself

At the end of the last presentation, we asked the following question about our weather stations measuring wind and temperature:

What if we now have a weather station that also measure humidity ? Do we have to rewrite everything ?

Give your own answer by doing the following tasks:

  • Write a class HumidWeatherStation inheriting WeatherStation (code reproduced below) to implement a new attribute to store the humidity measurements.
  • Write a function humidity_at_max_temp that returns the value of the humidity at the maximal temperature. Use the fact that HumidWeatherStation inherits from WeatherStation and therefore can use the method arg_max_temp previously implemented !
  • Advanced: Overloadg the methods of WeatherStation to take humidity into account when computing percieved temperatures Tp. For simplicity, we will assume that Tp = Tw + 5*humidity with Tw the temperature computed with the wind chill effect.
  • Advanced: Write tests for this new class
In [15]:
# Code to use for the DIY
class WeatherStation(object):
    """ A weather station that holds wind and temperature """

    def __init__(self, wind, temperature):
        """ initialize the weather station. 
        Precondition: wind and temperature must have the same length
        :param wind: any ordered iterable
        :param temperature: any ordered iterable"""
        self.wind = [x for x in wind]
        self.temp = [x for x in temperature]
        if len(self.wind) != len(self.temp):
            raise ValueError(
                "wind and temperature should have the same size"
            )

    def perceived_temp(self, index):
        """ computes the perceived temp according to 
        https://en.wikipedia.org/wiki/Wind_chill
        i.e. The standard Wind Chill formula for Environment Canada is: 
        apparent = 13.12 + 0.6215*air_temp - 11.37*wind_speed^0.16 + 0.3965*air_temp*wind_speed^0.16
        
        :param index: the index for which the computation must be made
        :return: the perceived temperature"""
        air_temp = self.temp[index]
        wind_speed = self.wind[index]
        # Perceived temperature does not have a sense without wind...
        if wind_speed == 0:
            apparent_temp = air_temp
        else:
            apparent_temp = 13.12 + 0.6215*air_temp \
                            - 11.37*wind_speed**0.16 \
                            + 0.3965*air_temp*wind_speed**0.16
        # Let's round to the integer to avoid trailing decimals...
        return round(apparent_temp,0)
    
    def perceived_temperatures(self):
        """ Returns an array of percieved temp computed from the temperatures and wind speed data """
        apparent_temps = []
        for index in range(len(self.wind)):
            # Reusing the method perceived_temp defined above
            apparent_temperature = self.perceived_temp(index)
            apparent_temps.append(apparent_temperature)
        return apparent_temps

    def max_temp(self, perceived=False):
        """ returns the maximum temperature record in the station"""
        if perceived:
            apparent_temp = self.perceived_temperatures()
            return max(apparent_temp)
        else:
            return max(self.temp)

    def arg_max_temp(self, perceived=False):
        """ returns the index of (one of the) maximum temperature record in the station"""
        if perceived:
            temp_array_to_search = self.perceived_temperatures()
        else:
            temp_array_to_search = self.temp
        return temp_array_to_search.index(self.max_temp(perceived))

A Solution

In [16]:
class HumidWeatherStation(WeatherStation):
    """ A weather station that holds wind, temperature and humidity. Inherits from WeatherStation """
    
    def __init__(self, wind, temperature, humidity):
        """ initialize the weather station. 
        Precondition: wind, temperature and humidity must have the same length
        :param wind: any ordered iterable
        :param temperature: any ordered iterable
        :param humidity: any ordered iterable"""
        # Delegate the initialisation of wind and temperature to the mother class constructor
        super(HumidWeatherStation, self).__init__(wind, temperature)
        # Or: super().init(wind, temperature)
        
        # Add humidity treatement
        self.humidity = [x for x in humidity]
        if len(self.humidity) != len(self.temp):
            raise ValueError("humidity and temperature should have the same size")
        # If humidity and temp have the same size, humidity and wind do as well
        # as len(temp) == len(wind) is enforced from the mother class constructor
    
    def humidity_at_max_temp(self):
        """ Returns the value of humidity at the maximal temperature 
        """
        index_max_temp = self.arg_max_temp()
        return self.humidity[index_max_temp]
    
    def perceived_temp(self, index):
        """ Compute the perceived temperature according to wind_speed (wind-chill) and humidity 
        
        :param index: the index for which the computation must be made
        :return: the perceived temperature"""
        # Compute the wind-chilled temp from WeatherStation method
        wind_chilled_temp = super().perceived_temp(index)
        apparent_temp = wind_chilled_temp + 5*self.humidity[index]
        return round(apparent_temp, 2)
    
    def perceived_temps(self):
        """ Returns an array of percieved temp computed from the temperatures, wind speed and humidity data """
        apparent_temps = []
        for index in range(len(self.temp)):
            # This time, we use the perceived_temp method of HumidWeatherStation
            apparent_temperature = self.perceived_temp(index)
            apparent_temps.append(apparent_temperature)
        return apparent_temps
    
singapore = HumidWeatherStation(wind=[11, 23, 23, 19, 18, 18], 
                           temperature = [28, 33, 31, 32, 35, 34],
                           humidity = [0.78, 0.63, 0.61, 0.58, 0.5, 0.72])
print(singapore.humidity_at_max_temp()) #0.5 expected
print(singapore.max_temp()) #35 expected
# As we overloaded perceived_temp, the rest of the class features work with the new behaviour
print(singapore.perceived_temps())
print(singapore.max_temp(perceived=True))
0.5
35
[33.9, 39.15, 37.05, 37.9, 41.5, 41.6]
41.6

In this case, we used inheritance to create the new object (HumidWeatherStation) to:

  • Add new features (humidity_at_max_temp) to an existing object without rewritting the common features
  • Define new behaviours for features already present (perceived_temp) that integrate well with the structure in place

For such needs, think about inheritance.

Do it yourself (advanced)

  • Write a class named MovingObject that has at least one attribute position and implements two functions start() and stop(). These 2 functions could just print for example "starting" and "stoping" (or they could do more funny things)...

  • Write another class named Car that inherits MovingObject and overload start and stop functions.

  • Use the classes (instantiate objects and use them).

  • Options : add a static method in Car class that returns the number of cars.

In [17]:
# a solution
pollution = 0.

class MovingObject:
    
    def __init__(self, position=0., max_speed=1., acceleration=1.):
        self.position = position
        self.max_speed = max_speed
        self.acceleration = acceleration
    
    def start(self):
        print ("starting")

    def stop(self):
        print ("stoping")


class Car(MovingObject):
    count = 0

    def __init__(self, position=0., max_speed=1., acceleration=1., name='', pollution_start=0.5):
        super(Car, self).__init__(position, max_speed, acceleration)
        self.name = name
        self.pollution_start = pollution_start
        Car.count += 1

    def start(self):
        global pollution
        print ('The car ' + self.name + ' starts (vrooum)')
        pollution += self.pollution_start

    def stop(self):
        print ('The car ' + self.name + ' stops')        
        
    @staticmethod
    def get_number_of_cars():
        print ("There are " + str(Car.count) + " cars")

class Bike(MovingObject):
    pass
        
ferrari = Car(name='Ferrari')
mybike = Bike()
mybike.name = 'blue bike'
porsche = Car(name='Porsche', pollution_start=0.8)

ferrari.start()
ferrari.stop()
porsche.start()
porsche.stop()

objs = [ferrari, porsche, mybike]
for f in objs:
    f.start()
    
print(f'pollution at the end: {pollution}')
The car Ferrari starts (vrooum)
The car Ferrari stops
The car Porsche starts (vrooum)
The car Porsche stops
The car Ferrari starts (vrooum)
The car Porsche starts (vrooum)
starting
pollution at the end: 2.6