Python如何序列化词法闭包?

22

是否有一种使用标准库在Python中序列化词法闭包的方法?Pickle和marshal似乎不能处理词法闭包。我并不关心二进制与字符串序列化等细节,只要能工作即可。例如:

def foo(bar, baz) :
    def closure(waldo) :
        return baz * waldo
    return closure

我想要将闭包实例转储到文件中并读回来。

编辑: 相对明显的解决方法是使用一些反射技巧将词法闭包转换为类对象,反之亦然。然后可以将其转换为类,进行序列化、反序列化,再将其转换回闭包。实际上,由于Python是鸭子类型的,如果你重载了类的函数调用操作符以使其看起来像一个函数,那么你甚至不需要将其转换回闭包,而使用它的代码也不会发现区别。如果有任何Python反射API专家,请发言。


一个定义了词法闭包的代码示例可能会有所帮助。 - jfs
1
你知道如何将普通模块级函数的实例转储到文件中并读取它们吗? - jfs
我猜你可以用Pickle或其他什么方式来实现。我是一名经验丰富的程序员,但对Python非常陌生,因此我了解序列化和词法闭包,但对Python本身的工作原理知之甚少。 - dsimcha
我所知道的普通函数的选项仅有以下两种:1. 可以保存为源代码文件(*.py)/ 通过 import (或 execfile、eval 等)进行加载;2. 可以保存为序列化的代码对象(如 *.pyc 文件)/ 通过 marshal 将字符串转换为代码对象并执行(或 eval 等)。 - jfs
5个回答

20

PiCloud发布了一个开源(LGPL)的pickler,可以处理函数闭合和更多有用的内容。它可以独立于他们的云计算基础设施使用 - 它只是一个普通的pickler。整个过程在这里here进行了说明,您可以通过'pip install cloud'下载代码。总之,它可以实现您想要的功能。让我们通过pickle一个closure来演示:

import pickle
from StringIO import StringIO

import cloud

# generate a closure
def foo(bar, baz):
    def closure(waldo):
        return baz * waldo
    return closure
closey = foo(3, 5)

# use the picloud pickler to pickle to a string
f = StringIO()
pickler = cloud.serialization.cloudpickle.CloudPickler(f)
pickler.dump(closey)

#rewind the virtual file and reload
f.seek(0)
closey2 = pickle.load(f)

现在我们有closey,原始闭包,和closey2,从字符串序列化中恢复的闭包。让我们来测试一下。

>>> closey(4)
20
>>> closey2(4)
20

很好。这个模块是纯Python的——您可以打开它并轻松地了解其中的工作原理。(答案是许多代码。)


19
如果你一开始就使用一个带有__call__方法的类,那么它应该可以与pickle无缝配合工作。
class foo(object):
    def __init__(self, bar, baz):
        self.baz = baz
    def __call__(self,waldo):
        return self.baz * waldo

另一方面,将闭包转换为在运行时创建的新类的实例的黑客方法将不起作用,因为 pickle 处理类和实例的方式。 pickle 不存储类;只存储模块名和类名。在读回实例或类时,它试图导入模块并在其中找到所需的类。如果你使用了在运行时创建的类,那就没戏了。


1
#!python

import marshal, pickle, new

def dump_func(f):
    if f.func_closure:
        closure = tuple(c.cell_contents for c in f.func_closure)
    else:
        closure = None
    return marshal.dumps(f.func_code), f.func_defaults, closure


def load_func(code, defaults, closure, globs):
    if closure is not None:
        closure = reconstruct_closure(closure)
    code = marshal.loads(code)
    return new.function(code, globs, code.co_name, defaults, closure)


def reconstruct_closure(values):
    ns = range(len(values))
    src = ["def f(arg):"]
    src += [" _%d = arg[%d]" % (n, n) for n in ns]
    src += [" return lambda:(%s)" % ','.join("_%d"%n for n in ns), '']
    src = '\n'.join(src)
    try:
        exec src
    except:
        raise SyntaxError(src)
    return f(values).func_closure




if __name__ == '__main__':

    def get_closure(x):
        def the_closure(a, b=1):
            return a * x + b, some_global
        return the_closure

    f = get_closure(10)
    code, defaults, closure = dump_func(f)
    dump = pickle.dumps((code, defaults, closure))
    code, defaults, closure = pickle.loads(dump)
    f = load_func(code, defaults, closure, globals())

    some_global = 'some global'

    print f(2)

1

是的!我搞定了(至少我认为是)——也就是说,更通用的问题是如何将函数序列化。Python真是太棒了:),我通过dir()函数和几次网络搜索找到了大部分答案。解决这个问题真是太好了,我也需要它。

我还没有对co_code的强健性进行过多的测试(嵌套函数等),如果有人能查一下如何钩住Python,使函数可以自动序列化(例如,它们有时可能是闭包参数),那就太好了。

Cython模块_pickle_fcn.pyx

# -*- coding: utf-8 -*-

cdef extern from "Python.h":
    object PyCell_New(object value)

def recreate_cell(value):
    return PyCell_New(value)

Python文件

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# author gatoatigrado [ntung.com]
import cPickle, marshal, types
import pyximport; pyximport.install()
import _pickle_fcn

def foo(bar, baz) :
    def closure(waldo) :
        return baz * waldo
    return closure

# really this problem is more about pickling arbitrary functions
# thanks so much to the original question poster for mentioning marshal
# I probably wouldn't have found out how to serialize func_code without it.
fcn_instance = foo("unused?", -1)
code_str = marshal.dumps(fcn_instance.func_code)
name = fcn_instance.func_name
defaults = fcn_instance.func_defaults
closure_values = [v.cell_contents for v in fcn_instance.func_closure]
serialized = cPickle.dumps((code_str, name, defaults, closure_values),
    protocol=cPickle.HIGHEST_PROTOCOL)

code_str_, name_, defaults_, closure_values_ = cPickle.loads(serialized)
code_ = marshal.loads(code_str_)
closure_ = tuple([_pickle_fcn.recreate_cell(v) for v in closure_values_])
# reconstructing the globals is like pickling everything :)
# for most functions, it's likely not necessary
# it probably wouldn't be too much work to detect if fcn_instance global element is of type
# module, and handle that in some custom way
# (have the reconstruction reinstantiate the module)
reconstructed = types.FunctionType(code_, globals(),
    name_, defaults_, closure_)
print(reconstructed(3))

干杯,
尼古拉斯

编辑 - 对于真实世界的情况需要更强大的全局处理。fcn.func_code.co_names 列出全局名称。


0

命名元组包含一个动态定义类的函数。这个类支持序列化。

以下是其核心:

result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')

结合@Greg Ball的建议,在运行时创建一个新类,可能会回答你的问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接