通过子进程解开大 python 对象

Unpickle big python object through subprocess

如何通过子进程传递和取消选中大对象。所以我下面的例子适用于小对象(字典),但如果它有大数据就停止工作:

这是我的工作示例:

return_pickle.py

import pickle
import io
import sys

NUMS = 10
    
sample_obj = {'a':1, 'b': [x for x in range(NUMS)]}
d = pickle.dumps(sample_obj)
sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='latin-1')
print(d.decode('latin-1'), end='', flush=True)

unpickle.py

import subprocess
import pickle

proc = subprocess.Popen(["python", "return_pickle.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

output, err = proc.communicate()
data = pickle.loads(output)
print(data)

所以上面的工作正常,但是如果我将 NUMS 更改为 100 它会出错并显示 _pickle.UnpicklingError: invalid load key, '\x0a'. 或者如果我将 sample_obj 更改为有一个列表字典,如果列表很大,我会得到同样的错误。我该如何解决这个问题?

我在 Python 3.7 和 Windows 10 机器上使用

如果您将 protocol=0 添加到您的 dumps() 调用中,它将起作用,但这非常令人费解。 Proto 0 是“文本模式”,在许多方面效率低下更高的 pickle 协议改进,但在 Windows 上它可以产生巨大的差异。

对象的大小并不重要。如果您只是将 NUMS 设置为 11,您的示例将失败。会发生什么情况:如果列表中的元素恰好是 10,pickle 会生成一个“操作码”,其中一个字节的值为 10。但是 chr(10) == '\n',在 Windows 上的文本模式输出中,实现说“哦,一个换行符!我必须将其更改为 carriage-return + 换行符”。

那么 是什么 原始 pickle 流中的单个 10 字节被损坏为 13 (\r) 字节后跟一个 10 (\n )字节。 13 最终被放入 unpickler 正在构建的列表中,然后剩下的 10 在上下文中完全没有意义。这就是“无效加载密钥,'\x0a'”消息的来源 - 0x0a == 10.

当然还有许多其他方法可以将值为 10 的字节 最终放入 pickle 流中,但是如果您以文本模式写入,它们都会在Windows.

有一些直接的 platform-independent 方法可以用二进制 pickle 做到这一点,比试图欺骗 stdout 使其成为它不想要的东西更容易。最简单:pickle.dump(obj, f) 在一端以二进制写入模式打开的文件,然后在另一端简单地 pickle.load(f) 为在另一端以二进制模式读取打开的同一文件。

给百合镀金 ;-)

受@flakes 的启发,这里有一种不同的方法来欺骗 stdout 使用二进制模式,但仅依赖于记录在案的可移植 API:

import os, sys, pickle
...
with os.fdopen(sys.stdout.fileno(), "wb", closefd=False) as stdout:
    pickle.dump(sample_obj, stdout)

使用匿名 OS-level 管道

为了显示可能的复杂性,这里使用 os.pipe() 的情况大致相同。这很烦人,因为 OS 管道末端在 Unix-y 系统上是“文件描述符”,但在 Windows 上实际上是“句柄”。所以你需要的代码取决于你使用的平台。这里我就迎合一下Windows

writepik.py,由 readpik.py:

调用
import os, pickle, msvcrt, sys

data = {"d": 1, "L": list(range(50000))}

h = int(sys.argv[1])
d = msvcrt.open_osfhandle(h, 0)
with os.fdopen(d, "wb") as dest:
    pickle.dump(data, dest)

所以它在命令行上传递了一个整数“句柄”,它必须将其更改为“文件描述符”,然后传递给 fdopen() 以创建一个足够长的文件对象来转储泡菜。

readpik.py:

import os, pickle, msvcrt, subprocess

r, w = os.pipe()
h = msvcrt.get_osfhandle(w)
os.set_handle_inheritable(h, True)
proc = subprocess.Popen(["py", "writepik.py", str(h)], close_fds=False)
os.close(w)
with os.fdopen(r, "rb") as src:
    data = pickle.load(src)
print(data)

所以这有点相反。 os.pipe() returns “文件描述符”,但是为了让子进程正确继承打开的 Windows 句柄,我们必须使句柄可继承,而不是文件描述符。所以我们通过 get_osfhandle(w) 获得足够长的数字“句柄”以将其标记为可继承并将其值插入 writepik.py.

的命令行

其实不难,但是舞蹈很细腻,很容易出错。

如果您不对结果进行字符串化,而是 post 直接将结果输出到标准输出缓冲区,那么我可以在 windows 机器上工作:

return_pickle.py

import pickle, sys

sample_obj = {'a':1, 'b': [x for x in range(100)]}
sys.stdout.buffer.write(pickle.dumps(sample_obj))
import subprocess, pickle

proc = subprocess.Popen(
    ["python", "return_pickle.py"],
    stdout=subprocess.PIPE,
    stderr=subprocess.DEVNULL,
)

output, _ = proc.communicate()
print(pickle.loads(output))