为什么生成器不能被序列化?
Python的pickle模块(这里指的是标准的Python 2.5/2.6/2.7)不能处理锁、文件对象等。
它也不能处理生成器和lambda表达式(或者其他任何匿名代码),因为pickle实际上只存储名称的引用。
对于锁和依赖操作系统的特性,不能处理它们的原因是显而易见的,这很合理。
但是为什么生成器不能被处理呢?
注意:为了更清楚,我想了解的是根本原因(或者设计决策背后的假设和选择)为什么,而不是“因为它会给你一个Pickle错误”。
我意识到这个问题有点宽泛,所以这里有一个简单的判断标准来看看你是否回答了这个问题:“如果这些假设被提出,或者允许的生成器类型在某种程度上更受限制,生成器的处理会再次有效吗?”
2 个回答
其实你是可以做到的,这要看具体的实现方式。PyPy 和 Stackless Python 都在某种程度上支持这个功能:
Python 2.7.1 (dcae7aed462b, Aug 17 2011, 09:46:15)
[PyPy 1.6.0 with GCC 4.0.1] on darwin
Type "help", "copyright", "credits" or "license" for more information.
And now for something completely different: ``Not your usual analyses.''
>>>> import pickle
>>>> gen = (x for x in range(100))
>>>> next(gen)
0
>>>> pickled = pickle.dumps(gen)
>>>> next(pickle.loads(pickled))
1
在 CPython 中,你也可以创建一个 迭代器对象,来模拟一个可以被保存的生成器。
关于这个问题,有很多信息可以参考。如果想了解官方的看法,可以看看这个(已关闭的) Python bug跟踪问题。
做出这个决定的其中一位人士在这篇博客中详细解释了原因:
生成器本质上是一个升级版的函数,我们需要保存它的字节码,但字节码在不同版本的Python之间不一定兼容。此外,生成器的状态(比如局部变量、闭包和指令指针)也需要保存。实现这一点相当麻烦,因为这基本上意味着整个解释器都要支持序列化。因此,支持生成器的序列化需要对CPython的核心做大量修改。
如果生成器的局部变量中有一些不支持序列化的对象(比如文件句柄、套接字、数据库连接等),那么这个生成器就无法自动序列化,无论我们是否为生成器实现了序列化支持。在这种情况下,你仍然需要提供自定义的
__getstate__
和__setstate__
方法。这使得对生成器的序列化支持变得相当有限。
这里提到两种建议的解决方法:
如果你需要这样的功能,可以考虑使用Stackless Python,它实现了上述所有功能。而且,由于Stackless的解释器是可序列化的,你还可以免费获得进程迁移的功能。这意味着你可以中断一个任务(Stackless的绿色线程叫做tasklet),将其序列化,发送到另一台机器,反序列化后继续执行,这样你就完成了进程迁移。这真是个非常酷的功能!
但在我看来,解决这个问题的最佳方法是将生成器重写为简单的迭代器(也就是带有
__next__
方法的那种)。迭代器的状态是明确的,因此序列化起来既简单又高效。不过,你仍然需要显式处理一些代表外部状态的对象;这点是无法避免的。