Search code examples
pythonpropertiesattributesnestedencapsulation

python nested attributes encapsulation


I have some question about encapsulation nested attributes in python. Let's assume few classes: Here we have a main class (DataWrapper) that includes two more classes: InnerWrapper1 and InnerWrapper2. Both inner wrappers includes two attributes.

class DataWrapper(object):     
    @property
    def inner_wrapper1(self): 
        return self.__inner_wrapper1
    @inner_wrapper1.setter
    def inner_wrapper1(self, value): 
        self.__inner_wrapper1 = value

    @property
    def inner_wrapper2(self): 
        return self.__inner_wrapper2
    @inner_wrapper2.setter
    def inner_wrapper2(self, value): 
        self.__inner_wrapper2 = value

class InnerWrapper1(object):
    @property
    def property1(self): 
        return self.__property1
    @property1.setter
    def property1(self, value): 
        self.__property1 = value

    @property
    def property2(self): 
        return self.__property2
    @property2.setter
    def property2(self, value): 
        self.__property2 = value

class InnerWrapper2(object):
    @property
    def property3(self): 
        return self.__property3
    @property3.setter
    def property3(self, value): 
        self.__property3 = value

    @property
    def property4(self): 
        return self.__property4
    @property4.setter
    def property4(self, value): 
        self.__property4 = value

Is it possible to override somehow getattr and setattr methods to make possible below encapsulation? What I want to achieve is to have an access to those nested attributes from the top class- DataWrapper.

data_wrapper = DataWrapper()
data_wrapper.property1 = "abc"
...
var = data_wrapper.property2
...

The first thing that came to my mind was to execute hasattr in getattr, but that gave a maximum recursion depth...

Here's a complete code:

class DataWrapper(object):

    def __init__(self):
            self.inner_wrapper1 = InnerWrapper1()
            self.inner_wrapper2 = InnerWrapper2()

    @property
    def inner_wrapper1(self):
            return self.__inner_wrapper1
    @inner_wrapper1.setter
    def inner_wrapper1(self, value):
            self.__inner_wrapper1 = value

    @property
    def inner_wrapper2(self):
            return self.__inner_wrapper2
    @inner_wrapper2.setter
    def inner_wrapper2(self, value):
            self.__inner_wrapper2 = value

    def __setattr__(self, attribute, value):
            #if attribute in {'innerwrapper1', 'innerwrapper2'}:
            if attribute in ['inner_wrapper1', 'inner_wrapper2']:
                    return super(DataWrapper, self).__setattr__(attribute, value)
            if hasattr(self.inner_wrapper1, attribute):
                return setattr(self.inner_wrapper1, attribute, value)
            elif hasattr(self.inner_wrapper2, attribute):
                return setattr(self.inner_wrapper2, attribute, value)



    def __getattr__(self, attribute):
            try:
                    return getattr(self.inner_wrapper1, attribute)
            except AttributeError: pass

            try:
                    return getattr(self.inner_wrapper2, attribute)
            except AttributeError: pass

class InnerWrapper1(object):
    @property
    def property1(self):
            return self.__property1
    @property1.setter
    def property1(self, value):
            self.__property1 = value

    @property
    def property2(self):
            return self.__property2
    @property2.setter
    def property2(self, value):
            self.__property2 = value

class InnerWrapper2(object):
    @property
    def property3(self):
            return self.__property3
    @property3.setter
    def property3(self, value):
            self.__property3 = value

    @property
    def property4(self):
            return self.__property4
    @property4.setter
    def property4(self, value):
            self.__property4 = value

def main():
    data_wrapper = DataWrapper()
    data_wrapper.property1 = "abc"

if __name__ == "__main__":
    main()

Solution

  • You get an infinite recursion error because you forgot to take into account setting the inner_wrapper1 and inner_wrapper2 attributes in your __init__ method.

    When you do this:

    self.inner_wrapper1 = InnerWrapper()
    

    Python will also use your __setattr__ method. This then tries to use self.inner_wrapper1 which doesn't yet exist so __getattr__ is called, which tries to use self.inner_wrapper1 which doesn't yet exist, and you enter into an infinite recursion loop.

    In __setattr__ delegate attribute setting to the superclass:

    def __setattr__(self, attribute, value):
        if attribute in {'innerwrapper1', 'innerwrapper2'}:
            return super(DataWrapper, self).__setattr__(attribute, value)
        if hasattr(self.inner_wrapper1, attribute):
            return setattr(self.inner_wrapper1, attribute, value)
        elif hasattr(self.inner_wrapper2, attribute):
            return setattr(self.inner_wrapper2, attribute, value)
    

    If you used a single leading underscore for 'private' attributes (so _innerwrapper1 and _innerwrapper2) you could just test for that:

    def __setattr__(self, attribute, value):
        if attribute[0] == '_':  # private attribute
            return super(DataWrapper, self).__setattr__(attribute, value)
    

    so you don't have to hardcode a whole set of names.

    Since your updated full script uses __inner_wrapper1 and __inner_wrapper2 as the actual attribute names, and you are using properties, you'll have to adjust your __setattr__ test to look for those names. Because you are using double-underscore names you need to adjust for the name mangling of such attributes:

    def __setattr__(self, attribute, value):
        if attribute in {
                'inner_wrapper1', 'inner_wrapper2',
                '_DataWrapper__inner_wrapper1', '_DataWrapper__inner_wrapper2'}:
            return super(DataWrapper, self).__setattr__(attribute, value)
    

    Unless you are going to subclass DataWrapper and must protect your attributes from accidental overriding, I'd avoid using double-underscored names altogether, however. In Pythonic code, you don't worry about other code accessing attributes, there is no concept of truly private attributes.

    Using properties is also overkill here; properties don't buy you encapsulation, in Python you'd only use those to simplify the API (replacing a method call with attribute access).

    Note that the hasattr() tests for the InnerWrapper* property* attributes will fail because you don't have default values:

    >>> inner = InnerWrapper1()
    >>> hasattr(inner, 'property1')
    False
    

    hasattr() doesn't test for properties, it simply tries to access an attribute and if any exception is raised it returns False:

    >>> inner = InnerWrapper1()
    >>> hasattr(inner, 'property1')
    False
    >>> inner.property1
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 43, in property1
    AttributeError: 'InnerWrapper1' object has no attribute '_InnerWrapper1__property1'
    >>> inner.property1 = 'foo'
    >>> inner.property1
    'foo'
    >>> hasattr(inner, 'property1')
    True
    

    By removing all the @property objects you can simplify this greatly:

    class DataWrapper(object):
        def __init__(self):
            self._inner_wrapper1 = InnerWrapper1()
            self._inner_wrapper2 = InnerWrapper2()
    
        def __setattr__(self, attribute, value):
            if attribute[0] == '_':
                return super(DataWrapper, self).__setattr__(attribute, value)
            if hasattr(self._inner_wrapper1, attribute):
                return setattr(self._inner_wrapper1, attribute, value)
            elif hasattr(self._inner_wrapper2, attribute):
                return setattr(self._inner_wrapper2, attribute, value)
    
        def __getattr__(self, attribute):
            try:
                return getattr(self._inner_wrapper1, attribute)
            except AttributeError: pass
            return getattr(self._inner_wrapper2, attribute)
    
    class InnerWrapper1(object):
        property1 = None
        property2 = None
    
    class InnerWrapper2(object):
        property3 = None 
        property4 = None