鉴于代码:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
class Block:
size = 40
def __init__(self, x=1, y=1):
self.pixel_position = Point(x * Block.size, y * Block.size)
self.__block_position = Point(x, y)
@property
def block_position(self):
return self.__block_position
@block_position.setter
def block_position(self, point):
#I want for pixel_position to be updated whenever block position changes
self.pixel_position.x += point.x * size
self.pixel_position.y += point.y * size
self.__block_position = point
现在对于这样的代码,简单的赋值很好
block = Block()
block.block_position = Point(2, 1)
但如果我想增加x
的block position
......好吧,代码不会进入setter。
block.block_position.x -= 1
# now block_position = (1, 1) but pixel_position = (80, 40) not (40, 40)
我怎么改变它?
我知道我可以通过为__pixel_position
添加属性来解决这个问题,它会在返回之前计算Block.size * __block_position
,但这种方法并不能满足我 - 我想知道如何在python中为字段的字段设置属性。
我的问题不是找到任何解决方案,而是找到一个解决方案,改变字段block_position.x
会将我重定向到我的setter / getter。
由于您要求它,我提供了一个示例实现,其中对象的属性在设置时通知其所有者,以便与另一个对象同步。我在这个例子中只考虑1D点(x
),因为y
的实现是一样的:
class NotificationProperty(property):
def __init__(self, fget=None, fset=None, fdel=None, doc=None, notify=None):
super().__init__(fget, fset, fdel, doc)
self.notify = notify
def __set_name__(self, owner, name):
self.name = name
def __set__(self, instance, val):
super().__set__(instance, val)
self.notify(instance, self.name, val)
# Should define similar methods for other attributes
# (see https://docs.python.org/3/howto/descriptor.html#properties).
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__, self.notify)
def notification_property(func):
from functools import partial
return partial(NotificationProperty, notify=func)
class SyncPoint:
def __init__(self, x, sync_with=None):
self.sync_with = sync_with
self.x = x
def sync(self, which, value):
if self.sync_with is not None:
obj, scale = self.sync_with
value = int(scale * value)
if getattr(obj, which) != value: # Check if already synced -> avoid RecursionError.
setattr(obj, which, value)
@notification_property(sync)
def x(self):
return self._x
@x.setter
def x(self, val):
self._x = val
class Block:
size = 40
def __init__(self, x=1):
self.pixel_position = SyncPoint(self.size * x)
self.block_position = SyncPoint(x, sync_with=(self.pixel_position, self.size))
self.pixel_position.sync_with = (self.block_position, 1/self.size)
block = Block(3)
print('block_pos: ', block.block_position.x) # block_pos: 3
print('pixel_pos: ', block.pixel_position.x) # pixel_pos: 120
block.block_position.x -= 1
print('block_pos: ', block.block_position.x) # block_pos: 2
print('pixel_pos: ', block.pixel_position.x) # pixel_pos: 80
block.pixel_position.x -= Block.size
print('block_pos: ', block.block_position.x) # block_pos: 1
print('pixel_pos: ', block.pixel_position.x) # pixel_pos: 40
x.setter(func)
以下是上述代码的变体,您可以在x.setter
定义期间指定要通知的函数。这可能会更直观,因为通知发生在__set__
上,但最后它是一个品味问题:
from functools import partial
class notification_property(property):
def __init__(self, fget=None, fset=None, fdel=None, doc=None, notify=None):
super().__init__(fget, fset, fdel, doc)
self.notify = notify
def __set_name__(self, owner, name):
self.name = name
def __set__(self, instance, val):
super().__set__(instance, val)
self.notify(instance, self.name, val)
# Should define similar methods for other attributes
# (see https://docs.python.org/3/howto/descriptor.html#properties).
def setter(self, func=None):
return partial(type(self), self.fget, fdel=self.fdel, doc=self.__doc__, notify=(func or self.notify))
class SyncPoint:
def __init__(self, x, sync_with=None):
self.sync_with = sync_with
self.x = x
def sync(self, which, value):
if self.sync_with is not None:
obj, scale = self.sync_with
value = int(scale * value)
if getattr(obj, which) != value: # Check if already synced -> avoid RecursionError.
setattr(obj, which, value)
@notification_property
def x(self):
return self._x
@x.setter(sync)
def x(self, val):
self._x = val
问题在于你混合了一些试图使用Python property
的概念,事情变得令人困惑 -
基本的是,如果要计算pixel_position
- 它本身应该是一个属性。
你要做的是在“block_position”的值设置上设置一些门并导出 - 当block_position被更改为pixel_position的新值时。这是行不通的,因为你没有“gatted”所有可能修改block_position中的值的方法。
制作时会发生什么:
block.block_position.x += 1
是否已激活block_position的属性getter
- 其中的Point对象的x
属性已更改 - 但此更改永远不会通过outter块对象,因为x
是一个普通属性,而不是属性。
现在,有可能检测你的Point
类,以便每当x
ou y
改变时都可以触发动作 - 但这可能会变得非常复杂和快速。
更好的方法是将pixel_position本身作为属性而不是普通属性,并且懒惰地计算其值 - 在需要时生成 - 并且不依赖于block_position改变时急切设置的值。这是“反应性编程”的模式。实际上,您会发现“block_poisition”本身可以是普通的实例属性,而不是属性 - 除非您希望其检查器确保分配的对象是Point
的实例。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __mul__(self, scale):
return Point(self.x * scale, self.y * scale)
class Block:
size = 40
def __init__(self, x=1,y=1):
self._block_position = Point(x,y)
@property
def block_position(self):
return self._block_position
@block_position.setter
def block_position(self, point):
if not isinstance(point, Point):
# raise TypeError() # or, as an option, accept sequences of 2 coordinates:
point = Point(point[0], point[1])
self._block_position = point
@property
@pixel_position(self):
return self._block_position * self.size
所以,现在情况正好相反 - 而且像素位置无法设置,并且保证始终更新。
那里有三件事:
__mul__
方法添加到您的Point中,所以现在可以使用*
运算符将其乘以标量。__
预先添加到Python“隐藏”属性是没有必要或有意义的。该语言的早期教程或非官方文档可能误导性地说它是一种拥有“私有属性”的方式。这是一个错误 - 在Python中没有私有属性,__
所做的是名称修改,以便允许类具有不被子类搞砸的属性。在近20年的Python编程中,我从未真正需要这个功能。另一方面,如果你有Block
的子类,它可以给你奇怪的错误。只是不要。接受的约定是单个“_”表示不应该由类的用户直接更改或访问的属性,这没有副作用。pixel_position
大多是“不可更改的” - 如果在重新启动block.pixel_position
之后确实改变了其中的属性,他将更改一个脱离的Point实例。如果你真的需要在pixel_position和block_position之间进行往返更改(也就是说,使用类似用户可以更改任一属性并将更改反映在另一个属性中),而不是尝试在Point
内部检测更改通知,建议你让Point
成为一个可以改变的类。任何想要改变坐标的人都必须创建一个新的点实例 - 因此block.block_position.x += 1
将不再起作用 - 一个人必须这样做:block.block_position += Point(1, 0)
(然后你在Point中实现__add__
,就像我做__mul__
一样)。然后你可以为pixel_position
编写你的setter,以便在block_position
被改变时强制使用新值。
从好的方面来说,如果你使Point不可变,你可以添加__hash__
并让它在集合和字典键中工作:许多其他用途打开。
检查class V2
上的this project实现,以了解一个很好的实现。 (声明:链接项目是我现在正在做的一个爱好,我实际上已经在过去一周实现了这个课程)
你可以通过使pixel_position
成为一个属性来解决这个问题,因为这个属性似乎依赖于另一个,block_position
。这样block_position
甚至不必是一个财产:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'{type(self).__name__}({self.x}, {self.y})'
class Block:
size = 40
def __init__(self, x=1, y=1):
self.block_position = Point(x,y)
@property
def pixel_position(self):
return Point(self.block_position.x * self.size, self.block_position.y * self.size)
block = Block()
block.block_position = Point(2,1)
block.block_position.x -= 1
print(block.block_position) # Point(1, 1)
print(block.pixel_position) # Point(40, 40)
通常,从属属性适用于属性,因为它们可以从其他独立的属性“即时”计算。