Python 类结构、获取和设置属性和继承



这是我经常遇到的问题,我终于想找出最Pythonic的解决方案。从本质上讲,情况可以描述如下:

  1. 类包含一些必须基于__init__()参数进行计算或测试的属性。
  2. 属性可能允许也可能不允许在类实例化后进行更改。
  3. 允许更改的属性必须在每次更改后重新计算/测试。
  4. 该类应该是"继承友好型"的。
示例项目中的

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

我的问题如下:

  1. 这些方法之一被认为是常规的吗?为什么/为什么不呢?
  2. 在处理此问题时,还需要记住哪些其他约定?
  3. 还有哪些其他技巧或来源可以帮助我改进课程结构?
  4. 从 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文档可以在这里找到。

相关内容

  • 没有找到相关文章

最新更新