EvilTosha
EvilTosha

Reputation: 167

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?

Upvotes: 1

Views: 1814

Answers (3)

blhsing
blhsing

Reputation: 107134

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

Upvotes: 1

juanpa.arrivillaga
juanpa.arrivillaga

Reputation: 96350

My point wasn't to copy the property implementation, that is very generic on purpose, which probably isn't needed if your logic is always the same for your getter and setter. So it would be better to do something like:

def perform_action(key, value):
    print(key, value)

class PerformKeyAction:
    def __init__(self, key):
        self.key = key

    def __get__(self, instance, owner):
        # maybe common getter behavior
        return getattr(instance, self.attr_name)

    def __set__(self, instance, value):
        perform_action(self.key, value)
        return setattr(instance, self.attr_name, value)

    def __set_name__(self, owner, name):
        self.attr_name = f'_{name}'

class FooAspect:
    bar = PerformAction(key='bar')

class BazAspect:
    buzz = PerformAction(key='buzz_key')

class FizzAspect:
    fang = PerformAction(key='fang_key')

And this way you avoid the boilerplate when writing your various classes instead of repeating it in various getters/setters in various classes.

Upvotes: 4

EvilTosha
EvilTosha

Reputation: 167

Thanks to @juanpa.arrivillaga I came up with this solution implementing my own descriptor:

# myproperty.py

class MyProperty:
    def __init__(self, fget=None, fset=None, key=None):
        self.fget = fget
        self.fset = fset
        self.key = key

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")

        perfrom_action(self.key, value)

        self.fset(obj, value)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.key)

    def _setter(self, fset):
        return type(self)(self.fget, fset, self.key)

    def setter(self, key):
        return type(self)(self.fget, self.fset, key=key)._setter
# foo_aspect.py

from myproperty import MyProperty

class FooAspect:
  # ...

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

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

Upvotes: 1

Related Questions