Сеттер для поля поля - PullRequest
       13

Сеттер для поля поля

2 голосов
/ 04 апреля 2019

Учитывая код:

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 ... хорошо, код не входит в сеттер.

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 перенаправит меня к моему установщику / получателю.

Ответы [ 3 ]

3 голосов
/ 04 апреля 2019

Проблема в том, что вы смешиваете некоторые концепции, пытаясь использовать Python property, и все становится запутанным -

Суть в том, что если нужно вычислить pixel_position - этосам по себе должен иметь свойство.

То, что вы пытаетесь сделать, - это установить некоторые параметры для установки значений в "block_position" и получить - когда block_position изменилось, новое значение для pixel_position.Это не работает, потому что вы не «схватили» все способы, которыми можно было бы изменить значения внутри вашего block_position.

Что происходит, когда вы делаете:

 block.block_position.x += 1

Является ли свойство getter для block_position активированным - у объекта Point в этом случае его атрибут x будет изменен - ​​но это изменениеникогда не проходит через внешний блок-объект, так как 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

Итак, теперь все наоборот - и pixel_position не может быть установлен и гарантированно будет всегда обновляться.

Три вещи там:

  • Я добавил__mul__ метод для вашей точки, так что теперь можно просто использовать оператор *, чтобы умножить его на скаляр.
  • Нет необходимости или смысла в добавлении __ к «скрытым» атрибутам Python,Ранние учебники или неофициальная документация по языку могут ввести в заблуждение, говоря, что это способ иметь «личные атрибуты».Это ошибка - в Python нет закрытых атрибутов, которые __ делает для искажения имен, чтобы позволить классу иметь атрибуты, которые не перепутаны подклассами.За почти 20 лет программирования на Python мне никогда не требовалась эта функция.С другой стороны, он может выдавать странные ошибки, если у вас есть подклассы Block.Просто не надо.Общепринятым соглашением является то, что один «_» указывает на атрибут, который не должен изменяться или использоваться пользователями класса напрямую, и это не имеет побочных эффектов.
  • Без установщика pixel_position в основном «неизменяемый»"- и если кто-то изменит атрибуты внутри него после получения block.pixel_position, он будет изменять отсоединенный экземпляр Point.

Если вам действительно необходимо переключение между пикселями_позицией и блок-позицией в обоих направлениях (то есть сделать так, чтобы пользователь класса мог изменить один из атрибутов и отразить изменение в другом), а не пытатьсячтобы использовать уведомление об изменении внутри Point, я предлагаю вместо этого сделать Point неизменным классом.Любой, кто хочет изменить координаты, должен будет создать новый экземпляр точки - в результате block.block_position.x += 1 больше не будет работать - нужно будет сделать: block.block_position += Point(1, 0) (и тогда вы реализуете __add__ в Point, как я делал __mul__).Тогда вы можете написать свой установщик для pixel_position, чтобы принудительно установить новое значение в block_position, если оно будет изменено.

С другой стороны, если вы сделаете Point неизменяемым, вы можете добавить к нему __hash__ и заставить его работать в наборах и в качестве ключей словаря: открывается множество других применений.

Проверьте реализацию class V2 в этого проекта , чтобы иметь представление о хорошей реализации.(Отказ от ответственности: над проектом ссылок я сейчас работаю как хобби, и на самом деле я реализовал этот класс за последнюю неделю)

1 голос
/ 04 апреля 2019

Поскольку вы просили об этом, я предоставляю пример реализации, где свойства объекта уведомляют своего владельца при установке, чтобы он синхронизировался с другим объектом. Я рассматриваю только 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
1 голос
/ 04 апреля 2019

Вы можете решить эту проблему, задав 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)

В общем случае зависимые атрибуты подходят для свойств, так как они могут быть вычислены «на лету» из других независимых атрибутов.

...