I have fought with this all day long and done plenty of Google searches. I am having what appears to be an inheritance problem.
I have a class named BaseClass that does some simple things for me like set logging defaults, hold the log, and manage Read Only attributes. In the past I have had no issues with this class but I can say I've only been using python for a couple of months now. Additionally, there are a couple of caveats I suspect are important:
__getattr__
or __getattribute__
in any of the classes that inherited BaseClass in the past. Today I am. BaseClass has a __getattribute__
method of its own in order to manage the getting of Read Only attributes. The BaseFormData class has a __getattr__
method because I'd read this was the right way to provide access to data without having to explicitly declare all attributes. The form data being loaded up has a dozen or more pieces of data in it (depending on the version) so I'm explicitly declaring some properties that are vital or aliased while leaving the rest to handled by __getattr__
.Here is the __getattribute__
from BaseClass:
def __getattribute__(self, attr):
try:
# Default behaviour
return object.__getattribute__(self, attr)
except:
try:
return self.__READ_ONLY[attr]
except KeyError:
lcAttr = php_basic_str.strtolower(attr)
return self._raw[lcAttr]
The lines that use self._raw
are there solely because classes that inherit BaseClass often times use _raw
. There is no _raw
that BaseClass requires. Ideally the reference to _raw
would only appear in the __getattr__
or __getattribute__
of the inheriting class. However, I need to use __getattribute__
in BaseClass in order to get the Read Only functionality working properly.
And the __getattr__
from BaseFormData:
def __getattr__(self, name):
"""
Return a particular piece of data from the _raw dict
"""
# All the columns are saved in lowercase
lcName = s.strtolower(name)
try:
tmp = self._raw[lcName]
except KeyError:
try:
tmp = super(BaseFormData, self).__getattribute__(name)
except KeyError:
msg = "'{0}' object has no attribute '{1}'. Note that attribute search is case insensitive."
raise AttributeError(msg.format(type(self).__name__, name))
if tmp == 'true':
return True
elif tmp == 'false':
return False
else:
return tmp
Here is the error I'm receiving:
File "D:\python\lib\mybasics\mybasics\cBaseClass.py", line 195, in __getattribute__
return self.__READ_ONLY[attr]
RuntimeError: maximum recursion depth exceeded while calling a Python object
The short version is that I have fought all day with this because with a tweak here or there I get differing response. Most of the time __getattr__
from BaseFormData is never called. Other times I get this recursion error. Other time I can get it to work but then I add something small and everything breaks again.
There is something that I'm clearly missing with regards to inheritance and the order in which __getattribute__
and __getattr__
are called. I've done a lot of tweaking of the code today and if memory serves I can't have a __getattribute__
in BaseFormData as well as BaseClass but I don't remember the error off the top of my head.
I'm guessing the recursion issue stems from this line in __getattr__
:
tmp = super(BaseFormData, self).__getattribute__(name)
Obviously, what I'm wanting to do is look in the current class first, then go to the BaseClass __getattribute__
in order to check for Read Only attributes.
Any help is greatly appreciated.
----------- Results of a few tweaks.... -----------
So I changed BaseFormClass __getattr__
to __getattribute__
and it seemed to run before BaseClass's __getattribute__
which makes sense.
However it led to infinite recursion which I thought might be due to the order certain things happened in the __init__
of BaseFormClass and children classes of BaseFormClass. Then it seemed to be due to __READ_ONLY being created too late and I fixed that as well.
Ultimately, _raw
is in the children class not in BaseClass so I removed any reference to _raw
in BaseClass's _getattribute__
. I took rchang's suggestion about changing self to object and this helped in BaseClass's __getattribute__
but seemed to cause issues in BaseFormData's. I have gotten the "get" portion to work from the interpreter with the code below:
BaseClass:
def __getattribute__(self, attr):
try:
# Default behaviour
return object.__getattribute__(self, attr)
except:
return self.__READ_ONLY[attr]
BaseFormClass:
def __getattribute__(self, name):
# All the columns are saved in lowercase
lcName = s.strtolower(name)
try:
# Default behaviour
return object.__getattribute__(self, name)
except:
try:
tmp = object.__getattribute__(self, '_raw')[lcName]
except KeyError:
try:
tmp = BaseClass.__getattribute__(self, name)
except KeyError:
msg = "'{0}' object has no attribute '{1}'. Note that attribute search is case insensitive."
raise AttributeError(msg.format(type(self).__name__, name))
if tmp == 'true':
return True
elif tmp == 'false':
return False
else:
return tmp
Ironically this has created another __getattribute__
issue though. When I attempt to overwrite a Read Only attr the log warning fires but then fails on a call to self.__instanceId
. This log warning worked fine yesterday (when I could get the classes to instantiate without error). And I can instantiate the class like this:
a = Jan2014Lead(leadFile)
and get the Instance ID like this:
a.__instanceId
Out[7]: '8c08dee80ef56b1234fc4822627febfc'
Here's the actual error:
File "D:\python\lib\mybasics\mybasics\cBaseClass.py", line 255, in write2log
logStr = "[" + self.__instanceId + "]::[" + str(msgCode) + "]::" + msgMsg
File "cBaseFormData.py", line 226, in __getattribute__
raise AttributeError(msg.format(type(self).__name__, name))
AttributeError: 'Jan2014Lead' object has no attribute '_BaseClass__instanceId'. Note that attribute search is case insensitive.
----------- Got it working but seems hacky.... -----------
So the error above is from looking for _BaseClass__instanceId in _READ_ONLY but __instanceId is in _READ_ONLY. So I simply trimmed the attr string passed to remove _BaseClass from the beginning.
This seems like a hack though. Is there a standard way to do this?
I can't say I ever fully figured this out to my satisfaction. However, the BaseClass __getattirbute__
method required a change in the order in which things were being called:
def __getattribute__(self, attr):
if attr in self.__READ_ONLY:
return self.__READ_ONLY[attr]
else:
# Default behaviour
return object.__getattribute__(self, attr)
Then I needed another change to the __getattribute__
in BaseFormClass:
def __getattribute__(self, name):
# All the columns are saved in lowercase
lcName = s.strtolower(name)
try:
# Default behaviour
return object.__getattribute__(self, name)
except:
name = s.str_replace('_' + type(self).__name__, '', name)
lcName = s.strtolower(name)
try:
tmp = object.__getattribute__(self, '_raw')[lcName]
except KeyError:
#tmp = BaseClass.__getattribute__(self, name)
tmp = super(BaseFormData, self).__getattribute__(name)
if tmp == 'true':
return True
elif tmp == 'false':
return False
else:
return tmp
There are really just 2 changes here. The easy one first, I removed the raise and let the BaseClass handle that. The harder to describe change is the one where I strip out the class name using s.str_replace
. Without this line Python was often looking for something like _BaseClass__{some attr}
in the _READ_ONLY
dict. However, it was only named {some attr}
within the _READ_ONLY
dict. So I strip the _BaseClass_
portion out.
Obviously this is a hack. I'm not sure if implementing some form of aliasing wouldn't be better than stripping it out. Possibly more explicit? Either way it seems like a hack to get around Python's built-in method for keeping attributes unique among classes.
I imagine there is a better way to do this and this will likely get completely rewritten in a year or so to reflect that better way.