3

I am looking for a way to not repeat definitions for getters/setters that are structurally the same using the @property decorator. An Example:

class foo:
    def __init__(self,length):
        self.length=length
    # length
    @property
    def length(self):
        return self._length

    @length.setter
    def length(self, val):
        # do some checks here (e.g. for units, etc.) (same as in the class below)
        self._length = val


class bar:
    def __init__(self,width):
        self.width=width
    # width
    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, val):
        # do some checks here (e.g. for units, etc.)(same as in the class above)
        self._width = val

The attributes width in class bar and length in class foo have structurally the same setters so I want to define a generic setter I can apply for these attributes. However, I am not sure how to achieve the desired behaviour? Maybe with second degree decorators?

Help is greatly appreciated :)

Adam D. Ruppe
  • 25,382
  • 4
  • 41
  • 60
Tobias Triesch
  • 152
  • 1
  • 9
  • 2
    It is tedious unfortunately to do it this way, but it is recommended for a few reasons: it's explicit, so people reading your code know exactly what's going on; and it works with inheritance which can be a surprisingly tricky thing to get right. – robbrit Mar 20 '20 at 15:44
  • If you are trying to avoid code duplication best way is to write a class method in say foo and then call it in bar and foo. I meant just for the checking units part in the setter. or make it a global check function. – Albin Paul Mar 20 '20 at 15:46
  • Yes I started out doing it with the duplicates, but when you want to change something at some point in the future you are in trouble. Therefore, I was wondering whether there is a standard solution. After all, I am still learning Python and looking for good ways to solve these problems when they appear =) – Tobias Triesch Mar 20 '20 at 16:57

3 Answers3

3

Maybe You could use python descriptor protocol to achieve what You want

class Unit(object):
    def __set_name__(self, owner, name):
        self.name = name

    def __set__(self, instance, value):
        # put validation here
        instance.__dict__[self.name] = value

    def __get__(self, instance, owner):
        if instance is None:
            # allow access to the descriptor as class attribute
            return self
        return instance.__dict__.get(self.name)


class Container:
    length = Unit()
    width = Unit()

    def __init__(self):
        self.length = 100
        self.width = 200


def main():
    t = Container()
    print(t.length)
    print(t.width)
    t.length = 3
    t.width = 15
    print(t.length)
    print(t.width)
wim
  • 338,267
  • 99
  • 616
  • 750
qwetty
  • 1,238
  • 2
  • 10
  • 24
  • Thank you for the quick answer! I think that will do the trick. I will have to have a look at it in my work's context :) – Tobias Triesch Mar 20 '20 at 16:53
  • 1
    –1 this is wrong usage of descriptor protocol (leak of state). If you put `t1 = Foo(10)` and then `t2 = Foo(20)` then you will see that `t1.length` is now incorrect value. – wim Mar 22 '20 at 20:40
  • You are right @wim and fixed code example. In general my idea was to point that there is such solution, that could help :) – qwetty Mar 23 '20 at 08:13
  • Yes - looks better now. I agree that using descriptors is a good suggestion. – wim Mar 23 '20 at 20:19
2

Ultimately @property just returns an object implementing the descriptor protocol, so you could easily implement such a thing like this:

class ReusableProperty:
   def __set_name__(self, owner, name):
       self.name = name

    def __get__(self, obj, objtype=None):
       return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
       # do tests
       obj.__dict__[self.name] = value

class Foo:
    bar = ReusableProperty()

f = Foo()
print(f.bar)
f.bar = 'baz'
print(f.bar)

This is a minimal example, you'll want to familiarise yourself with the descriptor protocol and perhaps decorators to make this more complete and perhaps dress this up in more reusable ways, but as long as you set the property to an object implementing __get__ and __set__, you can customise this to your liking. See https://stackoverflow.com/a/17330273/476 for a good overview.

Note that __set_name__ is only available since Python 3.6; in older versions you'd need to explicitly pass the name through __init__, like bar = ReusableProperty('bar').

deceze
  • 510,633
  • 85
  • 743
  • 889
0

so I thought I could give an update on what I ended up with. So the reason why I was looking for a solution like this was also, that I wanted to incorporate unit-tracking for my attributes, via their setters and be able to parse unit-value data.


import varCheck #modified parsing functions for my specific data
from astropy import units as u
########################################################################################
# length
########################################################################################
def make_length(internalVarName, description):
    def _get_length(self):
        return getattr(self, internalVarName)

    def _set_length(self, val):
        setattr(self, internalVarName, varCheck.valueUnitPair(val))
        # assert that the unit is actually of type length e.g. 'm' by
        # calling the astropy unit conversion function within varCheck.valueUnitPair
        # NOTE: this will not convert the unit specified in the input!
        assert getattr(
            self, internalVarName).to(u.m).unit, "val not of type 'length'!"

    def _del_length(self):
        del self.__dict__[internalVarName]

    return property(_get_length, _set_length, _del_length, str(description))



class foo:
    length = make_length('_length', 'the yardstick')

def __init__(self,length):
    self.length = length

To me this solution seems fairly neat, but maybe I am overseeing something... If you have any comments on that, feel free to comment :)

Tobias Triesch
  • 152
  • 1
  • 9