I'd like to write classes which are Pythonic, readable and easy-usable. A main issue for me is to keep data consistency. Here is an example (version 1):
from collections import namedtuple import math Point = namedtuple('Point', ['x', 'y']) class PointsAlongCircle(object): '''Set of regularly spaced points along a circle''' def __init__(self, center, radius, numpoints): ''' center -- center of the circle (Point) radius -- radius of the circle (float) numpoints -- number of points along the circle >>> pts = PointsAlongCircle(Point(0, 0), 3, 4) >>> pts.points ''' self.center = center self.radius = radius self.numpoints = numpoints center_x, center_y = self.center angles = [i * 2 * math.pi / numpoints for i in range(numpoints)] self.points = [Point(center_x + radius*math.cos(angle), center_y + radius*math.sin(angle)) for angle in angles] pts = PointsAlongCircle(center=Point(0, 0), radius=3, numpoints=4) print(pts.points)
This code displays as expected
[ Point(x=3.0, y=0.0), Point(x=1.8369701987210297e-16, y=3.0), Point(x=-3.0, y=3.6739403974420594e-16), Point(x=-5.51091059616309e-16, y=-3.0) ]
This code works. But in Python, public attributes are meant to be played around, right? So someone might legitimately try
pts.center = Point(1, 2) and expect that
pts.points will be updated to be centered around this new point, which is in reality not the case. And data consistency in the object is broken: the actual center of
pts.points is not anymore
Also in Python, there is duck-typing. Well, another developer might pass do something like this:
center = [0, 0] pts = PointsAlongCircle(center=center, radius=3, numpoints=4) # so far so good center = 1. # breaks data consistency in pts
The last line has a side effect:
pts.center is updated, and it also breaks the consistency in
Here is a second version of the class
PointsAlongCircle which corrects these issues (version 2):
class PointsAlongCircle(object): '''Set of regularly spaced points along a circle''' def __init__(self, center, radius, numpoints): ''' center -- center of the circle (Point) radius -- radius of the circle (float) numpoints -- number of points along the circle >>> pts = PointsAlongCircle(Point(0, 0), 3, 4) >>> pts.points ''' # remark: convert the input in immutable types self._center = Point(x=float(center), y=float(center)) self._radius = radius if isinstance(numpoints, int): self._numpoints = int(numpoints) else: raise ValueError('numpoints must be an integer') center_x, center_y = self.center angles = [i * 2 * math.pi / numpoints for i in range(numpoints)] self._points = [Point(center_x + radius*math.cos(angle), center_y + radius*math.sin(angle)) for angle in angles] # Read-only attributes @property def center(self): return self._center @property def numpoints(self): return self._numpoints @property def radius(self): return self._radius @property def points(self): return tuple(self._points) # return immutable to prevent any mess
I used properties to simulate read-only attributes, thus
pts.center = something will raise an exception. And by converting the argument
center in an immutable Point, I prevent any other modification of
pts.center. Someone can still directly modify
pts._center, but it is not my business anymore (consenting adults do not play with private attributes).
pts.points: I return an immutable object to prevent things like this:
points = pts.points points = something
Here are now my questions:
- Are my assumptions correct?
- Version 2 of
PointsAlongCircleseems more robust than version 1, but I am not convinced that it is better code. What do you think?
- Is it acceptable to assume that every developer will read the documentation of the classes I write, and never try to change public attribute unless it is explicitly written that it is permitted? Or is it my job to lock attribute which are supposed to be read-only, even if I feel a bit like a control freak?
- And what if I cannot always return read-only versions of the attributes, or if it costs too much resources?
A few last remarks:
- I do not pretend that using a class in this case is a very good choice, a function which returns a named tuple does the job. This code is meant to illustrate several questions I have.
- I also know that I can improve the code with magic methods (to have
pts.numpoints). But this is not the main concern of this topic.
- I could also use properties to compute the attributes
return len(self.points)), instead of saving them. But let's assume we cannot due this because computing the attributes takes too much resources.