# 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¶

#### 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_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!')

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 ?

• 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