Search code examples
pythondecoratorpython-decorators

Extending @property.setter decorator in Python


I'm writing multiple classes with a common logic added to a bunch of attributes. This is a simplified version of the code:

class FooAspect:
  _bar_prop = 'bar'

  def __init__(self, bar_value: int):
    self._bar = bar_value

  @property
  def bar(self) -> int:
    return self._bar

  @bar.setter
  def bar(self, value: int) -> None:
    self._bar = value

    perfrom_action(self._bar_prop, value)

perform_action always has similar form, and I would like to encapsulate it with a decorator. Essentially I'm looking for a way to write something like this:

# ... define @my_setter

class FooAspect:
  # ...

  @property
  def bar(self) -> int:
    return self._bar

  @bar.my_setter(key='bar')
  def bar(self, value: int) -> None:
    self._bar = value

Is it possible to extend @property or @prop.setter to achieve this?


Solution

  • While copying and pasting the reference code of property and making minor modifications would work as demonstrated by your answer, in the interest of code reuse you can subclass the property class and call super() to access methods of the parent class instead.

    Also, the setter function in your implementation is unnecessarily instantiating a new instance of MyProperty, when it can reuse the current object by returning self._setter:

    class MyProperty(property):
        def __set__(self, obj, value):
            super().__set__(obj, value)
            perform_action(self.key, value)
    
        def _setter(self, fset):
            obj = super().setter(fset)
            obj.key = self.key
            return obj
    
        def setter(self, key):
            self.key = key
            return self._setter
    

    so that:

    class FooAspect:
        @MyProperty
        def bar(self) -> int:
            return self._bar
    
        @bar.setter(key='bar')
        def bar(self, value: int) -> None:
            self._bar = value
    
    def perform_action(key, value):
        print(key, value)
    
    f = FooAspect()
    f.bar = 'foo'
    

    outputs:

    bar foo