lib
lib

Reputation: 2956

Override an attribute with a property in python class

I have a class like:

class Parent(object):
    def __init__(self, a1 = None, a2 = None):
        someInitialization()
        self.a1 = a1
        self.a2 = a2

    def coolMethod(self): 
        return dosomething(self.a2)

Let's say that

EDIT my actual constraints are not really so strict, but I ask this question also for understanding better the class attributes, methods, etc..

I am using the simplest solution without initialization inheritance:

class Child(Parent):
    _propertiesCache = {'a1':None,'a2':None}
    def _initProperty(self, propertyName):
        value = self._propertiesCache[propertyName]
        if value is None:
            self._propertiesCache[propertyName]=expensiveFunction(self.b, propertyName)
        return  self._propertiesCache[propertyName]

    @property
    def a1(self):
        return self._initProperty('a1')

    @property
    def a2(self):
        return self._initProperty('a2')

    def __init__(self,b):
        self.b = b
        someInitialization()

Is there a way to properly call the initialization of the parent class? If I use super(Child,self).__init__() I get AttributeError: can't set attribute. I tried to override __new__ but I am always missing something

class Child(Parent):
    _propertiesCache = {'a1':None,'a2':None}
    @classmethod
    def _initProperty(cls, propertyName):
        value = cls._propertiesCache[propertyName]
        if value is None:
            cls._propertiesCache[propertyName] = someTimeConsumingFunctionThatIDontWantToCallAtInitialization(propertyName)
        return  cls._propertiesCache[propertyName]

    @property
    def a1(self):
        return self._initProperty('a1')

    @property
    def a2(self):
        return self._initProperty('a2')

    def __new__(cls):
        super(Child,cls).__new__(cls)
        return cls
    def __init__(self,b):
        self.b = b
        super(Child,self).__init__(a1=self.a1, a2=self.a2)

Gives me:

>>c = Child();c.a1
<property at 0x3aa4228>
>>c.a1.fget(c)
"ValueOfa1"

Upvotes: 7

Views: 9763

Answers (2)

Vladimir
Vladimir

Reputation: 1553

I believe that much more pythonic and in line with the OOP convention is not to change the Parent class - it does not have to anticipate how its children override its arguments and behavior.

In order for Child to use a1 and a2 as properties it has to implement both the getter (@property) and dummy setter (@a1.setter).

The code below assumes that a1 could not be modified by user (so the setter is implemented as a dummy and __init__ of Parent is called with arbitrary value). Property a2 has slightly more complicated behavior: it could be initialized or modified by user, but if its value is not valid (e.g. negative here) - it is overridden by internal logic.

class Child(Parent):    
    def __init__(self):
        super().__init__(None, -42)


    @property
    def a1(self):
        return 42

    @a1.setter
    def a1(self, value):
        """ Do nothing. Implemented to prevent Attribute Error """


    @property
    def a2(self):
        if getattr(self, '_a2', -1) < 0:
            self._a2 = self._initProperty('a2')
        return self._a2

    @a2.setter
    def a2(self, value):
        self._a2 = value

P.S. You could reduce the dummy setter to just one line:

class Child(Parent):    
    # ...
    a1 = a1.setter(lambda self, value: None)

Upvotes: 1

Tadhg McDonald-Jensen
Tadhg McDonald-Jensen

Reputation: 21464

instead of calling your method _initProperty call it __getattr__ so that it will be called every time the attribute is not found in the normal places it should be stored (the attribute dictionary, class dictionary etc.) then the first time the attribute is tried to be accessed it gets initialized.

Be sure to not set them in the Parent initialization, maybe only set them if they are not None:

class Parent:
     def __init__(self,a1=None,a2=None):
         if a1 is not None:
             self.a1 = a1
         if a2 is not None:
             self.a2 = a2

As well to stay consistent with errors you will want to raise an AttributeError if the attribute doesn't exist instead of letting the KeyError go through, and maybe add a reference to the value in the regular attribute dict so that it doesn't need to run the __getattr__ every time:

_propertiesCache = {'a1':None,'a2':None}
def __getattr__(self, propertyName):
    if propertyName not in self._propertiesCache:
        raise AttributeError(propertyName)
    value = self._propertiesCache[propertyName]
    if value is None:
        value = self._propertiesCache[propertyName]=expensiveFunction(self.b, propertyName)
    setattr(self,propertyName,value)
    return  value

Any way you implement this you need to make sure:

The attribute is not set until the first time they are used (at which point __getattr__ gets used)

Upvotes: 4

Related Questions