Search code examples
python-2.7inheritancegetattrgetattribute

Python Inheritance and __getattr__ & __getattribute__


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:

  1. All past inheritance has been a single inheritance. In other words I have inherited BaseClass but the class that inherits BaseClass is NOT then inherited by another class. In today's problem, I inherit BaseClass into BaseFormData which is then inherited into July2013FormData which is then inherited into Jan2014FormData. So obviously there are more layers now.
  2. I have not been attempting to override __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?


Solution

  • 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.