Adding Methods Retroactively
Mon 16 September 2019 by Moshe ZadkaThe following post was originally published on OpenSource.com as part of a series on seven libraries that help solve common problems.
Imagine you have a "shapes" library.
We have a Circle
class,
a Square
class,
etc.
A Circle
has a radius
,
a Square
has a side
,
and maybe Rectangle
has height
and width
.
The library already exists:
we do not want to change it.
However,
we do want to add an area
calculation.
If this was our library,
we would just add an area
method,
so that we can call shape.area()
,
and not worry about what the shape is.
While it is possible to reach into a class and add a method, this is a bad idea: nobody expects their class to grow new methods, and things might break in weird ways.
Instead,
the singledispatch
function in functools
can come to our rescue:
@singledispatch def get_area(shape): raise NotImplementedError("cannot calculate area for unknown shape", shape)
The "base" implementation for the get_area
function
just fails.
This makes sure that if we get a new shape,
we will cleanly fail instead of returning a nonsense result.
@get_area.register(Square) def _get_area_square(shape): return shape.side ** 2 @get_area.register(Circle) def _get_area_circle(shape): return math.pi * (shape.radius ** 2)
One nice thing about doing things this way is that if
someone else writes a new shape that is intended to play well with our
code,
they can implement the get_area
themselves:
from area_calculator import get_area @attr.s(auto_attribs=True, frozen=True) class Ellipse: horizontal_axis: float vertical_axis: float @get_area.register(Ellipse) def _get_area_ellipse(shape): return math.pi * shape.horizontal_axis * shape.vertical_axis
Calling
get_area
is straightforward:
print(get_area(shape))
This means we can change a function that has a long
if isintance()/elif isinstance()
chain
to work this way,
without changing the interface.
The next time you are tempted to check if isinstance
,
try using singledispatch
!