Pickle doesn't take my registered custom type reduce function into account on python 2.6

pickle, python, python-2.6

Solution

A new feature was added to Python 2.7.3 that allows for dynamically created classes to be pickled. See issue 7689:

Currently, it's impossible to use the usual pickle mechanisms to pickle a dynamically created class, even if the user requests a different pickling mechanism via copy_reg. The attached patch makes this customization possible by simply transposing two blocks of code.

and

The copy_reg module exists to handle custom pickling of objects, which is exactly what's needed here. However, the code from #494904 that checks for instances of a metaclass does this *just before* it looks at the copy_reg dispatch table. The patches just reorder these tests in pickle.py and cPickle.c, so that one can register a custom pickler for instances of a metaclass.

And from the 2.7.3 changelog:

Issue #7689: Allow pickling of dynamically created classes when their metaclass is registered with copy_reg. Patch by Nicolas M. Thiéry and Craig Citro.

The only way around this for Python 2.6 is to port the `pickle.py` module over to that version. You could try to just bundle the 2.7.3 `pickle.py` module with 2.6, as a pure Python implementation it should Just Work.

Alternatively, monkey-patch the `pickle.Pickler.save` method:

from copy_reg import dispatch_table
from types import TypeType, StringType, TupleType
from pickle import Pickler, PicklingError


def pickler_save(self, obj):
    # Check for persistent id (defined by a subclass)
    pid = self.persistent_id(obj)
    if pid:
        self.save_pers(pid)
        return

    # Check the memo
    x = self.memo.get(id(obj))
    if x:
        self.write(self.get(x[0]))
        return

    # Check the type dispatch table
    t = type(obj)
    f = self.dispatch.get(t)
    if f:
        f(self, obj) # Call unbound method with explicit self
        return

    # Check copy_reg.dispatch_table
    reduce = dispatch_table.get(t)
    if reduce:
        rv = reduce(obj)
    else:
        # Check for a class with a custom metaclass; treat as regular class
        try:
            issc = issubclass(t, TypeType)
        except TypeError: # t is not a class (old Boost; see SF #502085)
            issc = 0
        if issc:
            self.save_global(obj)
            return

        # Check for a __reduce_ex__ method, fall back to __reduce__
        reduce = getattr(obj, "__reduce_ex__", None)
        if reduce:
            rv = reduce(self.proto)
        else:
            reduce = getattr(obj, "__reduce__", None)
            if reduce:
                rv = reduce()
            else:
                raise PicklingError("Can't pickle %r object: %r" %
                                    (t.__name__, obj))

    # Check for string returned by reduce(), meaning "save as global"
    if type(rv) is StringType:
        self.save_global(obj, rv)
        return

    # Assert that reduce() returned a tuple
    if type(rv) is not TupleType:
        raise PicklingError("%s must return string or tuple" % reduce)

    # Assert that it returned an appropriately sized tuple
    l = len(rv)
    if not (2 <= l <= 5):
        raise PicklingError("Tuple returned by %s must have "
                            "two to five elements" % reduce)

    # Save the reduce() output and finally memoize the object
    self.save_reduce(obj=obj, *rv)


Pickler.save = pickler_save

With the above monkey patch, your example works on Python 2.6.8:

>>> pickle.dumps(B)
Hey there
'c__main__\nB\np0\n.'

Problem

Given the following code: ``` import pickle import copy_reg class A(type): pass class B(object): __metaclass__ = A def _reduce_a(a): print('Hey there') return a.__name__ copy_reg.pickle(A, _reduce_a) pickle.dumps(B) ``` The registered `_reduce_a` function is never called on python 2.6 when attempting to pickle `B` which is an `A` instance but it's called on 2.7. It this a known bug?

Original source