Python class 构建、获取和设置属性以及继承

Python class structuring, getting and setting properties and inheritance

这是我经常遇到的问题,我终于想找到最Pythonic的解决方案。归根结底,情况可以描述如下:

  1. 一个class包含一些必须根据__init__()参数计算或测试的属性。
  2. 属性在 class 实例化后可能允许也可能不允许更改。
  3. 每次允许更改的属性必须recalculated/tested。
  4. class 应该是“继承友好的”。

示例项目中的目标:一个几何模块,可以帮助进行二维形状计算(例如多边形之间的最小距离或线之间的交点)。 Lines 和 Polygons 有很多共同的属性,所以创建一个 BaseGeometry class 来继承 LinePolygon classes。 BaseGeometry 属性:

下面,我写了三个(简化的)解决方案来解决这个问题,各有优缺点。

方法 1:错误地允许更改 domain 并且不清楚哪些 class 属性存在。

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。清楚哪些属性存在,但感觉clunky/illogical。任意 calculate_domain() 是否有 points 作为参数或只使用 self.pointscalculate_domain() 每次 points 更新时都必须调用。

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 感觉很奇怪。此外,仅对所有 class 属性使用 @属性 装饰器是不好的做法吗?

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. 这些方法中的一种被认为是传统方法吗? Why/why 不是吗?
  2. 在处理这个问题时要记住哪些额外的约定?
  3. 还有哪些其他技巧或资源可以帮助我改进 class 结构?
  4. 从 GeometryBase3 继承的 class 是否必须完全重新定义 points.setter 方法以引入一些从 points 计算的新属性?

另外,我是第一次在这里提问,所以也欢迎对 post 提出任何反馈。预先感谢您抽出宝贵的时间和任何答复!

亲切的问候,

约斯特

这里没有一个正确答案,但我喜欢方法 2 和方法 3 的部分内容。

这样的事情怎么样?这使用 descriptor 协议,其中 property class 只是一种特定形式。它比 class.

中的每个属性都使用 @property 装饰器更符合 DRY(“不要重复自己”)原则
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)))

此方法的好处在于它的可扩展性——如果您需要向子class添加更多不可设置的属性,这样做很容易,您可以扩展update_shape()通过在 subclass.

中调用 super()

这是怎么回事?

如果你实例化一个BaseGeometry4的实例,你会发现它有一个points属性和一个domain属性,但都不能直接设置。

这里发生的事情是,当您“访问”points 属性时,这将调用 UnsettableGeometricDescriptor class 中的 __get__ 方法。这个 __get__ 方法实际上只是将您重定向到 BaseGeometry4 实例的 _points 属性(对于 points 属性,对于 _domain domain 属性。描述符的 __set__ 方法只是抛出一个异常,因为您永远不希望用户能够直接更新这些属性。这些描述符-classes 遵循(_points_domain)的值在BaseGeometry4

update_shape() 方法中设置

有一个关于描述符协议的很棒的 RealPython 教程 here, and the official python docs on descriptors can be found here