This is a problem I come across quite frequently, and I finally want to find out the most Pythonic solution for it. At its core, the situation can be described as follows:
- A class contains some attributes that must be calculated or tested based on
__init__()parameters. - Attributes may or may not allow alteration after class instantiating.
- Attributes that are allowed to be altered must be recalculated/tested each time after doing so.
- The class should be "inheritance friendly."
Goal in example project: A geometry module that helps with 2d shape calculations (such as the minimum distance between polygons or intersection points between lines). Lines and Polygons have quite some attributes in common, so create a BaseGeometry class from which the Line and Polygon classes inherit. The BaseGeometry attributes:
pointsa (n,x,y) list of points that are set on instantiation and can be mutated. Each time it is set, it must be asserted to be a numpy array.domain: tuple of (xmin, xmax, ymin, ymax) that describes the shape's bounds. Must be recalculated each timepointsis set, but may not be externally altered.
Below, I have written up three (simplified) solutions for approaching this problem, all with their pros and cons.
METHOD 1: Wrongly allows alteration of domain and unclear on which class attributes exist.
class BaseGeometry1:
def __init__(self, points):
self.set_points(points)
def set_points(self, points):
assert type(points) is np.ndarray
self.points = points
x, y = points.T
self.domain = ((min(x), max(x), min(y), max(y)))
METHOD 2: Still allows domain to be altered. Clear on which attributes exist, but feels clunky/illogical. Arbitrary whether calculate_domain() has points as a parameter or just use self.points. calculate_domain() Would have to be called each time points is updated.
class BaseGeometry2:
def __init__(self, points):
self.points = self.set_points(points)
self.domain = self.calculate_domain()
def set_points(self, points):
assert type(points) is np.ndarray
return points
def calculate_domain(self):
x, y = self.points.T
return (min(x), max(x), min(y), max(y))
METHOD 3: I feel like this is on the right track, but I'm still unsure about the structuring. It feels weird that points.setter also sets _domain. Also, is it bad practice to just use the @property decorators for all class attributes?
class BaseGeometry3:
def __init__(self, points):
self.points = points
@property
def points(self):
return self._points
@points.setter
def points(self, points):
assert type(points) is np.ndarray
self._points = points
x, y = points.T
self._domain = ((min(x), max(x), min(y), max(y)))
@property
def domain(self):
return self._domain
My questions are as follows:
- Is one of these methods considered conventional? Why/why not?
- What are some additional conventions to keep in mind while approaching this problem?
- What are some other tips or sources that can help me to improve my class structuring?
- Would a class inheriting from GeometryBase3 have to completely redefine the
points.settermethod in order to introduce some new attributes that are calculated frompoints?
Also, I'm new to asking questions here, so any feedback on the post is welcome as well. Thanks in advance for your time and any answers!
Kind regards,
Joost