Search code examples
pythonrecursionprivate-membersgetattr

Infinite recursion for __getattr__() - but why is it even called once?


Well I have a class that consist of (relevant part):

class satellite(object):
    def __init__(self, orbit, payload=None, structural_mass=0, structural_price=0):
        self.__all_parts = []
        self.orbit = orbit
        self.payload = payload
        self.structural_mass = structural_mass
        self.structural_price = structural_price

    def __getattr__(self, item):
        found = False
        v = 0
        for i in self.__all_parts:
            t = getattr(i, item, None)
            if t is not None:
                v += t
                found = True
        if found:
            return v
        else:
            raise AttributeError(item)

Basically what I wish to do is propagate all (sum of) attributes from the "parts" into the satellite. Ie if I have 10 parts that have mass, the mass of the satellite is the sum of those. - If I then add another part that has energy storage - I immediatelly can look that also up. - If no part has the attribute, the attribute is considered to be "bad"/"unexistent" and it raises the normal error.

Now this works, except when I do:

s = satellite(None) #most simplistic one
ss = copy.copy(s) 

The whole thing bugs out, giving an infinite recursion depth error in __getattr__().

Now inspection (pycharm's debugger) shows me that it keeps iterating the getattr with as argument:

item = _satellite__all_parts And it starts its next iteration at the line for i in self.__all_parts:

Now I'm startled by this: why is this line even going to __getattr_() - as far as I know __getattr__ is only called for attributes that aren't existing right? - But self.__all_parts is obviously declared in the __init__ event of the object, so why is __getattr__ even activated? And furthermore: why does it not understand the object anymore?

And of course: how can I make this work?

EDIT: just for clarity - this occurs ONLY DUE TO COPY, and it is (was thanks to martijn) specific to the copying behaviour. The linked question doesn't handle the copy case.


Solution

  • copy.copy() tries to access the __setstate__ attribute on an empty instance, no attributes set yet, because __init__ hasn't been called (and won't be; copy.copy() is responsible to set the new attributes on it).

    The __setstate__ method copy.copy() is looking for is optional, and your class indeed has no such attribute. Because it is missing, __getattr__ is called for it, and because there is no __all_parts attribute yet either, the for i in self.__all_parts: line ends up calling __getattr__ again. And again and again and again.

    The trick is to cut out of this loop early by testing for it. The best way to do this is to special-case all attributes that start with an underscore:

    if item.startswith('_'):
        # bail out early
        raise AttributeError(item)