在Python中将函数反序列化到不同上下文
我写了一个Python接口,用于我们公司内部正在开发和使用的一个以流程为中心的工作分配系统。虽然使用这个接口的人都是相当熟练的程序员,但主要是研究科学家,而不是软件开发者,所以让这个接口简单易用、尽量不干扰他们的工作是最重要的。
我的库会把一系列输入展开成一系列的pickle文件,存放在一个共享的文件服务器上,然后启动一些任务,这些任务会加载这些输入,进行计算,保存结果为pickle文件,然后退出;客户端脚本接着会继续执行,生成一个加载并返回结果的生成器(或者重新抛出计算函数产生的任何异常)。
这个过程之所以有用,是因为计算函数本身也是被序列化的输入。cPickle可以很高兴地处理函数引用,但要求被序列化的函数必须在同一个上下文中可以重新导入。这就有点麻烦了。我已经解决了找到模块以重新导入的问题,但大多数情况下,被序列化的函数是顶层函数,因此没有模块路径。我找到的唯一能在计算节点上反序列化这种函数的方法,就是用一种让人恶心的小技巧,模拟函数被序列化时的原始环境,然后再进行反序列化:
...
# At this point, we've identified the source of the target function.
# A string by its name lives in "modname".
# In the real code, there is significant try/except work here.
targetModule = __import__(modname)
globalRef = globals()
for thingie in dir(targetModule):
if thingie not in globalRef:
globalRef[thingie] = targetModule.__dict__[thingie]
# sys.argv[2]: the path to the pickle file common to all jobs, which contains
# any data in common to all invocations of the target function, then the
# target function itself
commonFile = open(sys.argv[2], "rb")
commonUnpickle = cPickle.Unpickler(commonFile)
commonData = commonUnpickle.load()
# the actual function unpack I'm having trouble with:
doIt = commonUnpickle.load()
最后一行是这里最重要的部分——这是我的模块在获取它应该运行的函数。这段代码按预期工作,但直接操作符号表让我感到不安。
我该如何做到这一点,或者做一些类似的事情,而不强迫研究科学家把他们的计算脚本分成一个合适的类结构(他们像使用最优秀的图形计算器一样使用Python,我希望继续让他们这样做),而不是使用上面提到的令人不快、不安全、甚至有点可怕的__dict__
和globals()
的操作?我坚信一定有更好的方法,但exec "from {0} import *".format("modname")
没有奏效,几次尝试将pickle加载注入到targetModule
引用中也没有成功,eval("commonUnpickle.load()", targetModule.__dict__, locals())
也不行。所有这些都因为找不到<module>
中的函数而导致Unpickle的AttributeError
错误。
有没有更好的方法呢?
4 个回答
要让一个模块被认定为已经加载,我觉得它必须在 sys.modules 这个地方,而不仅仅是它的内容被导入到你的全局或局部命名空间中。你可以尝试执行所有的内容,然后从一个人造的环境中获取结果。
env = {"fn": sys.argv[2]}
code = """\
import %s # maybe more
import cPickle
commonFile = open(fn, "rb")
commonUnpickle = cPickle.Unpickler(commonFile)
commonData = commonUnpickle.load()
doIt = commonUnpickle.load()
"""
exec code in env
return env["doIt"]
你的问题很长,而我喝了太多咖啡,没能耐心看完。不过,我觉得你想做的事情其实已经有很好的解决方案了。现在有一个叫做 parallel python
(也就是 pp
)的库的一个分支,它可以把函数和对象进行序列化,然后把它们发送到不同的服务器上,再把它们解压执行。这个分支在 pathos
包里,但你也可以在这里单独下载:
http://danse.cacr.caltech.edu/packages/dev_danse_us
这里提到的“其他上下文”其实就是另一台服务器,而对象则是通过把它们转换成源代码再转换回对象的方式来传输的。
如果你想继续使用类似于现在的序列化方式,有一个 mpi4py
的扩展可以序列化参数和函数,并返回序列化后的返回值。这个包叫做 pyina
,通常用来把代码和对象发送到集群节点,配合集群调度器使用。
无论是 pathos
还是 pyina
,它们都提供了 map
和 pipe
的抽象,试图把并行计算的所有细节隐藏起来,这样科学家们只需要学习如何编写普通的串行 Python 代码就可以了。他们只需使用 map
或 pipe
函数,就能实现并行或分布式计算。
哦,我差点忘了。dill
序列化器包含了 dump_session
和 load_session
函数,允许用户轻松序列化整个解释器会话,并将其发送到另一台计算机(或者只是保存以备后用)。这在不同的上下文中非常方便。
你可以在这里获取 dill
、pathos
和 pyina
:https://github.com/uqfoundation
把函数进行序列化(也就是“腌制”)有时候会让人很头疼,特别是当你想把它们放到不同的环境中去用。如果这个函数没有引用它所在模块里的任何东西,或者只引用了一些肯定会被导入的模块,你可以看看在Python Cookbook上找到的一个基础数据库引擎的代码。
为了支持视图,这个学术模块在序列化查询时会从可调用对象中抓取代码。当需要反序列化视图时,会创建一个LambdaType实例,这个实例包含了代码对象和一个引用,指向一个包含所有已导入模块的命名空间。这个解决方案有一些局限性,但在练习中效果还不错。
视图的示例
class _View:
def __init__(self, database, query, *name_changes):
"Initializes _View instance with details of saved query."
self.__database = database
self.__query = query
self.__name_changes = name_changes
def __getstate__(self):
"Returns everything needed to pickle _View instance."
return self.__database, self.__query.__code__, self.__name_changes
def __setstate__(self, state):
"Sets the state of the _View instance when unpickled."
database, query, name_changes = state
self.__database = database
self.__query = types.LambdaType(query, sys.modules)
self.__name_changes = name_changes
有时候,似乎需要对系统中可用的注册模块进行一些修改。例如,如果你需要引用第一个模块(__main__
),你可能需要创建一个新的模块,并把你可用的命名空间加载到这个新模块对象中。上面提到的同一个例子使用了以下技术。
模块的示例
def test_northwind():
"Loads and runs some test on the sample Northwind database."
import os, imp
# Patch the module namespace to recognize this file.
name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
module = imp.new_module(name)
vars(module).update(globals())
sys.modules[name] = module