Search code examples
matplotlibscalepickle

pickable figures in matplotlib and Log10Transform


You may already know, that in matplotlib 1.2.0 there is a new experimental feature, that figures are pickable (they can be saved with pickle module).

However, it doesn't work when one uses logscale, eg.

import matplotlib.pyplot as plt
import numpy as np
import pickle
ax = plt.subplot(111)
x = np.linspace(0, 10)
y = np.exp(x)
plt.plot(x, y)
ax.set_yscale('log')
pickle.dump(ax, file('myplot.pickle', 'w'))

results in:

PicklingError: Can't pickle <class 'matplotlib.scale.Log10Transform'>: attribute lookup matplotlib.scale.Log10Transform failed

Anybody knows any solution/workaround to this?


Solution

  • I've opened this as a bug report on matplotlib's github issue tracker. Its a fairly easy fix to implement on the matplotlib repository side (simply don't nest the Log10Transform class inside the LogScale class), but that doesn't really help you in being able to use this with mpl 1.2.0...

    There is a solution to getting this to work for you in 1.2.0, but I warn you - its not pretty!

    Based on my answer to a pickling question it is possible to pickle nested classes (as Log10Transform is). All we need to do is to tell Log10Transform how to "reduce" itself:

    import matplotlib.scale
    
    class _NestedClassGetter(object):
        """
        When called with the containing class as the first argument, 
        the name of the nested class as the second argument,
        and the state of the object as the third argument,
        returns an instance of the nested class.
    
        """
        def __call__(self, containing_class, class_name, state):
            nested_class = getattr(containing_class, class_name)
            # return an instance of a nested_class. Some more intelligence could be
            # applied for class construction if necessary.
            c = nested_class.__new__(nested_class)
            c.__setstate__(state)
            return c
    
    def _reduce(self):
        # return a class which can return this class when called with the 
        # appropriate tuple of arguments
        cls_name = matplotlib.scale.LogScale.Log10Transform.__name__
        call_args = (matplotlib.scale.LogScale, cls_name, self.__getstate__())
        return (_NestedClassGetter(), call_args)
    
    matplotlib.scale.LogScale.Log10Transform.__reduce__ = _reduce 
    

    You might also decide to do this for other Log based transforms/classes, but for your example, you can now pickle (and successfully unpickle) your example figure:

    import matplotlib.pyplot as plt
    import numpy as np
    import pickle
    
    
    ax = plt.subplot(111)
    x = np.linspace(0, 10)
    y = np.exp(x)
    plt.plot(x, y)
    ax.set_yscale('log')
    
    pickle.dump(ax, file('myplot.pickle', 'w'))
    plt.savefig('pickle_log.pre.png')
    plt.close()
    
    pickle.load(file('myplot.pickle', 'r'))
    plt.savefig('pickle_log.post.png')
    

    I'm going to get on and fix this for mpl 1.3.x so that this nasty workaround isn't needed in the future :-) .

    HTH,