2021 字
约 6 分钟
17
S10-A26 · `__new__` 与 `__init__`:单例、不可变与"为什么我的校验被跳过"

一个团队要实现"配置单例",写了个 __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. 自测

  1. __new__ 与 __init__ 的职责与返回值差别是什么?为什么 __init__ 必须返回 None?
  2. 为什么"Python 风格的单例"是"模块级实例"?import 机制保证了什么?
  3. "不可变对象"为什么必须在 __new__ 里赋值?
  4. __new__ 返回"已有实例"时 __init__ 会怎样?返回"别的类型实例"时呢?
  5. "校验放 __new__"适合什么场景?它与"__init__ 里抛异常"的差别是什么?
  6. 判断"该改 __new__ 还是 __init__"的那个问题是什么?

补一句"排查"技巧:"当'单例'行为不对时,第一件事是'打印两次 id()'"——"id 不同 → 问题在 __new__;id 相同但状态被重置 → 问题在 __init__"——这两步能"十秒定位",比"读三遍代码"快.

9. 一句话总结

__new__ 管"有没有",__init__ 管"是什么"——而"单例/池化/不可变"都属于"有没有"的范畴,所以"只能在 __new__ 里做"。而"在 __init__ 里 return 实例"这个写法,会"静默无效",因为**__init__ 的返回值被 Python 忽略(并要求为 None)**;想清楚这一点,这类 bug 就不再出现。

S10-A26 · `__new__` 与 `__init__`:单例、不可变与"为什么我的校验被跳过"
https://www.clxhxhhr.top/posts/6709/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。