一个团队要实现"配置单例",写了个 __init__ 里判断 if self._instance: return self._instance——结果"每次调用都拿到新对象"。因为 __new__ 已经"创建"了对象,而 __init__ 不返回值,它"返回 None",这个"什么都没做"很典型:__init__ 的返回值被解释器忽略,而"写 return 的人"以为"自己在控制创建"——**于是代码"看起来对、行为全错".
于是"return 一个实例"这件事"什么都没做".
Python 的对象创建分两步:__new__ 造出对象、__init__ 初始化它。而"控制'有没有对象'"只能改 __new__;"控制'对象长什么样'"才是 __init__.
1. 两步的职责
| 方法 | 职责 | 返回值 |
|---|---|---|
__new__ |
分配/返回实例 | 实例 |
__init__ |
初始化属性 | 必须是 None |
第二项是"最容易被违反"的:**__init__ 返回非 None 会抛 TypeError**第二项的"报错"长这样:TypeError: __init__() should return None, not 'Foo'——而它"只在"你"真的返回了值"时出现;"返回 None 但没做任何事"是"最沉默"的那种.
——而"想让它'返回旧实例'"这种意图,就会"变成异常"或"静默无效".
"__new__ 单例"要"防'重复初始化'",否则"每次取都重置":
class Config:
_inst = None
def __new__(cls, *a, **kw):
if cls._inst is None: cls._inst = super().__new__(cls)
return cls._inst
def __init__(self): # 注意:它"每次都会跑"
if getattr(self, "_ready", False): return
self._ready = True; self.data = load()
// 判据:单例的"初始化"必须"幂等/只跑一次"
2. "单例"的三种写法
| 写法 | 说明 |
|---|---|
__new__ 里缓存实例 |
经典(要"注意线程") |
| 模块级实例(推荐) | 最简单(Python 模块天然单例) |
| 元类/装饰器 | 复杂(多用于"框架") |
第二项的"额外好处"是"可测":"测试里可以 monkeypatch 那个模块变量"——而"元类单例"往往"没法替换",于是"测试互相污染".
第二项是"Python 风格"的:"一个模块里放一个实例"**——**而 import 机制保证"只执行一次".
"不可变"在"现代 Python"里有更省事的写法,先试它,再考虑 __new__:
from dataclasses import dataclass
@dataclass(frozen=True)
class Money: # 冻结:赋值会抛 FrozenInstanceError
cents: int
// 校验放 __post_init__(此时字段已赋值)
// 判据:能用 frozen dataclass 就别手写 __new__
3. "不可变对象"为什么要用 __new__
| 类型 | 场景 |
|---|---|
tuple/str/int 子类 |
值在"创建时固定" |
namedtuple |
同上 |
| 冻结 dataclass | 用 __post_init__ + object.__setattr__ |
第一项的"原因"是"顺序":__init__ 在"对象已存在"之后才跑第一项的"具体失败"是"AttributeError: can't set attribute"**(当"父类已冻结"时)**——**而"想用 init` 给不可变对象赋值"的人,会在"第一行"就撞上它.
——**而"不可变"要求"值在'能被看到'之前就设好".
"__init__ 是否被调用"有一条清晰规则,值得记成一句:
// 规则:__new__ 返回"本类(或其子类)实例" → 调 __init__
// 返回"其它类型"或"根本不返回实例" → 不调 __init__
1 返回 cls 实例 → __init__ 调用(单例因此"每次初始化")
2 返回 int/str 等 → 不调用(工厂函数的常见写法)
3 返回 None → 连"创建"都失败(调用方拿到 None)
// 判据:控制"是否初始化"= 控制"返回值类型"
4. "三种被跳过"的情形
| 情形 | 现象 |
|---|---|
__new__ 返回"已有实例" |
__init__ 仍会被调用(同类型时) |
__new__ 返回"别的类型实例" |
__init__ 不调用 |
反序列化(pickle/copy) |
可能"绕过 __init__" |
第一项的"后果"是"状态丢失":"get_instance() 拿到同一个对象,而它的'初始化'每次清空了缓存"——而"看起来像'缓存不生效'".
第一项是"经典坑":"单例 __init__ 每次都被跑一遍"——于是"每次取单例都重置了状态".
第二项是"判断依据":"返回的不是本类实例,就跳过 __init__".
"反序列化绕过 __init__"这件事,最容易被"安全校验"忽略:
import pickle
class Payload:
def __init__(self, raw): validate(raw); self.raw = raw
// 从 pickle 恢复时:不调用 __init__ → 校验被绕过
// 修法:自定义 __setstate__ 里再校验一次
def __setstate__(self, st): validate(st["raw"]); self.__dict__.update(st)
// 判据:任何"能绕过构造函数"的路径,都要"补校验"
5. "校验放哪"的三条判据
| 位置 | 适合 |
|---|---|
__new__ |
要在"创建前"拒绝 |
__init__ |
常规校验(对象已存在) |
__post_init__(dataclass) |
字段已赋值后校验 |
第一项在"缓存/池化"里有用:"参数非法就'不创建'(省内存)"第一项的"边界"是"可读性":"在 __new__ 里做业务校验"会让"类的入口有两个"——**于是"读代码的人"要"同时看两个方法",而"漏看一个"就会"误判".
——**而 __init__ 里抛异常时"对象已分配",只是"被丢弃".
"子类 __new__ 报错"在"多继承/带参数构造"时最常见,而修法只有一句:
class Base:
def __new__(cls, *args, **kwargs): # 必须"接收任意参数"再转交
return super().__new__(cls)
// 反例:def __new__(cls): ... # 子类传参 → TypeError
// 判据:__new__ 的签名"永远带 *args, **kwargs"
6. 排错:五种"创建阶段问题"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 单例"每次新" | 改的是 __init__ |
改 __new__ |
| 单例"状态被重置" | __init__ 每次都跑 |
加"已初始化"标记 |
| 不可变"改不动" | 用了 __init__ 赋值 |
用 __new__/__post_init__ |
pickle 后"校验没了" |
绕过 __init__ |
用 __getstate__/__setstate__ |
子类"父类 __new__ 报错" |
__new__ 参数不匹配 |
用 *args, **kwargs |
7. 边界:什么情况"别碰 __new__"
| 场景 | 为什么 | 做法 |
|---|---|---|
| 普通业务类 | 无"创建期"需求 | 只写 __init__ |
| 用 dataclass 就能解决 | 有现成机制 | 用 @dataclass(frozen=True) |
| 需要"单例" | 模块级实例更简单 | 模块级 |
| 依赖注入框架 | 容器管生命周期 | 跟随框架 |
最后一条与"框架"有关的边界:"ORM 的实体"通常"由框架'绕过 __init__'创建"**(如"从数据库行映射")——**于是"在 init 里做校验"的实体,"从库里读出来时"根本"没校验过";这类场景应把"校验"放到"post_init/显式 validate()". 判断口径一句话:**"我要控制的是'有没有对象',还是'对象长什么样'"**。前者(单例、池化、缓存、不可变)→ new;后者(属性、校验、注册)→ init——**而"在 init` 里 return 实例"这个写法,是"两件事搞混"的典型症状.
8. 自测
__new__与__init__的职责与返回值差别是什么?为什么__init__必须返回None?- 为什么"Python 风格的单例"是"模块级实例"?
import机制保证了什么? - "不可变对象"为什么必须在
__new__里赋值? __new__返回"已有实例"时__init__会怎样?返回"别的类型实例"时呢?- "校验放
__new__"适合什么场景?它与"__init__里抛异常"的差别是什么? - 判断"该改
__new__还是__init__"的那个问题是什么?
补一句"排查"技巧:"当'单例'行为不对时,第一件事是'打印两次 id()'"——"id 不同 → 问题在 __new__;id 相同但状态被重置 → 问题在 __init__"——这两步能"十秒定位",比"读三遍代码"快.
9. 一句话总结
__new__ 管"有没有",__init__ 管"是什么"——而"单例/池化/不可变"都属于"有没有"的范畴,所以"只能在 __new__ 里做"。而"在 __init__ 里 return 实例"这个写法,会"静默无效",因为**__init__ 的返回值被 Python 忽略(并要求为 None)**;想清楚这一点,这类 bug 就不再出现。