Python class 构建、获取和设置属性以及继承
Python class structuring, getting and setting properties and inheritance
这是我经常遇到的问题,我终于想找到最Pythonic的解决方案。归根结底,情况可以描述如下:
- 一个class包含一些必须根据
__init__()
参数计算或测试的属性。
- 属性在 class 实例化后可能允许也可能不允许更改。
- 每次允许更改的属性必须recalculated/tested。
- class 应该是“继承友好的”。
示例项目中的目标:一个几何模块,可以帮助进行二维形状计算(例如多边形之间的最小距离或线之间的交点)。 Lines 和 Polygons 有很多共同的属性,所以创建一个 BaseGeometry
class 来继承 Line
和 Polygon
classes。 BaseGeometry
属性:
points
一个 (n,x,y) 点列表,在实例化时设置并且可以改变。每次设置时,必须断言它是一个numpy数组。
domain
:描述形状边界的 (xmin, xmax, ymin, ymax) 元组。每次设置 points
时都必须重新计算,但不能从外部更改。
下面,我写了三个(简化的)解决方案来解决这个问题,各有优缺点。
方法 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.points
。 calculate_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
我的问题如下:
- 这些方法中的一种被认为是传统方法吗? Why/why 不是吗?
- 在处理这个问题时要记住哪些额外的约定?
- 还有哪些其他技巧或资源可以帮助我改进 class 结构?
- 从 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。
这是我经常遇到的问题,我终于想找到最Pythonic的解决方案。归根结底,情况可以描述如下:
- 一个class包含一些必须根据
__init__()
参数计算或测试的属性。 - 属性在 class 实例化后可能允许也可能不允许更改。
- 每次允许更改的属性必须recalculated/tested。
- class 应该是“继承友好的”。
示例项目中的目标:一个几何模块,可以帮助进行二维形状计算(例如多边形之间的最小距离或线之间的交点)。 Lines 和 Polygons 有很多共同的属性,所以创建一个 BaseGeometry
class 来继承 Line
和 Polygon
classes。 BaseGeometry
属性:
points
一个 (n,x,y) 点列表,在实例化时设置并且可以改变。每次设置时,必须断言它是一个numpy数组。domain
:描述形状边界的 (xmin, xmax, ymin, ymax) 元组。每次设置points
时都必须重新计算,但不能从外部更改。
下面,我写了三个(简化的)解决方案来解决这个问题,各有优缺点。
方法 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.points
。 calculate_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
我的问题如下:
- 这些方法中的一种被认为是传统方法吗? Why/why 不是吗?
- 在处理这个问题时要记住哪些额外的约定?
- 还有哪些其他技巧或资源可以帮助我改进 class 结构?
- 从 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。