Practice: intro to classes#
Let us suppose we have a set of weather stations that do measurements of wind speed and temperature in a weather station (for example in Paris). We need to store these measurements in a clear way so that computing statistics (maximum wind, average temperature…) is easy and not error prone. This chapter shows different ways of storing the data, its advantages and disadvantages.
The basic way: only with lists#
Exercise 21 (A list of lists)
A basic representation of our Paris weather station will be a unique list of lists. Write one list with two elements containing the wind values and temperature values. Find the values of the maximum temperature and wind.
wind_values = [10, 0, 20, 30, 20, 4]
temperature_values = [1, 5, 1, -1, -1, 3]
Solution to Exercise 21 (A list of lists)
paris = [wind_values, temperature_values]
# get wind when temperature is maximal
idx_max_temp = paris[1].index(max(paris[1]))
print(f"max temp is {paris[1][idx_max_temp]}°C at index {idx_max_temp} ")
print(f"wind speed at max temp = {paris[0][idx_max_temp]} km/h")
max temp is 5°C at index 1
wind speed at max temp = 0 km/h
More complex structures: mixing dicts and lists#
Exercise 22 (A dict of lists)
Instead of storing everything in lists, use a dictionnary to store the different types of measurements
Solution to Exercise 22 (A dict of lists)
paris = {"wind": wind_values, "temperature": temperature_values}
# get wind when temperature is minimal
paris_temp = paris["temperature"]
idx_max_temp = paris_temp.index(max(paris_temp))
print(f"max temp is {paris_temp[idx_max_temp]}°C at index {idx_max_temp}")
print(f"wind speed at max temp = {paris['wind'][idx_max_temp]} km/h")
max temp is 5°C at index 1
wind speed at max temp = 0 km/h
Comments#
Pro
More readable code (reading
paris["temperature"]is clearer thanparis[1]).Less error prone code (i.e. using words as keys allow to not use index numbers that are easily mistaken and lead to code that is hard to read and debug)
Con
The code to compute the final result is not very readable
Improve database: add functions#
Exercise 23 (Improve the dict weather station)
Write a function that returns the maximum temperature and another one that returns the index of the maximum temperature. It should work on the dict you wrote in the previous exercise.
Solution to Exercise 23 (Improve the dict weather station)
paris = {"wind": wind_values, "temperature": temperature_values}
def max_temp(station):
"""returns the maximum temperature available in the station"""
return max(station["temperature"])
def arg_max_temp(station):
"""returns the index of maximum temperature available in the station"""
max_temperature = max_temp(station)
return station["temperature"].index(max_temperature)
idx_max_temp = arg_max_temp(paris)
print(f"max temp is {max_temp(paris)}°C at index {arg_max_temp(paris)}")
print(f"wind speed at max temp = {paris['wind'][idx_max_temp]} km/h")
max temp is 5°C at index 1
wind speed at max temp = 0 km/h
Comments#
Pro:
Adding functions leads to a code that is easier to read, hence easier to debug.
Testing functions can be done separately from the rest of the code.
The computation done on the second part depends upon the functions (i.e it depends on the function definitions not their implementations).
Adding function allows to reuse code: computing the max temperature is something one could want to do in other places.
Con
We rely on the fact that the dictionaries have been built correctly (for example wind and temperature arrays have the same length).
Continue improving: build a init function#
Exercise 24 (Build an init function)
Define a function that builds the station (delegate the generation of the station dictionary to a function). The init function should test if the two set of measurements have the same length, and stop the execution if not.
Solution to Exercise 24 (Build an init function)
def build_station(wind, temp):
"""Build a station given wind and temp
:param wind: (list) floats of winds
:param temp: (list) float of temperatures
"""
if len(wind) != len(temp):
raise ValueError("wind and temperature should have the same size")
return {"wind": list(wind), "temperature": list(temp)}
def max_temp(station):
"""returns the maximum temperature available in the station"""
return max(station["temperature"])
def arg_max_temp(station):
"""returns the index of maximum temperature available in the station"""
max_temperature = max_temp(station)
return station["temperature"].index(max_temperature)
paris = build_station(wind_values, temperature_values)
idx_max_temp = arg_max_temp(paris)
print(f"max temp is {max_temp(paris)}°C at index {arg_max_temp(paris)}")
print(f"wind speed at max temp = {paris['wind'][idx_max_temp]} km/h")
max temp is 5°C at index 1
wind speed at max temp = 0 km/h
Comments#
If the dedicated function
build_stationis used, the returned dictionary is well structured.If one changes
build_station, onlymax_tempandarg_max_temphave to be changed accordinglyWe use a list comprehension to be able to have parameters wind and temp provided by any ordered iterable (e.g. see
test_build_station_with_iterablewtihrange)BUT if we have a new kind of station, i.e. that holds only wind and humidity, we want to avoid to be able to use
max_tempwith it.
Last improvement: using a class#
We would like to “embed” the max_temp and the arg_max_temp in the “dictionary
station” in order to address the last point.
And here comes object-oriented programming !
A class defines a template used for building object. In our example, the class (named
WeatherStation) defines the specifications of what is a weather station (i.e, a
weather station should contain an array for wind speeds, named “wind”, and an array for
temperatures, named “temp”). paris should now be an object that answers to these
specifications. Is is called an instance of the class WeatherStation.
When defining the class, we need to define how to initialize the object (special
“function” __init__).
Exercise 25 (Build the class)
We provide a skeleton for the class. Take the code defined in the previous exercises and fill the holes
class WeatherStation:
"""A weather station that holds wind and temperature
:param wind: any ordered iterable
:param temperature: any ordered iterable
wind and temperature must have the same length.
"""
def __init__(self, wind, temperature):
"""initialize the weather station.
Precondition: wind and temperature must have the same length.
ValueError is raised if this is not the case
:param wind: any ordered iterable
:param temperature: any ordered iterable"""
pass
def max_temp(self):
"""returns the maximum temperature recorded in the station"""
pass
def arg_max_temp(self):
"""returns the index of (one of the) maximum temperature recorded in the station"""
pass
Solution to Exercise 25 (Build the class)
class WeatherStation:
"""A weather station that holds wind and temperature
:param wind: any ordered iterable
:param temperature: any ordered iterable
wind and temperature must have the same length.
"""
def __init__(self, wind, temperature):
"""initialize the weather station.
Precondition: wind and temperature must have the same length.
ValueError is raised if this is not the case
:param wind: any ordered iterable
:param temperature: any ordered iterable"""
self.wind = list(wind)
self.temp = list(temperature)
if len(self.wind) != len(self.temp):
raise ValueError(
"wind and temperature should have the same size"
f" got len(wind)={len(self.wind)} vs "
f" len(temp)={len(self.temp)}"
)
def max_temp(self):
"""returns the maximum temperature recorded in the station"""
return max(self.temp)
def arg_max_temp(self):
"""returns the index of (one of the) maximum temperature recorded in the station"""
return self.temp.index(self.max_temp())
paris = WeatherStation(wind_values, temperature_values)
idx_max_temp = paris.arg_max_temp()
print(f"max temp is {paris.max_temp()}°C at index {paris.arg_max_temp()}")
print(f"wind speed at max temp = {paris.wind[idx_max_temp]} km/h")
max temp is 5°C at index 1
wind speed at max temp = 0 km/h
Comments#
The
max_tempand thearg_max_tempare now part of the classWeatherStation. Functions attached to classes are named methods. Similarly,windandtemplists are also now part this class. Variables attached to classes are named members or attributes.if
max_tempmethod is called in many places, we can improve it by caching the result. This will not affect code the uses the class.arg_max_tempmethod should be rewritten as we implicitly check equality of floats.
An object (here paris) thus contains both attributes (holding data for example) and
methods to access and/or process the data.
Exercise 26 (Try to code with class)
Add a method (
perceived_temp) that takes as input a temperature and wind and return the perceived temperature, i.e. taking into account the wind chill effect.Modify
max_tempandarg_max_tempso that they take an additional optional boolean parameter (e.g. perceived default to False). Ifperceivedis False, the methods have the same behaviour as before. If perceived is True, the temperatures to process are the perceived temperatures.
Solution to Exercise 26 (Try to code with class)
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
ValueError is raised if this is not the case
: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"
f" got len(wind)={len(self.wind)} vs "
f" len(temp)={len(self.temp)}"
)
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 avoid trailing decimals...
return round(apparent_temp, 2)
def perceived_temperatures(self):
"""Returns an array of perceived 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):
"""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):
"""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))
Comments#
The wind array was changed to have different maximum temperatures for the air and perceived temperatures: for air temperatures, the max is 5°C (with a wind speed 50 km/h). For perceived temperatures, the max is 3°C (as the wind speed is 0).
It was a choice to set the apparent/perceived temperature to the air temperature if the wind speed is 0 so the tests were written with this in mind. Testing such choices allows to have clear inputs/outputs.
isinstanceallows to test the type of an object (in this case, we test ifapparent_tempsis a list)When testing boolean in
ifstructures: useif perceived:rather thanif perceived == True:. It is equivalent but clearer and shorter !
Coming next: inheritance#
What if we now have a weather station that also measure humidity ?
Do we need to rewrite everything ?
What if we rewrite everything and we find a bug ?
Here comes inheritance
Comments on this solution#
Many problems:
if the number of measurements increases (e.g. having rainfall, humidity, …) the previous indexing will not be valid (what will
paris[5]represent? wind, temperature, …, ?)Code analysis is not (that) straightforward