这是我经常遇到的问题,我终于想找出最Pythonic的解决方案。从本质上讲,情况可以描述如下:
- 类包含一些必须基于
__init__()
参数进行计算或测试的属性。 - 属性可能允许也可能不允许在类实例化后进行更改。
- 允许更改的属性必须在每次更改后重新计算/测试。
- 该类应该是"继承友好型"的。
Goal:帮助进行二维形状计算的几何模块(例如多边形之间的最小距离或线之间的交点)。线和多边形具有相当多的共同属性,因此请创建一个BaseGeometry
类,Line
类和Polygon
类继承自该类。BaseGeometry
属性:
points
实例化时设置并且可以更改的点的 (n,x,y) 列表。每次设置它时,都必须断言它是一个 numpy 数组。domain
:描述形状边界的元组(xmin,xmax,ymin,ymax)。每次设置points
时都必须重新计算,但不能从外部更改。
下面,我写了三个(简化的)解决方案来解决这个问题,它们都有优点和缺点。
方法 1:错误地允许更改domain
并不清楚存在哪些类属性。
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)))
方法 2:仍然允许更改domain
。清楚存在哪些属性,但感觉笨拙/不合逻辑。任意calculate_domain()
是否将points
作为参数或仅使用self.points
. 每次更新points
时都必须调用calculate_domain()
。
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))
方法3:我觉得这是在正确的轨道上,但我仍然不确定结构。感觉很奇怪,points.setter
也设置了_domain
.另外,只对所有类属性使用@property装饰器是一种不好的做法吗?
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
我的问题如下:
- 这些方法之一被认为是常规的吗?为什么/为什么不呢?
- 在处理此问题时,还需要记住哪些其他约定?
- 还有哪些其他技巧或来源可以帮助我改进课程结构?
- 从 GeometryBase3 继承的类是否必须完全重新定义
points.setter
方法才能引入一些从points
计算的新属性?
另外,我是在这里提问的新手,所以也欢迎对帖子的任何反馈。提前感谢您的时间和任何答案!
亲切问候
佑斯特
这里没有一个正确的答案,但我喜欢方法 2 和方法 3 的部分内容。
像这样的事情怎么样?这使用descriptor
协议,其中property
类只是其中的特定形式。它更符合 DRY("不要重复自己")原则,而不是为类中的每个属性使用@property
装饰器。
class UnsettableGeometricDescriptor:
def __set_name__(self, owner, name):
self.name = name
self.lookup_name = f'_{name}'
def __get__(self, obj, type=None):
return getattr(obj, self.lookup_name)
def __set__(self, obj, value):
raise AttributeError(f'Attribute {self.name} cannot be set directly, use method "update_shape" instead')
class BaseGeometry4:
def __init__(self, points):
self.update_shape(points)
points = UnsettableGeometricDescriptor()
domain = UnsettableGeometricDescriptor()
def update_shape(self, points):
assert isinstance(points, np.ndarray), "points parameter has to be a numpy array!!!"
self._points = points
x, y = points.T
self._domain = ((min(x), max(x), min(y), max(y)))
这种方法的好处是它的可扩展性——如果你需要向子类添加更多不可设置的属性,这很容易做到,你可以通过在子类中调用 super() 来扩展 update_shape()。
这是怎么回事?
如果实例化BaseGeometry4
的实例,您会发现它具有points
属性和domain
属性,但两者都不能直接设置。
这里发生的事情是,当您"访问"points
属性时,这会调用UnsettableGeometricDescriptor
类中的__get__
方法。对于points
属性,此__get__
方法实际上只是将您重定向到BaseGeometry4
实例的_points
属性,在domain
属性的情况下重定向到_domain
。描述符的__set__
方法只是引发异常,因为您永远不希望用户能够直接更新这些属性。这些描述符类遵循的值(_points
和_domain
)是在update_shape()
方法中设置的BaseGeometry4
这里有一个关于描述符协议的很棒的RealPython教程,关于描述符的官方python文档可以在这里找到。