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