编号:09 | 报错类型:TypeError / PicklingError | 专栏:Python 生产环境报错速查 | 难度:中级
TL;DR
你用 unittest.mock.patch 给一个函数打了 Mock,然后这个函数通过 multiprocessing.Pool 在子进程中执行——结果程序炸了,抛出一个完全看不出根因的 TypeError: cannot pickle 'Mock' object。
根因:multiprocessing 默认用 pickle 把参数序列化后传给子进程,而 unittest.mock.Mock 对象不可 pickle。本文给三种修复方案,按推荐度排列。
1. 现象
from unittest.mock import Mock, patch
from multiprocessing import Pool
def worker(db):
return db.query("SELECT 1")
with patch("__main__.Database") as mock_db:
with Pool(2) as pool:
pool.map(worker, [mock_db, mock_db])
报错:
TypeError: cannot pickle 'unittest.mock.Mock' object
或者更隐蔽的变体:
_pickle.PicklingError: args[0] from __newobj__ args has the wrong class
后一条是 CPython 已知 bug,从 2012 年挂到现在(cpython#100090,前身 #58782)。
2. 为什么 Mock 不能 pickle?
pickle 的工作原理是通过 __reduce__ 或 __getstate__ 把对象转成字节流。Mock 对象内部持有复杂的动态属性(_mock_methods、_mock_children、_spec_class 等),这些属性无法被 pickle 安全地序列化和还原。
CPython 核心开发者 2012 年就明确表示:这是预期行为,不会改。 Mock 对象的设计目标是在单进程测试中使用,不是跨进程传输。
3. 三种修复方案
方案 A(推荐):用 spawn 替代 fork
import multiprocessing as mp
mp.set_start_method("spawn", force=True)
fork 模式下,子进程继承了父进程的内存空间——包括无法 pickle 的对象。spawn 模式启动全新的 Python 解释器,不会再尝试 pickle 父进程中的 Mock。
代价:启动子进程稍慢(约 0.1-0.3 秒),对于测试完全可接受。
注意:spawn 模式下,子进程需要重新 import 所有模块——如果 Mock 依赖模块级状态,需要用 initializer 参数传递。
方案 B:用 pathos.multiprocessing
from pathos.multiprocessing import ProcessingPool as Pool
with Pool(2) as pool:
pool.map(worker, [mock_db, mock_db])
pathos 使用 dill 序列化库替代 pickle,dill 能序列化 Mock 对象。安装:pip install pathos。
代价:引入额外依赖。dill 比 pickle 慢约 2-3 倍,对于测试中传递对象数量不多时影响可忽略。
方案 C:手写 __reduce__
如果你不想引入新依赖、也不想改启动方式,可以给 Mock 实例打补丁:
from unittest.mock import Mock
def _mock_reduce(self):
return (Mock, ())
Mock.__reduce__ = _mock_reduce
这会让 pickle 认为 Mock 是一个无状态的空对象,允许序列化。但这只适合不需要保留 Mock 内部状态(如 return_value、side_effect)的场景——你需要在子进程中重新设置这些属性。
4. 自检清单
| 检查项 | 命令/方法 |
|---|---|
| 当前启动方式 | mp.get_start_method() |
| Mock 能否 pickle | pickle.dumps(your_mock) |
| spawn 是否生效 | 加 mp.current_process().name 日志 |
| --- | |
| ## 5. 生产环境与测试环境的差异 | |
| 有一个容易忽略的细节:同一个代码在 Docker 里能跑,在本机就跑不了。 | |
| 这是因为: | |
- Docker 内 Linux 默认 fork,但 fork 对 Mock 的要求较低(同一进程空间的子进程可以访问 Mock 的原始引用) |
|
- macOS 从 Python 3.8 起默认 spawn,子进程全新启动,必须 pickle 所有参数 |
|
所以如果你和同事在不同 OS 上开发,务必统一 mp.set_start_method("spawn")。 |
|
| --- | |
| ## 6. 启示 | |
这个报错之所以坑,在于它不发生在代码逻辑层,而是发生在 Python 进程模型的底层。 你能看到 Mock 对象在父进程里好好的,pool.map 一行就炸,错误信息还不提 Mock。 |
|
| 排查这类问题的通用思路: | |
| 1. 谁在用 pickle? → multiprocessing、concurrent.futures、某些 RPC 框架 | |
| 2. 什么对象不能序列化? → Mock、lambda、generator、thread lock、socket、数据库连接 | |
| 3. 能不能绕过序列化? → 换进程启动方式 / 换序列化库 / 重构参数传递 | |
| --- | |
| 出处:cpython#100090(11 年未修复) | Stack Overflow: Can't pickle function |
| 下一篇预告:#10 UnicodeDecodeError —— 当你的文件编码不是 UTF-8 |
本文由 admin 原创,转载请注明出处。
评论
0