In [1]:
name = '2017-11-13-building-classes'
title = 'Building classes'
tags = 'basics, oop'
author = 'Denis Sergeev'
In [2]:
from nb_tools import connect_notebook_to_post
from IPython.core.display import HTML
html = connect_notebook_to_post(name, title, tags, author)
First, let's refresh the terminology of object-oriented programming.
Tell Python to make a new type of thing.
Two meanings: the most basic type of thing, and any instance of some thing.
What you get when you tell Python to create a class.
How you define a method of a class.
Inside the methods in a class, self is a variable for the instance/object being accessed.
The concept that one class can inherit traits from another class, much like you and your parents.
A property that classes have that are from composition and are usually variables.
A phrase to say that something inherits from another, as in a "salmon" is-a "fish."
You might end up with lots of list
s or dict
s that share the same keys to access different kinds of data associated with a single logical (geographical) object:
obs_temperature[42] = 20 # Temperature at the observational point No. 23
obs_humidity[42] = 75 # Humidity at the observational point No. 23
obs_name[42] = 'research_vessel' # Name of the obs. point No. 23
By switching to classes you could have a single list of objects, each of which has several named fields on it to address the associated data:
# Everyting at the point No. 42
obs[42].temperature = 20
obs[42].humidity = 75
obs[42].name = 'research_vessel'
Now you can keep all of the fields under one roof, which makes accessing and passing these objects around much more convenient.
It's also easier to pass around big tuples of stuff from function to function.
Note: we will have a look at Python's namedtuples
in a future session.
The OOP version:
if not garage.is_full:
garage.add(my_car)
my_car.turn_off()
garage.close()
vs the non-OOP / procedural version:
if not is_garage_full(garage):
add_car_to_garage(my_car, garage)
turn_off_car(my_car)
close_garage(garage)
E.g. GUI applications
In [3]:
class MyDescriptiveError(Exception):
pass
__getitem__()
, and x
is an instance of this class, then x[i]
is roughly equivalent to type(x).__getitem__(x, i)
.AttributeError
or TypeError
).Note: dunder-methods vs private attributes
In [4]:
class ObsPoint:
"""
Observational Point
Attributes
----------
temperature: float
Air temperature (K)
pressure: float
Air pressure (Pa)
"""
def __init__(self, temperature, pressure):
"""
"""
self.temperature = temperature
self.pressure = pressure
In [5]:
OP = ObsPoint(temperature=20, pressure=1013)
For example:
class WeatherBuoy(ObsPoint):
# ... other code ...
def calculate_wave_height(self):
# ...
In [6]:
class ObsPoint:
def __init__(self, temperature, pressure):
"""
"""
self.temperature = temperature
self.pressure = pressure
calc_density
or calc_rho
ObsPoint
with $T=25^{\circ}C$ and $p=1020~hPa$. CHECK THE UNITS!
In [7]:
class ObsPoint:
def __init__(self, temperature, pressure):
self.temperature = temperature
self.pressure = pressure
# self.density = self.pressure / (self.temperature * Rd)
def calc_density(self, Rd=287):
return self.pressure / (self.temperature * Rd)
In [8]:
OP = ObsPoint(pressure=102000, temperature=298)
Result:
In [9]:
OP.calc_density
Out[9]:
In [10]:
OP.calc_density()
Out[10]:
A possible improvement: store the result as an attribute
In [11]:
class ObsPoint:
def __init__(self, temperature, pressure):
self.temperature = temperature
self.pressure = pressure
def calc_density(self, Rd=287):
self.density = self.pressure / (self.temperature * Rd) # store the result as an attribute
return self.density
In [12]:
OP = ObsPoint(pressure=102000, temperature=298)
In [13]:
d = OP.calc_density()
In [14]:
OP.density
Out[14]:
In [15]:
print(OP)
__str__
and __repr__
"dunder" methods.
Writing your own Java-esque "tostring" methods is considered unpythonic.__str__
method should be readable. The result of __repr__
should be unambiguous.__repr__
to your classes. The default implementation for __str__
just calls __repr__
internally, so by implementing repr
support you'll get the biggest benefit.
In [16]:
class ObsPoint:
def __init__(self, temperature, pressure, title='Unknown Observational Point'):
self.temperature = temperature
self.pressure = pressure
self.title = title
def calc_density(self, Rd=287):
self.density = self.pressure / (self.temperature * Rd) # store the result as an attribute
return self.density
def __str__(self):
return '{self.title}\nwith:\ntemperature = {self.temperature:4.2f} K\npressure = {self.pressure:4.1f} Pa'.format(self=self)
def __repr__(self):
return 'ObsPoint(temperature={self.temperature!r}, pressure={self.pressure!r})'.format(self=self)
In [17]:
OP = ObsPoint(pressure=102000, temperature=298)
In [18]:
print(OP)
In [19]:
repr(OP)
Out[19]:
Or if you're lazy, at least add __repr__
:
In [20]:
class ObsStation:
def __init__(self, temperature, pressure, title='Unknown Observational Point'):
self.temperature = temperature
self.pressure = pressure
self.title = title
def calc_density(self, Rd=287):
self.density = self.pressure / (self.temperature * Rd) # store the result as an attribute
return self.density
def __repr__(self):
# __str__ falls back to __repr__
return '{self.__class__.__name__}(title={self.title!r}, temperature={self.temperature!r}, pressure={self.pressure!r})'.format(self=self)
In [21]:
OP = ObsStation(title='UEA automatic weather station', pressure=102000, temperature=298)
In [22]:
print(OP)
In [23]:
OP.__class__.__name__
Out[23]:
def __repr__(self):
return (f'{self.__class__.__name__}('
f'{self.pressure!r}, {self.temperature!r})')
__call__
method
In [24]:
class ObsPoint:
def __init__(self, temperature, pressure, title='Unknown Observational Point'):
self.temperature = temperature
self.pressure = pressure
self.title = title
def calc_density(self, Rd=287):
self.density = self.pressure / (self.temperature * Rd) # store the result as an attribute
return self.density
def __repr__(self):
# __str__ falls back to __repr__
return 'ObsPoint(title={self.title!r}, temperature={self.temperature!r}, pressure={self.pressure!r})'.format(self=self)
def __call__(self, value):
""" Print summary """
self.some_value = value
print('Very-very-very long summary'*10)
In [25]:
OP = ObsPoint(pressure=102000, temperature=298)
In [26]:
OP
Out[26]:
Now call this instance:
In [27]:
OP(123)
In [28]:
HTML(html)
Out[28]: