Setter 一个字段的字段

Setter for a field of a field

给定代码:

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)

但是如果我想增加 block positionx... 那么,代码不会进入 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.

您可以通过将 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)

一般来说,依赖属性适用于属性,因为它们可以从其他独立属性计算"on-the-fly"。

问题是您在尝试使用 Python property 时混合了一些概念,事情变得混乱了 -

基本的事情是,如果要计算 pixel_position - 它本身应该是 属性。

您正在尝试做的是对 "block_position" 的值设置设置一些门控,并导出 - 当 block_position 更改时 pixel_position 的新值。这是行不通的,因为您并不总是 "gatted" 可能会修改 block_position 中的值。

当你做的时候会发生什么:

 block.block_position.x += 1

是否激活了 block_position 的 属性 getter - 那里的 Point 对象然后更改了其 x 属性 - 但此更改永远不会通过外部块对象,因为 x 是普通属性,而不是 属性.

现在,可以检测您的 Point class,以便在 x 或 [=19] 时触发操作=] 被改变了——但这可能会变得非常复杂,而且速度很快。

一个更好的方法是让 pixel_position 本身是一个 属性,而不是一个普通的属性,并且延迟计算它的值 - 在需要时生成 - 而不是依赖于每当 block_position 更改时立即设置值。这是来自 "reactive programing" 的模式。 实际上,你会发现 "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

所以,现在情况正好相反 - pixel_position 无法设置并且保证始终更新。

里面有三样东西:

  • 我已将 __mul__ 方法添加到您的 Point 中,因此现在可以使用 * 运算符将其乘以标量。
  • 实际上在 __ 到 Python "hidden" 属性之前没有必要也没有意义。该语言的早期教程或非官方文档可能会误导性地说这是一种拥有 "private attributes" 的方式。这是一个错误 - Python 中没有私有属性 __ 所做的是名称重整,以允许 class 具有不被 sub[=95= 搞乱的属性]是的。在将近 20 年的 Python 编程中,我实际上从未需要该功能。另一方面,如果您有 Block 的子 class,它 可以 给您带来奇怪的错误。只是不要。公认的惯例是一个单独的“_”表示 class 的用户不应直接更改或访问的属性,并且这没有副作用。
  • 没有 setter,pixel_position 主要是 "unchangeable" - 如果在检索 block.pixel_position 后确实更改了其中的属性,他将更改一个分离点实例。

如果您确实需要在 pixel_position 和 block_position 之间进行往返更改(也就是说,以这种方式让 class 用户可以更改任一属性并反映更改另一方面),而不是尝试在 Point 中设置更改通知,我建议您将 Point 设为不可变的 class 。任何想要更改坐标的人都必须创建一个新的点实例 - 结果 block.block_position.x += 1 将不再起作用 - 必须这样做:block.block_position += Point(1, 0)(然后你在 Point 中实现 __add__ ,就像我做的那样 __mul__)。然后,您可以为 pixel_position 编写 setter,以便在更改时将新值强制为 block_position

从好的方面来说,如果你让 Point 不可变,你可以向它添加 __hash__ 并让它在集合中工作并作为字典键:许多其他用途开放。

检查 class V2this project 上的实现以了解一个好的实现(免责声明:link 项目是我现在作为业余爱好工作的东西,我有实际上在过去一周 class 实施了这个)

鉴于您的要求,我提供了一个示例实现,其中对象的属性在设置时会通知其所有者,以便它与另一个对象同步。我在此示例中只考虑一维点 (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