Infinite recursion in __reduce__ when calling original base class reduce, why?

Chris Torek nospam at torek.net
Mon Jun 13 20:40:37 EDT 2011


In article <4df669ea$0$49182$e4fe514c at news.xs4all.nl>
Irmen de Jong  <irmen.NOSPAM at xs4all.nl> wrote:
>I've pasted my test code below. It works fine if 'substitute' is True,
>but as soon as it is set to False, it is supposed to call the original
>__reduce__ method of the base class. However, that seems to crash
>because of infinite recursion on Jython and IronPython and I don't
>know why. It works fine in CPython and Pypy.

In this particular case (no fancy inheritance going on), the base
__reduce__ method would be object.__reduce__.  Perhaps in those
implementations, object.__reduce__ goes back to TestClass.__reduce__,
rather than being appropriately magic.

>I wonder if my understanding of __reduce__ is wrong, or that I've
>hit a bug in IronPython and Jython?  Do I need to do something with
>__reduce_ex__ as well?

You should not *need* to; __reduce_ex__ is just there so that you
can do something different for different versions of the pickle
protocol (I believe).

Nonetheless, there is something at least slightly suspicious here:

>import pickle
>
>class Substitute(object):
>    def __init__(self, name):
>        self.name=name
>    def getname(self):
>        return self.name
>
>class TestClass(object):
>    def __init__(self, name):
>        self.name=name
>        self.substitute=True
>    def getname(self):
>        return self.name
>    def __reduce__(self):
>        if self.substitute:
>            return Substitute, ("SUBSTITUTED:"+self.name,)
>        else:
>            # call the original __reduce__ from the base class
>            return super(TestClass, self).__reduce__()  #  crashes on
>ironpython/jython
[snip]

In general, the way __reduce__ is written in other class implementations
(as distributed with Python2.5 at least) boils down to the very
simple:

    def __reduce__(self):
        return self.__class__, (arg, um, ents)

For instance, consider a class with a piece that looks like this:

    def __init__(self, name, value):
        self.name = name
        self.value = value
        self.giant_cached_state = None

    def make_parrot_move(self):
        if self.giant_cached_state is None:
            self._do_lots_of_computation()
        return self._quickstuff_using_cache()

Here, the Full Internal State is fairly long but the part that
needs to be saved (or, for copy operations, copied -- but you can
override this with __copy__ and __deepcopy__ members, if copying
the cached state is a good idea) is quite short.  Pickled instances
need only save the name and value, not any of the computed cached
stuff (if present).  So:

    def __reduce__(self):
        return self.__class__, (name, value)

If you define this (and no __copy__ and no __deepcopy__), the
pickler will save the name and value and call __init__ with the
name and value arguments.  The copy.copy and copy.deepcopy operations
will also call __init__ with these arguments (unless you add
__copy__(self) and __deepcopy__(self) functions).

So, it seems like in this case, you would want:

    def __reduce__(self):
        if self.substitute:
            return Substitute, ("SUBSTITUTED:"+self.name,)
        else:
            return self.__class__, (self.name,)

or if you want to be paranoid and only do a Substitute if
self.__class__ is your own class:

    if type(self) == TestClass and self.substitute:
            return Substitute, ("SUBSTITUTED:"+self.name,)
        else:
            return self.__class__, (self.name,)

In CPython, if I import your code (saved in foo.py):

    >>> x = foo.TestClass("janet")
    >>> x
    <foo.TestClass object at 0x66290>
    >>> x.name
    'janet'
    >>> x.__reduce__()
    (<class 'foo.Substitute'>, ('SUBSTITUTED:janet',))
    >>> x.substitute=False
    >>> x.__reduce__()
    (<function _reconstructor at 0x70bf0>, (<class 'foo.TestClass'>, <type 'object'>, None), {'name': 'janet', 'substitute': False})

which is of course the same as:

    >>> object.__reduce__(x)
    (<function _reconstructor at 0x70bf0>, (<class 'foo.TestClass'>, <type 'object'>, None), {'name': 'janet', 'substitute': False})

which means that CPython's object.__reduce__() uses a "smart" fallback
reconstructor.  Presumably IronPython and Jython lack this.
-- 
In-Real-Life: Chris Torek, Wind River Systems
Salt Lake City, UT, USA (40°39.22'N, 111°50.29'W)  +1 801 277 2603
email: gmail (figure it out)      http://web.torek.net/torek/index.html



More information about the Python-list mailing list