Search code examples
pythongetter-setter

Setter for a field of a field


Given the code:

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

Now for such code simple assignment works well

block = Block()
block.block_position = Point(2, 1)

but if I want to increment x of block position... well, code doesn't go into setter.

block.block_position.x -= 1
# now block_position = (1, 1) but pixel_position = (80, 40) not (40, 40)

How may I change it?

I know I can resolve this problem with adding property for __pixel_position that will calculate Block.size * __block_position before returning itself, but that approach doesn't satisfy me - well I want to know how in python one can set a property for a field of a field.

My question is not about finding any solution, but to find a solution where changing field block_position.x will redirect me to my setter/getter.


Solution

  • Since you asked for it, I'm providing an example implementation where properties of an object notify their owner when being set, for it to synchronize with another object. I only consider 1D points (x) in this example since the implementation for y is the very same:

    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)

    The following is a variation of the above code which let's you specify the function to be called for notifications during definition of x.setter. This might feel more intuitive since the notification happens on __set__ but in the end it's a matter of taste:

    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