安置者为领域的领域

问题描述 投票:2回答:3

鉴于代码:

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)

但如果我想增加xblock 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。

python getter-setter
3个回答
1
投票

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

Variation: specify the notify function via 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

3
投票

问题在于你混合了一些试图使用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的子类,它可以给你奇怪的错误。只是不要。接受的约定是单个“_”表示不应该由类的用户直接更改或访问的属性,这没有副作用。
  • 没有setter,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实现,以了解一个很好的实现。 (声明:链接项目是我现在正在做的一个爱好,我实际上已经在过去一周实现了这个课程)


1
投票

你可以通过使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)

通常,从属属性适用于属性,因为它们可以从其他独立的属性“即时”计算。

© www.soinside.com 2019 - 2024. All rights reserved.