Reputation: 640
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()
Upvotes: 1
Views: 2478
Reputation: 1124228
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
Upvotes: 1