当 运行 多处理 Python 代码时,cProfile 导致 pickling 错误

cProfile causes pickling error when running multiprocessing Python code

我有一个 Python 脚本,当我 运行 它正常时 运行 很好:

$ python script.py <options>

我正在尝试使用 cProfile 模块分析代码:

$ python -m cProfile -o script.prof script.py <options>

当我启动上述命令时,出现无法 pickle 函数的错误:

Traceback (most recent call last):
  File "scripts/process_grid.py", line 1500, in <module>
    _compute_write_index(kwrgs)
  File "scripts/process_grid.py", line 626, in _compute_write_index
    args,
  File "scripts/process_grid.py", line 1034, in _parallel_process
    pool.map(_apply_along_axis_palmers, chunk_params)
  File "/home/james/miniconda3/envs/climate/lib/python3.6/multiprocessing/pool.py", line 266, in map
    return self._map_async(func, iterable, mapstar, chunksize).get()
  File "/home/james/miniconda3/envs/climate/lib/python3.6/multiprocessing/pool.py", line 644, in get
    raise self._value
  File "/home/james/miniconda3/envs/climate/lib/python3.6/multiprocessing/pool.py", line 424, in _handle_tasks
    put(task)
  File "/home/james/miniconda3/envs/climate/lib/python3.6/multiprocessing/connection.py", line 206, in send
    self._send_bytes(_ForkingPickler.dumps(obj))
  File "/home/james/miniconda3/envs/climate/lib/python3.6/multiprocessing/reduction.py", line 51, in dumps
    cls(buf, protocol).dump(obj)
_pickle.PicklingError: Can't pickle <function _apply_along_axis_palmers at 0x7fe05a540b70>: attribute lookup _apply_along_axis_palmers on __main__ failed

代码使用多处理,我假设这是酸洗发生的地方。

游戏中的代码是here on GitHub

本质上,我在进程池中映射一个函数和一个相应的参数字典:

pool.map(_apply_along_axis_palmers, chunk_params)

据我所知,函数 _apply_along_axis_palmers 是 "picklable",因为它是在模块的顶层定义的。同样,当 运行 在 cProfile 上下文之外时不会发生此错误,所以这可能会为 pickling 添加额外的约束?

任何人都可以评论为什么会发生这种情况,and/or我该如何解决这个问题?

你在这里遇到的问题是,通过使用 -mcProfile,模块 __main__cProfile(代码的实际入口点),而不是你的脚本。 cProfile 尝试 解决这个问题,方法是确保当您的脚本 运行 时,它将 __name__ 视为 "__main__",因此它知道它是作为脚本 运行,不作为模块导入,但 sys.modules['__main__'] 仍然是 cProfile 模块。

问题是,pickle 处理 pickling 函数只是通过 pickle 其限定名称(加上一些样板,首先说明它是一个函数)。并且为了确保它能在往返过程中存活下来,它总是仔细检查是否可以在 sys.modules 中查找合格名称。因此,当您执行 pickle.dumps(_apply_along_axis_palmers)(在这种情况下通过将其作为映射函数传递来显式或隐式)时,在您的主脚本中定义了 _apply_along_axis_palmers 时,它会双重检查 sys.modules['__main__']._apply_along_axis_palmers 是否存在。但它不存在,因为 cProfile._apply_along_axis_palmers 不存在。

我不知道好的解决方案。我能想到的最好办法是手动修复 sys.modules 以使其正确公开您的模块及其内容。我还没有完全测试过,所以可能会有一些怪癖,但我找到的解决方案是更改名为 mymodule.py 的模块,格式为:

# imports...
# function/class/global defs...

if __name__ == '__main__':
    main()  # Or series of statements

至:

# imports...
import sys
# function/class/global defs...

if __name__ == '__main__':
    import cProfile
    # if check avoids hackery when not profiling
    # Optional; hackery *seems* to work fine even when not profiling, it's just wasteful
    if sys.modules['__main__'].__file__ == cProfile.__file__:
        import mymodule  # Imports you again (does *not* use cache or execute as __main__)
        globals().update(vars(mymodule))  # Replaces current contents with newly imported stuff
        sys.modules['__main__'] = mymodule  # Ensures pickle lookups on __main__ find matching version
    main()  # Or series of statements

从那以后,sys.modules['__main__'] 指的是您自己的模块,而不是 cProfile,所以事情似乎可行。尽管如此,cProfile 似乎仍然有效,并且酸洗会按预期找到您的功能。只有真正的成本是重新导入你的模块,但如果你做了足够多的实际工作,重新导入的成本应该相当小。