Issue 1062277: Pickle breakage with reduction of recursive structures
Created on 2004-11-08 08:06 by ddorfman, last changed 2022-04-11 14:56 by admin.
| Files | ||||
|---|---|---|---|---|
| File name | Uploaded | Description | Edit | |
| redcycle.diff | ddorfman, 2004-11-08 08:06 | Diff | ||
| issue1062277-test-py3k.diff | belopolsky, 2010-07-15 06:34 | review | ||
| Messages (10) | |||
|---|---|---|---|
| msg47267 - (view) | Author: Dima Dorfman (ddorfman) | Date: 2004-11-08 08:06 | |
Fix problems related to reduce cycles during pickling. A
"reduce cycle" is what happens when a __reduce__
implementation returns an args that cycles back through the
object it tried to reduce. This can't work because the
unpickler has to call the constructor with those args, but
it doesn't yet have that object to be able to place inside
the args. There are two problems related to this:
1. The standard reduce implementation for proto < 2 in
copy_reg (_reduce_ex) doesn't correctly deal with
recursive structures. reduce_2 in typeobject.c does the
right thing by using listitems and dictitems. Fix
_reduce_ex by making it do the same thing. This is okay
for proto < 2 because those arguments are a pickler-
side feature. Tested in test_stdreducecycle.
2. Our pickle implementations don't check for reduce
cycles. This is somewhat cosmetic except that stack
overflow protection is imperfect (for cPickle), causing
crashes, and some kinds of cycles trigger asserts (in
pickle). Fixed in pickle and cPickle by introducing a
reducing_now set; on entering save_reduce, the object
id being saved must not be in that set or we've been
called recursively while saving the callable or
arguments. Tested in test_reduce_cycle.
This shouldn't change any semantics. That reduce shouldn't
introduce cycles through args isn't documented, but it can't
work any other way.
Possible improvement: If we want to support reducing of real
immutable containers we might have to relax the reduction
cycle test to give the cycle a chance of resolving itself
normally (by constructing a partial object to pass to the
constructor and filling it in later). I'm not sure if this
trouble is worth it just to avoid writing a
frozenset.__setstate__.
See also: http://mail.python.
org/pipermail/python-dev/2004-October/049714.html
It would be very good if someone familiar with pickle
reviewed this; I am still not very confident that I
completely understand all the issues.
|
|||
| msg47268 - (view) | Author: Raymond Hettinger (rhettinger) * ![]() |
Date: 2004-11-09 07:31 | |
Logged In: YES user_id=80475 IMO, this is more of a feature request than a bug. Also, it needs to be fully thought-out and discussed. Putting this sort of thing in just before the Py2.4 release candidate is probably not wise. |
|||
| msg47269 - (view) | Author: Dima Dorfman (ddorfman) | Date: 2004-11-09 09:06 | |
Logged In: YES user_id=908995 Triggering assertions (pickle) and producing incorrect output (cPickle) are certainly bugs, and being able to pickle a recursive structure is not a feature request. The copy_reg part of the patch is the real fix, and as it's almost a translation of what reduce_2 already does, it should be safe for 2.4. I agree that the rest of the patch--to check for cycles during pickling--should wait until 2.5. |
|||
| msg82105 - (view) | Author: Daniel Diniz (ajaksu2) * ![]() |
Date: 2009-02-14 18:45 | |
Patch has test, can someone try it? I think it might be fixed already. |
|||
| msg110350 - (view) | Author: Alexander Belopolsky (belopolsky) * ![]() |
Date: 2010-07-15 06:34 | |
The issue is still present in py3k. Attaching an updated patch with tests only. Is this the same as issue998998? |
|||
| msg110868 - (view) | Author: Alexander Belopolsky (belopolsky) * ![]() |
Date: 2010-07-20 05:38 | |
As I explained in msg110617 under issue9269, it is possible that we can do better than simply detect reduce cycles and bail out. I am torn between two options: 1. Reject this patch and wait until a proper solution is found. 2. Admit that better is the enemy of good and start with proper error reporting on reduce cycles by accepting this patch. My only concern about this patch is that it is likely to affect performance because pickling will have to maintain the "reducing_now" dictionary that is parallel to the memo dictionary. |
|||
| msg178449 - (view) | Author: Eugene Toder (eltoder) * | Date: 2012-12-29 00:44 | |
To recap, the issue is that pickle doesn't handle recursion via reduce arguments (i.e. arguments to the constructor function as returned in 2nd element of the tuple from __reduce__). This leads to 2 kind of effects:
class C:
def __init__(self, x=None):
self.x = x if x is not None else self
def __reduce__(self):
return C, (self.x,)
A. Recursion error:
>>> pickle.dumps(C())
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
pickle.dumps(C())
RuntimeError: maximum recursion depth exceeded while calling a Python object
This cannot be helped with the current reduce protocol. The error may be improved, but that's about it.
B. Duplication of object when unpickling:
>>> c = C([])
>>> c.x.append(c)
>>> c.x[0] is c
True
>>> c2 = pickle.loads(pickle.dumps(c))
>>> c2.x[0] is c2
False
This happens because list (or another recursion-friendly type) inside the problematic object handles recursion, but we still get the outer object, identical to the inner one.
This can be solved the same way as for tuple:
>>> t=([],1,2)
>>> t[0].append(t)
>>> t2 = pickle.loads(pickle.dumps(t))
>>> t2[0][0] is t2
True
>>> pickletools.dis(pickle.dumps(t))
0: \x80 PROTO 3
2: ] EMPTY_LIST
3: q BINPUT 0
5: h BINGET 0
7: K BININT1 1
9: K BININT1 2
11: \x87 TUPLE3
12: q BINPUT 1
14: a APPEND
15: K BININT1 1
17: K BININT1 2
19: 0 POP
20: 0 POP
21: 0 POP
22: h BINGET 1
24: . STOP
After pickling its elements tuple checks if it got into memo. If it did, this means it was pickled by one of the elements, so it POPs all elements from the stack and fetches itself via GET. This is somewhat inefficient, but probably the best it can do.
I suggest we do 3 things:
1. Improve the documentation for __reduce__ function. It should mention that all state that a) can potentially point back to the object and b) not strictly necessary in the constructor function should be passed via the 3rd element of __reduce__ tuple (aka state) instead of the 2nd element, and applied by __setstate__. This handles recursion in robust and optimal way.
2. Check that all built-in/standard types follow this advice. I see that Stefan Mihaila already fixed sets.
3. To fix case B above add the memo check from save_tuple to save_reduce. While at it, we can consider checking after pickling every element instead of after pickling all elements, so we reduce the number of POPs and the wastage they cause.
|
|||
| msg221898 - (view) | Author: Alexander Belopolsky (belopolsky) * ![]() |
Date: 2014-06-29 20:25 | |
Problem "B" has been resolved, but problem "A" is still there. Python 3.4.1 (default, Jun 29 2014, 15:26:46) [GCC 4.4.7 20120313 (Red Hat 4.4.7-4)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> class C: ... def __init__(self, x=None): ... self.x = x if x is not None else self ... def __reduce__(self): ... return C, (self.x,) ... >>> import pickle >>> pickle.dumps(C()) Traceback (most recent call last): File "<stdin>", line 1, in <module> RuntimeError: maximum recursion depth exceeded while calling a Python object >>> c = C([]) >>> c.x.append(c) >>> c.x[0] is c True >>> c2 = pickle.loads(pickle.dumps(c)) >>> c2.x[0] is c2 True |
|||
| msg255046 - (view) | Author: Davis Herring (herring) | Date: 2015-11-21 08:17 | |
Re msg110868: it's impossible to resolve a __reduce__ loop that involves only immutable intermediate objects (including none at all): class Direct: def __reduce__(self): return id,(self,) # obviously impossible class Indirect: # Can't create either the new object or the tuple first! def __reduce__(self): return id,((self,),) With pre-existing mutable objects, the same trick as for tuples certainly could be applied: class Mutable: def __init__(self): self.bracketed=[self] # Create list, REDUCE, then populate list def __reduce__(self): return id,(self.bracketed,) The way an analog to the tuple implementation would deal with this would be something like id # the dummy "constructor" in this example [] # empty list, memoized immediately as, say, #5 id # try again to store the Mutable @5 # fetch from memo T1 # make singleton tuple R # reduce (have now succeeded in "making" the Mutable as, say, #20) APP # to the list T1 # finish the first __reduce__ attempt POP # abandon it, since the object has been created POP # abandon the "id" as well @20 # fetch the previous answer If the list were created directly in __reduce__, you'd still recurse infinitely trying to create each new list object. On the other hand, even complicated cases like this should work: class Switch: def __reduce__(self): return id,(self.lst,) a=Switch(); b=Switch() a.list=[b,a]; b.list=[a,b] where the pickling of, say, a would look something like id [] # -> #17 id # trying b now [] # -> #42 id # trying a again @17 T1 R # a done (#88) APP id # trying b again @42 T1 R # b done (#101) APP # b's list now populated POP POP # b abandoned @101 # b fetched APP # finally building a's list @88 # a is long done APP # a's list now populated POP POP # a abandoned @88 # final value: a Perhaps a technique for distinguishing these cases is to look for new objects having been created since the last time we visited an object. If there are none, we're in the immutable case and we lose. If there are yet more created between the second and third visits, on the other hand, we're generating more objects from __reduce__ every time and should again abort. |
|||
| msg255067 - (view) | Author: Eugene Toder (eltoder) * | Date: 2015-11-21 19:54 | |
Davis, the possible cases (i.e. recursion via appropriate mutable objects) are already supported, and the impossible cases are, well, impossible. The only open issue is whether to implement better error handling for infinite recursion problems, instead of relying on built-in maximum recursion depth. |
|||
| History | |||
|---|---|---|---|
| Date | User | Action | Args |
| 2022-04-11 14:56:08 | admin | set | github: 41150 |
| 2021-12-16 23:07:00 | ajaksu2 | set | versions: + Python 3.11, - Python 3.5 |
| 2015-11-21 19:54:36 | eltoder | set | messages: + msg255067 |
| 2015-11-21 08:17:59 | herring | set | nosy:
+ herring messages: + msg255046 |
| 2014-06-29 20:25:17 | belopolsky | set | assignee: belopolsky -> messages: + msg221898 versions: + Python 3.5, - Python 2.7, Python 3.2, Python 3.3, Python 3.4 |
| 2012-12-29 00:44:37 | eltoder | set | versions: + Python 3.3, Python 3.4, - Python 3.1 |
| 2012-12-29 00:44:17 | eltoder | set | nosy:
+ pitrou, eltoder messages: + msg178449 |
| 2012-07-27 14:37:38 | pitrou | unlink | issue9269 dependencies |
| 2012-07-27 12:56:12 | mstefanro | set | nosy:
+ mstefanro |
| 2012-07-27 01:21:51 | zzzeek | set | nosy:
+ zzzeek |
| 2011-04-24 18:34:59 | alex | set | nosy:
+ alex |
| 2011-03-08 12:59:22 | bcroq | set | nosy:
+ bcroq |
| 2010-08-19 18:26:57 | BreamoreBoy | set | type: enhancement -> behavior versions: + Python 3.1 |
| 2010-07-20 05:38:29 | belopolsky | set | superseder: Cannot pickle self-referencing sets messages: + msg110868 |
| 2010-07-20 05:11:35 | terry.reedy | set | nosy:
rhettinger, belopolsky, ddorfman, ajaksu2, alexandre.vassalotti components: + Library (Lib), - Interpreter Core |
| 2010-07-16 00:16:58 | belopolsky | link | issue9269 dependencies |
| 2010-07-15 21:55:54 | belopolsky | link | issue1581183 superseder |
| 2010-07-15 06:34:45 | belopolsky | set | files:
+ issue1062277-test-py3k.diff assignee: belopolsky messages:
+ msg110350 |
| 2009-02-14 18:45:34 | ajaksu2 | set | type: enhancement messages: + msg82105 nosy: + ajaksu2 versions: + Python 2.7 |
| 2004-11-08 08:06:54 | ddorfman | create | |

