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)
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.
An object is an entity that has state and behavior. Objects are the basic elements of object oriented system.
Classes are "families" of objects. A class describes how are organized its objects and how they work.
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.
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
.
We can create objects AdultBee
. We say that we instantiate objects of the class AdultBee
.
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...
bee2 = AdultBee('mother2', 'father2', tag='2')
__init__
is automatically called like this (for bee0
):AdultBee.__init__(bee0, 'mother0', 'father0', tag='0')
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.
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.
__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.
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...
# 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 :-(
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
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')
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
.
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
.
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)
We can define our own exceptions classes inheriting from an exception class.
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
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
:
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.
For some situation we don't even need to explicitly use the class or an instance. We can use static methods.
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)
p1 = IdPerson('Pierre')
p2 = IdPerson('Cyrille')
p3 = IdPerson('Olivier')
p4 = IdPerson('Franck')
IdPerson.show_nb_person()
Number of persons created: 4
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:
HumidWeatherStation
inheriting WeatherStation
(code reproduced below) to implement a new attribute to store the humidity measurements.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 !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.# 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))
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:
humidity_at_max_temp
) to an existing object without rewritting the common featuresperceived_temp
) that integrate well with the structure in placeFor such needs, think about inheritance.
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.
# 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