bigywater.tech
Django · 105

Signal 为什么可能重复注册

「保存一次,信号触发两次」是 Django 里常见的坑。这篇不去猜,直接复现能让它发生的几种情形,再用 dispatch_uid 解决。

函数身份
connect 去重按函数对象(或 dispatch_uid),不按代码内容
同文件两个模块名
导入两次 → @receiver 注册两次
dispatch_uid
相同 uid 只保留一个 receiver

去重的依据

实测 · 本机执行输出
同一个 h 连接两次,save 一次触发: 1
再连接两个「工厂造出来的相同代码」的函数,共 3 个 receiver 里有效的: 3 次触发(1 个 h + 2 个新函数)

同一个文件被导入两次

实测 · 本机执行输出
第一次(lab.handlers)导入后 receiver 数: 1
用另一个名字(handlers_again)再导入同一个文件后 receiver 数: 2
保存一次:lab.handlers.COUNT = 1  handlers_again.COUNT = 1

实验里用 spec_from_file_location 加载同一个文件、模块名不同来模拟。至于真实项目里是哪种导入路径不一致造成的,本文没有复现,只模拟了「同一文件、两个模块名」。

用 dispatch_uid

实测 · 本机执行输出
同一段代码执行 3 次(模拟重复导入),dispatch_uid 相同 → 保存一次触发: 1

lambda 与局部函数

实测 · 本机执行输出
在「ready()」里 connect 一个 lambda,连续执行 3 次 → receiver 增加了 3 个(每个 lambda 都是新对象)

只要 connect 的调用会执行多次(测试里的 setUp、被重复调用的 ready()),传入的又是每次都新建的函数,receiver 就会累加。

分步演示

重复注册
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite
    • 没有验证:runserver 自动重载的两个进程、INSTALLED_APPS 里同时写 app 和 AppConfig 路径是否导致重复(Django 有检查会直接报错,本文没有测)、测试里的重复连接
    京ICP备2026067502号-1京公网安备11010502063487号