classDiagram class Grid{ + nx + dx + ny + dy + find_index() } class ItemInfo{ + name + type + unit } class DataArray{ + data + time + item + geometry + plot() } DataArray --* Grid DataArray --* ItemInfo
Benefits of object oriented design:
Term | Meaning | Example |
---|---|---|
Class | Blueprint for objects | class Location: ... |
Instance | Concrete object created from a class | loc = Location("Antwerp") |
Instance variable | Data stored per object | self.name = name |
Attribute | Anything accessed with dot notation | loc.name , loc.plot() |
Method | Function defined inside a class | def plot(self): ... |
Property | Managed attribute using @property |
@property def name(self): ... |
Encapsulation | Hide internal details; provide safe interface | Use _var + property |
class Location:
def __init__(self, name, longitude, latitude):
self._name = name.upper() # Names are always uppercase
...
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value.upper()
>>> loc = Location("Antwerp", 4.42, 51.22)
>>> loc.name = "Antwerpen"
>>> loc.name
"ANTWERPEN" 😊
Composition in object oriented design is a way to combine objects or data types into more complex objects.
classDiagram class Grid{ + nx + dx + ny + dy + find_index() } class ItemInfo{ + name + type + unit } class DataArray{ + data + time + item + geometry + plot() } DataArray --* Grid DataArray --* ItemInfo
DataArray
has a geometry
(e.g. Grid
) and an item
(ItemInfo
).
classDiagram class Shape { +color +describe() +area()* } class Circle { +radius +area() } class Rectangle { +width +height +area() } Shape <|-- Circle Shape <|-- Rectangle
import math
class Shape:
def __init__(self, color="black"):
self.color = color
def describe(self):
return f"A {self.color} shape with area = {self.area():.2f}"
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, radius, color="black"):
super().__init__(color)
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height, color="black"):
super().__init__(color)
self.width = width
self.height = height
def area(self):
return self.width * self.height
Composition: a car has a wheel.
Inheritance: a sports car is a car.
sum()
allows us to operate on a higher level of abstraction.classDiagram Container <|-- Collection Sized <|-- Collection Iterable <|-- Collection class Container{ __contains__(self, x) } class Sized{ __len__(self) } class Iterable{ __iter__(self) }
__len__
it is a Sized
object.__contains__
it is a Container
object.__iter__
it is a Iterable
object.classDiagram Container <|-- Collection Sized <|-- Collection Iterable <|-- Collection Collection <|-- Sequence Collection <|-- Set Sequence <|-- MutableSequence Mapping <|-- MutableMapping Collection <|-- Mapping MutableSequence <|-- List Sequence <|-- Tuple MutableMapping <|-- Dict
If you want your code to be Pythonic, you have to be familiar with these types and their methods.
Dundermethods:
__getitem__
__setitem__
__len__
__contains__
class JavaLikeToolbox:
def __init__(self, tools: Collection[Tool]) -> None:
self.tools = tools
def getToolByName(self, name: str) -> Tool:
for tool in self.tools:
if tool.name == name:
return tool
def numberOfTools(self) -> int:
return len(self.tools)
tb = JavaLikeToolbox([Hammer(), Screwdriver()])
tb.getToolByName("hammer")
<__main__.Hammer at 0x7f64246eda90>
<__main__.Hammer at 0x7f64247047d0>
class SparseMatrix:
def __init__(self, shape, fill_value=0.0, data=None):
self.shape = shape
self._data = data if data is not None else {}
self.fill_value = fill_value
def __setitem__(self, key, value):
i,j = key
self._data[i,j] = float(value)
def __getitem__(self, key) -> float:
i,j = key
return self._data.get((i,j), self.fill_value)
def transpose(self) -> "SparseMatrix":
data = {(j,i) : v for (i,j),v in self._data.items()}
return SparseMatrix(data=data,
shape=self.shape,
fill_value=self.fill_value)
def __repr__(self):
matrix_str = ""
for j in range(self.shape[1]):
for i in range(self.shape[0]):
value = self[i, j]
matrix_str += f"{value:<4}"
matrix_str += "\n"
return matrix_str
IToolbox
and implement it for Toolbox
.import math
class Circle: # no base class
def __init__(self, radius, color="black"):
self.radius = radius
self.color = color
def area(self):
return math.pi * self.radius**2
def describe(self):
return f"A {self.color} circle with area = {self.area():.2f}"
class Rectangle: # no base class
def __init__(self, width, height, color="black"):
self.width = width
self.height = height
self.color = color
def area(self):
return self.width * self.height
def describe(self):
return f"A {self.color} rectangle with area = {self.area():.2f}"
An example is a Scikit learn transformers
fit
transform
fit_transform
If you want to make a transformer compatible with sklearn, you have to implement these methods.
We can inherit some behavior from sklearn.base.TransformerMixin
from sklearn.base import TransformerMixin
class RemoveOutliersTransformer(TransformerMixin):
def __init__(self, lower_bound, upper_bound):
self.lower_bound = lower_bound
self.upper_bound = upper_bound
self.lower_ = None
self.upper_ = None
def fit(self, X, y=None):
self.lower_ = np.quantile(X, self.lower_bound)
self.upper_ = np.quantile(X, self.upper_bound)
def transform(self, X):
return np.clip(X, self.lower_, self.upper_)
# def fit_transform(self, X, y=None):
# we get this for free, from TransformerMixin
The Interval
class represent an interval in time.
What if we want to make another type of interval, e.g. a interval of numbers \([1.0, 2.0]\)?
As long as the start
, end
and x
are comparable, the Interval
class is a generic class able to handle integers, floats, dates, datetimes, strings …
a.k.a. the Robustness principle of software design
The consumers of your package (future self), will be grateful if you are not overly restricitive in what types you accept as input.
Sensor(name='Sensor 1', voltage=3.3, install_date=datetime.date(2020, 1, 1), location=(4.42, 51.22))
Before
Before
Opposite of extract mehtod.
Break up a long method into smaller methods.
If you want to learn more about refactoring, I recommend the book “Refactoring: Improving the Design of Existing Code” by Martin Fowler.
Python package development