Commit b37940d7 authored by paugier's avatar paugier
Browse files

Check / improve OOP presentations

parent d23dc77e
This diff is collapsed.
%% Cell type:markdown id: tags:
# 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
See https://docs.python.org/3/tutorial/classes.html
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.
%% Cell type:markdown id: tags:
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.
%% Cell type:markdown id: tags:
## Example of problem: Simulate populations of honeybees
<img src="https://static.independent.co.uk/s3fs-public/styles/story_large/public/thumbnails/image/2013/06/05/18/web-bees-epa.jpg" title="bee" alt="image bee" width="30%">
#### Hierarchy of honeybees
The "adult" bees can be:
- queen
- workers
- fertile males
Each type of adult bee have different characteristics, behaviors, activities and tasks.
%% Cell type:markdown id: tags:
## 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.
%% Cell type:code id: tags:
``` python
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
```
%% Cell type:markdown id: tags:
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`.
- `kind` and `limit_age` are **class variables**,
- `mother`, `father`, `tag` and `living` are **instance variables**,
- `__init__` is a **"special" method**,
- `act_and_envolve` and `die` are **methods**.
%% Cell type:markdown id: tags:
### Instantiation of a class
We can create objects `AdultBee`. We say that we *instantiate objects of the class `AdultBee`*.
%% Cell type:code id: tags:
``` python
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]
```
%% Cell type:markdown id: tags:
In this example, we manipulate the notions of class, object (instance), abstraction and encapsulation...
#### Syntax to create an object
%% Cell type:code id: tags:
``` python
bee2 = AdultBee('mother2', 'father2', tag='2')
```
%% Cell type:markdown id: tags:
#### What happens...
- the Python object is first created
- the object is initialized, i.e. the method `__init__` is automatically called like this (for `bee0`):
```python
AdultBee.__init__(bee0, 'mother0', 'father0', tag='0')
```
%% Cell type:markdown id: tags:
### 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.
%% Cell type:markdown id: tags:
### 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.
### Warning for C++ developers
`__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. All methods in Python are effectively virtual.
%% Cell type:markdown id: tags:
### Use the objects (instances)
%% Cell type:code id: tags:
``` python
print('second generation:', bee_second_generation0.tag)
print('third generation; ', bee_third_generation.tag)
print('warning: consanguinity...')
```
%% Output
second generation: ('0', '1')
third generation; (('0', '1'), ('0', '1'))
warning: consanguinity...
%% Cell type:code id: tags:
``` python
# 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 :-(')
```
%% Output
After 100 days, no more bees :-(
%% Cell type:markdown id: tags:
## Inheritance
%% Cell type:markdown id: tags:
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`
To indicate the dependency to another class, we put the parent class in parenthesis in the definition. The class `QueenBee` inherits from the class `AdultBee`
%% Cell type:code id: tags:
``` python
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')
```
%% Cell type:markdown id: tags:
- 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`.
%% Cell type:markdown id: tags:
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`.
%% Cell type:code id: tags:
``` python
queen = QueenBee('mother0', 'father0', tag='0')
print(dir(queen))
```
%% Output
['__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']
%% Cell type:markdown id: tags:
All the methods that star with the prefix `__` are inherited from the class `object`. All classes in Python3 inherit from `object`.
%% Cell type:code id: tags:
``` python
queen.act_and_envolve()
```
%% Output
I am the Queen!
%% Cell type:markdown id: tags:
### `super` function
We have used the function `super()` like this to call a function of the parent class:
```python
super().act_and_envolve(duration)
```
Remark: the python 2 syntax was more complicated. We would have to write:
```python
super(QueenBee, self).act_and_envolve(duration)
```
Remark: we can also call the method explicitly:
```
AdultBee.act_and_envolve(self, duration)
```
%% Cell type:markdown id: tags:
## Remark: the exceptions are classes...
We can define our own exceptions classes inheriting from an exception class.
%% Cell type:code id: tags:
``` python
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')
```
%% Output
A ValueError was raised
%% Cell type:markdown id: tags:
## Static methods and class methods (advanced)
%% Cell type:markdown id: tags:
### "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`:
%% Cell type:code id: tags:
``` python
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()
```
%% Output
The role for this class is student.
%% Cell type:markdown id: tags:
### "Static methods"
### "Static methods" and class variables
For some situation we don't even need to explicitly use the class or an instance. We can use static methods.
%% Cell type:code id: tags:
``` python
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)
```
%% Cell type:code id: tags:
``` python
p1 = IdPerson('Pierre')
p1 = IdPerson('Maya')
p2 = IdPerson('Cyrille')
p3 = IdPerson('Olivier')
p4 = IdPerson('Franck')
IdPerson.show_nb_person()
```
%% Output
Number of persons created: 4
%% Cell type:markdown id: tags:
## 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
%% Cell type:code id: tags:
``` python
# 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))
```
%% Cell type:markdown id: tags:
### A Solution
%% Cell type:code id: tags:
``` python
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)