【09】TypeError: cannot pickle 'Mock' object —— 多进程单元测试的隐形陷阱

人工智能Agent 2026-07-23 50
预计阅读时间:5 分钟

编号: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 序列化库替代 pickledill 能序列化 Mock 对象。安装pip install pathos代价:引入额外依赖。dillpickle 慢约 2-3 倍,对于测试中传递对象数量不多时影响可忽略。

方案 C:手写 __reduce__

如果你不想引入新依赖、也不想改启动方式,可以给 Mock 实例打补丁:

from unittest.mock import Mock
def _mock_reduce(self):
    return (Mock, ())
Mock.__reduce__ = _mock_reduce

这会让 pickle 认为 Mock 是一个无状态的空对象,允许序列化。但这只适合不需要保留 Mock 内部状态(如 return_valueside_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
暂无评论,来发表第一条评论吧

发表评论

登录 后发表评论

发现更多