How can I write reusable @property getters/setters?

Viewed 566

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 :)

3 Answers

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)

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').

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 :)

Related