4

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.

paul23
  • 8,799
  • 12
  • 66
  • 149
  • 1
    I don't know but I expect it has to do with name mangling of double-underscore-prefixed items. Does it work if you use `_all_parts` instead? – Daniel Roseman Jan 22 '16 at 15:23
  • I'm not sure that `__getattr__` implementation is a good idea. What if the attribute does not add well? I don't know if this is the case in your scenario, but there might be attributes that (a) can not be added, or (b) where addition does not make sense (e.g., the velocity of the sattelite is not the sum of the velocities of it's parts). – tobias_k Jan 22 '16 at 15:24
  • @DanielRoseman: Well that fixes for directly calling it - but still using "`copy.copy(t)`" gives the same recursion error. – paul23 Jan 22 '16 at 15:27
  • Possible duplicate of [\_\_getattr\_\_ going recursive in python](http://stackoverflow.com/questions/11145501/getattr-going-recursive-in-python) – kdopen Jan 22 '16 at 15:30
  • @tobias_k - well I wish for a satellite to behave like a "sum of parts" and parts have a property "mass" - so satellites also need to have this property etc etc. And if a simple sum is impossible I can add a specific version to satellite, or overload the summation. – paul23 Jan 22 '16 at 15:30
  • @DanielRoseman: no, because `copy.copy()` works with instances *without calling `__init__`*. And it tries to use `hasattr(empty_instance, '__setstate__')`, which in turn calls `getattr(empty_instance, '__setstate__')`. And then all hell breaks loose. – Martijn Pieters Jan 22 '16 at 15:31

1 Answers1

6

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)
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343