5

Python 3 here, just in case it's important.

I'm trying to properly understand how to implement inheritance when @property is used, and I've already searched StackOverflow and read like 20 similar questions, to no avail because the problems they are trying to solve are subtly different. This is the code I'm using for testing:

class Example:
    def __init__(self):
        self.__data = None

    @property
    def data(self):
        return self.__data

    @data.setter
    def data(self, data):
        self.__data = data


class Example2(Example):
    def __init__(self):
        super().__init__()

    @property
    def data(self):
        return super().data  # Works!

    @data.setter
    def data(self, data):
        data = '2' + data
        #Example.data = data   # Works, but I want to avoid using the parent name explicitly
        #super().data = data  # Raises AttributeError: 'super' object has no attribute 'data'
        #super().data.fset(self, data) # Raises AttributeError: 'NoneType' object has no attribute 'fset'
        #super(self.__class__, self.__class__).data = data  # Raises AttributeError: 'super' object has no attribute 'data'
        super(self.__class__, self.__class__).data.fset(self, data)  # Works!


a = Example2()
a.data = 'element a'
print(a.data)

What I can't understand is why super().data works in Example2 getter, but not in setter. I mean, why in the setter a class bound method is needed, but in the getter an instance bound method works?

Could anyone please point me to an explanation or explain why I'm getting AttributeError in three of the five different calls I'm testing?

Yes, I know, I could use Example.data in the setter, but that's not needed in the getter and a) I would prefer not to use the parent class name explicitly if possible and b) I don't understand the asymmetry between getter and setter.

user
  • 5,370
  • 8
  • 47
  • 75
  • I think this has been a problem for a while... http://stackoverflow.com/a/13599342/1345165. Aparently the only way is to explicitly use the parent class name – dnaranjo Feb 09 '16 at 11:50
  • You really shouldn't use `self.__class__` in `super()` calls, see [When calling super() in a derived class, can I pass in self.\_\_class\_\_?](https://stackoverflow.com/q/18208683) – Martijn Pieters Feb 09 '16 at 11:59
  • And your last call works, because passing in a *class object* to `super()` produces a proxy for the class, and accessing the property name on a class returns the original property object. So `super(Example2, Example2).data` returns the original property, so you can access `.fset`. Or you could bind the property with `super(Example2, Example2).__set__(self, new_value)`. – Martijn Pieters Feb 09 '16 at 12:01
  • 1
    If you wanted to override just the getter or just the setter but inherit the other property hooks, see [Python overriding getter without setter](https://stackoverflow.com/a/15786149) – Martijn Pieters Feb 09 '16 at 12:01
  • @dnaranjo, that question was one of the ones I read, and I thought that was solved. Looks like I was wrong. Thanks :) – Raúl Núñez de Arenas Coronado Feb 09 '16 at 17:51
  • @MartijnPieters, thanks a lot for all the information. My problem is not wanting to update only the setter or getter, I know I can do it in more than one way, using `@CLASS_NAME.var.setter` as decorator, using `property()`, etc. My doubt was why one syntax worked on the setter and why another syntax didn't. Thanks to your explanation, now I know why (more or less) and turns out I have to study properties more. – Raúl Núñez de Arenas Coronado Feb 09 '16 at 17:55
  • @MartijnPieters, BTW, the duplicate you specify is the ONLY thread I didn't find when I was investigating the issue!!! Thanks A LOT for pointing to that question! – Raúl Núñez de Arenas Coronado Feb 09 '16 at 18:43

1 Answers1

1

you should do something like this:

class Example:
    def __init__(self):
        self._data = None

    @property
    def data(self):
        return self._data

    @data.setter
    def data(self, data):
        self._data = data


class Example2(Example):
    def __init__(self):
        super().__init__()

    @Example.data.setter
    def data(self, data):
        data = '2' + data
        self._data = data


    a = Example2()
    a.data = 'element a'
    print(a.data)

you are getting the Attribute error because the class does not have the data Attribute, the instance has it.

If you want to override the @property, just do it:

class Example:
def __init__(self):
    self._data = None

    @property
    def data(self):
        return self._data

    @data.setter
    def data(self, data):
        self._data = data


class Example2(Example):
    def __init__(self):
    super().__init__()

    @property
    def data(self):
        return self._data

    @data.setter
    def data(self, data):
        data = '2' + data
        self._data = data
Serbitar
  • 2,134
  • 19
  • 25
  • But then you are skipping the @property, which was the point of the issue – dnaranjo Feb 09 '16 at 11:49
  • I edited some errors in the implementation. If you are redefining the @property you are not inheriting anything, you override everything. Whats the point there? The example works with overriding the property as well. – Serbitar Feb 09 '16 at 12:06
  • Thanks, Serbitar, I knew of this solution but I was trying to completely isolate child from parent, I mean, extend the interface and inherit only the interface. I didn't explain that in my OP because I thought it was obvious from the code, now I see it isn't. That's why I used super() instead of hardcoding the parent class name. What if I don't know the internals of the setter in the parent? I don't know how to solve that, and anyway my doubt was about why one "flavour" of super() worked and the other not. – Raúl Núñez de Arenas Coronado Feb 09 '16 at 17:46