有没有一种简单的方法来pickle一个Python函数(或者以其他方式序列化它的代码)?

140

我正在尝试通过网络连接(使用asyncore)传输一个函数。有没有一种简单的方法序列化python函数(在这种情况下,不会产生副作用),以便像这样传输?

理想情况下,我希望有一对类似于这些函数:

def transmit(func):
    obj = pickle.dumps(func)
    [send obj across the network]

def receive():
    [receive obj from the network]
    func = pickle.loads(s)
    func()

这将比所有REST的序列化和API类更酷。 - Kermit
12个回答

147

您可以序列化函数的字节码,然后在调用者处重新构建它。可以使用marshal模块对代码对象进行序列化,然后将其重新组合成函数。例如:

import marshal
def foo(x): return x*x
code_string = marshal.dumps(foo.__code__)

然后在远程进程中(传输 code_string 后):

import marshal, types

code = marshal.loads(code_string)
func = types.FunctionType(code, globals(), "some_func_name")

func(10)  # gives 100

需要注意几点:

  • marshal格式(包括任何Python字节码)可能在不同的Python主要版本之间不兼容。

  • 仅适用于CPython实现。

  • 如果函数引用了全局变量(包括导入的模块、其他函数等),您需要将它们序列化,或者在远程端重新创建它们。例如,我的示例只是给它提供了远程进程的全局命名空间。

  • 您可能需要做更多工作来支持更复杂的情况,比如闭包或生成器函数。


3
谢谢。这正是我所需要的。根据一些初步测试,它对于生成器的使用是有效的。 - Michael Fairley
2
如果您阅读了有关marshal模块的前几段,您会发现它强烈建议改用pickle?同样适用于pickle页面。http://docs.python.org/2/library/marshal.html - dgorissen
1
我正在尝试使用marshal模块对一个字典进行序列化,该字典被初始化为defaultdict(lambda: defaultdict(int))。但是它返回错误ValueError: unmarshallable object。请注意,我正在使用Python2.7。有什么想法吗?谢谢。 - user17375
1
@mgoldwasser:是的,但pickle不支持对代码对象进行序列化,这正是OP所要求的。 - Brian
4
在Python 3.5.3中,foo.func_code会引发AttributeError错误。是否有其他方法可以获取函数代码? - AlQuemist
显示剩余7条评论

60

看看Dill吧,它扩展了Python的pickle库以支持更多类型,包括函数:

>>> import dill as pickle
>>> def f(x): return x + 1
...
>>> g = pickle.dumps(f)
>>> f(1)
2
>>> pickle.loads(g)(1)
2

它还支持对函数闭包中的对象的引用:

>>> def plusTwo(x): return f(f(x))
...
>>> pickle.loads(pickle.dumps(plusTwo))(1)
3

2
dill 还可以很好地从函数和 lambda 中获取源代码并将其保存到磁盘,如果您更喜欢这种方式而不是对象 pickling。 - Mike McKerns
2
只需要导入就能直接使用,而且还是一种即插即用的解决方案,不需要修改pickle周围的任何其他代码。 - ego
它还会在函数内保存全局变量! - Princy
这应该成为新的被接受的答案吗? - xappppp

13

最简单的方法可能是使用 inspect.getsource(object)(参见inspect 模块),它可以返回一个字符串,其中包含函数或方法的源代码。


这看起来不错,除了函数名在代码中被明确定义,这有点棘手。我可以剥离掉代码的第一行,但这可以通过像“def /n func():”这样的操作来破坏。我可以将函数的名称与函数本身一起封装,但我无法保证名称不会冲突,或者我必须将函数放在包装器中,这仍然不是最干净的解决方案,但这可能是必须要做的。 - Michael Fairley
1
请注意,inspect模块实际上只是在询问函数的定义位置,然后从源代码文件中读取这些行 - 这并不算高级。 - too much php
1
你可以通过它的.__name__属性查找函数名。你可以在^def\s*{name}\s*上进行正则表达式替换,并给它任何你喜欢的名称。虽然不是万无一失,但对大多数情况都管用。 - too much php

13

我需要在这个特定的项目中坚持使用标准库。 - Michael Fairley
24
但这并不意味着你不能查看Pyro的代码,以了解它是如何完成的 :) - Aaron Digulla
6
@AaronDigulla- 确实如此,但值得一提的是,在阅读他人已发布的代码之前,您应始终检查软件的许可证。在未引用来源或遵守许可证/复制限制的情况下,阅读他人的代码并重用其中的想法可能被视为抄袭和/或侵犯版权的行为。 - mdscruggs

9
这完全取决于您是否在运行时生成函数:
如果是 - 对于动态生成的函数,inspect.getsource(object) 不起作用,因为它从 .py 文件获取对象的源代码,所以只能检索到执行前定义的函数源代码。
如果您的函数已经放置在文件中,为什么不将接收者访问权限赋予它们,并仅传递模块和函数名称。
我能想到的唯一解决动态创建函数的方法是在传输之前将函数构造为字符串,传输源代码,然后在接收端使用eval()来执行它。
编辑:marshal 解决方案看起来也很聪明,我不知道您可以序列化除内置对象以外的其他内容。

6

在现代Python中,您可以对函数和许多变量进行pickle处理。考虑以下内容:

import pickle, time
def foobar(a,b):
    print("%r %r"%(a,b))

你可以将其进行序列化处理

p = pickle.dumps(foobar)
q = pickle.loads(p)
q(2,3)

你可以使用 pickle 序列化闭包

import functools
foobar_closed = functools.partial(foobar,'locked')
p = pickle.dumps(foobar_closed)
q = pickle.loads(p)
q(2)

即使闭包使用局部变量。
def closer():
    z = time.time()
    return functools.partial(foobar,z)
p = pickle.dumps(closer())
q = pickle.loads(p)
q(2)

但如果使用内部函数关闭它,将会失败。

def builder():
    z = 'internal'
    def mypartial(b):
        return foobar(z,b)
    return mypartial
p = pickle.dumps(builder())
q = pickle.loads(p)
q(2)

带有错误

pickle.PicklingError:无法对 <function mypartial at 0x7f3b6c885a50> 进行序列化:它没有被发现为 __ main __.mypartial

在 Python 2.7 和 3.6 中测试通过。


5
请注意,pickling并不会序列化所有的代码。它只是序列化对函数的引用。这意味着只有在将来函数存在时才能运行它。想象一下这样一个情况:你想在某个时间点重放代码(虽然你可能不会这么做),但是如果该函数不再存在或者不存在于相同的形式中,你就无法存储pickled函数并在移动到的代码库中调用它。 - Trent
绝对正确;但是OP明确要求“腌制”。 - undefined

5

3
code_string = '''
def foo(x):
    return x * 2
def bar(x):
    return x ** 2
'''
# 将代码字符串序列化为二进制数据 obj = pickle.dumps(code_string)

现在

# 反序列化并执行代码
exec(pickle.loads(obj))
foo(1) > 2
bar(3) > 9

3

Cloudpickle 可能是你正在寻找的东西。 Cloudpickle 的描述如下:

cloudpickle 特别适用于集群计算,其中 Python 代码通过网络发送到远程主机执行,可能靠近数据。

使用示例:

def add_one(n):
  return n + 1

pickled_function = cloudpickle.dumps(add_one)
pickle.loads(pickled_function)(42)

2
你可以这样做:
def fn_generator():
    def fn(x, y):
        return x + y
    return fn

现在,transmit(fn_generator())会发送fn(x,y)的实际定义而不是模块名称的引用。
您可以使用相同的技巧来在网络上发送类。

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