结论先行:__new__ 管"造对象",__init__ 管"初始化对象"。前者在实例诞生之前动手,负责返回一个实例;后者等实例造好了再往里填数据,返回 None。日常业务代码 99% 的初始化都该写在 __init__ 里,但有两类事情非 __new__ 不可------改造不可变类型的构造、控制实例的创建过程。这篇用 11 组实验把两者的分工边界一条条画出来。
1. 先看一组反常的实验结果
python
class Demo:
def __new__(cls):
print("__new__ 动手:造对象")
return super().__new__(cls)
def __init__(self):
print("__init__ 动手:填数据")
d = Demo()
输出顺序:
text
__new__ 动手:造对象
__init__ 动手:填数据
很多人以为 Demo() 是在"调用构造函数",但在 Python 里这一行其实是两步:解释器先调用 cls.__new__(cls) 拿到实例,再调用 instance.__init__()。造和填,是两套独立的钩子,中间隔着解释器的一个约定。
2. 职责对比表:一个管诞生,一个管初始化
| 维度 | __new__ |
__init__ |
|---|---|---|
| 方法类型 | 静态方法(隐式) | 实例方法 |
| 第一个参数 | cls(类本身) |
self(刚造好的实例) |
| 返回值 | 必须返回一个实例 | 必须返回 None |
| 调用时机 | 实例创建之前 | 实例创建之后 |
| 职责 | 对象的"诞生" | 对象的"初始化" |
两个细节值得单独验证。第一,__new__ 明明写成实例方法的模样,但它其实是静态方法 ------解释器会自动给它套上 staticmethod:
python
class Demo:
def __new__(cls):
return super().__new__(cls)
def __init__(self):
pass
print(type(Demo.__dict__['__new__'])) # <class 'staticmethod'>
print(type(Demo.__dict__['__init__'])) # <class 'function'>
第二,__init__ 敢返回别的值,解释器直接报错:
python
class Bad:
def __init__(self):
return 42
Bad() # TypeError: __init__() should return None, not 'int'
返回值约定是解释器强制的,不是风格问题。
3. object.new 到底干了什么
绝大多数重写里,super().__new__(cls) 这一行才是真正干体力活的。object.__new__ 只做一件事:在堆上分配内存,写好对象头(类型指针、引用计数),把骨架交出来。 它不接收你的业务参数------x、y 这些是给 __init__ 的,不是给它的。
python
class Point:
def __new__(cls, *args, **kwargs):
print("造骨架:", cls.__name__)
return super().__new__(cls) # 只收 cls,业务参数一概不碰
def __init__(self, x, y):
print("填数据:", x, y)
self.x = x
self.y = y
p = Point(1, 2)
print(p.x, p.y)
text
造骨架:Point
填数据:1 2
1 2
所以 __new__ 签名里出现 *args 时别懵:那些参数最终会流向 __init__,__new__ 只是过客,除非你有意拦截。
4. 不可变类型:为什么非 new 不可
int、str、tuple 这类不可变类型,实例的值在 __new__ 阶段就被冻结了 ,等 __init__ 跑的时候,木已成舟。想改造构造过程,只能动 __new__。
python
class HexStr(str):
def __new__(cls, s):
return super().__new__(cls, s.upper()) # 在这里动刀
h = HexStr("hello")
print(h) # HELLO
print(type(h)) # <class '__main__.HexStr'>
换成 __init__ 试试?白费:
python
class BadStr(str):
def __init__(self, s):
self.s = s.upper() # 只能加个属性,字符串本体纹丝不动
b = BadStr("hello")
print(b) # hello ------ 还是小写
str 本体的字符数据在创建时就定了,__init__ 里再怎么折腾也改不了。这就是"不可变类型的构造逻辑必须写在 __new__"的根本原因。
顺带一个标准库同款思路:pathlib.PurePath.__new__ 会判断当前是 Windows 还是 POSIX,直接返回对应子类的实例------用 __new__ 做工厂,连子类都替你选好了。
5. 最大的坑:new 返回了别家的实例
解释器有个很少人知道的约定:如果 __new__ 返回的不是 cls 的实例,__init__ 根本不会被调用。
python
class Real:
def __init__(self):
self.tag = "real"
class Fake:
def __new__(cls):
return Real() # 返回别家的实例
def __init__(self):
print("Fake.__init__ 永远不会打印")
f = Fake()
print(type(f)) # <class '__main__.Real'>
print(f.tag) # real
Fake() 造出来的却是个 Real,Fake.__init__ 静默跳过,isinstance(f, Fake) 还是 False。这类"工厂式 __new__"写起来很爽,调试起来很痛------你会看到一半的初始化逻辑神秘消失。 记住:返回本类实例,__init__ 照常跑;返回外类实例,__init__ 直接下班。
6. 单例的两种写法对比
用 __new__ 拦截实例化是最常见的单例写法,但它有个隐藏副作用:__new__ 被拦截了,__init__ 可没有被拦截。
python
class NaiveSingleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, x):
self.x = x
a = NaiveSingleton(1)
b = NaiveSingleton(2)
print(a is b) # True:实例确实只有一个
print(a.x, b.x) # 2 2:__init__ 跑了两次,状态被后一次覆盖
a is b 是 True,但 x 被第二次构造冲掉了。 想堵住,得在 __init__ 里加一道岗哨:
python
class Singleton:
_instance = None
_ready = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, x):
if self._ready:
return # 老实例,直接放行
self._ready = True
self.x = x
a = Singleton(1)
b = Singleton(2)
print(a is b) # True
print(a.x, b.x) # 1 1:状态保住了
另一种思路是上元类,把拦截点从"实例化"挪到"类调用",单例逻辑和业务代码彻底解耦:
python
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Service(metaclass=SingletonMeta):
def __init__(self, cfg):
self.cfg = cfg
s1 = Service("a")
s2 = Service("b")
print(s1 is s2) # True
print(s1.cfg) # a:__init__ 只跑了一次
追求解耦用元类,图省事用加了岗哨的 __new__------但千万别用不带岗哨的那版。
7. 工程里什么时候真需要 new
翻一遍标准库,__new__ 每次出场都有明确理由:
- 改造构造输入 :
IntEnum的成员本质是 int,值走的就是int.__new__;自定义int/str子类同理。 - 工厂选型 :
PurePath按操作系统返回不同子类的实例。 - 实例缓存 / 单例:框架里控制"全局唯一"的经典位置。
- C 扩展的桥接 :
numpy.ndarray这类需要在构造期分配大块内存的类型。
python
from enum import IntEnum
class Color(IntEnum):
RED = 1
GREEN = 2
print(Color.RED + 1) # 2:IntEnum 成员就是 int,值走 int.__new__
print(type(Color.RED)) # <enum 'Color'>
反过来,如果你的需求只是"存几个属性""校验几个参数",__init__ 就够了 ------上来就重写 __new__ 的代码,十有八九是用力过猛。
8. 一句话总结
__new__ 是出厂流水线,负责把对象造出来;__init__ 是精装修,负责给对象填数据。判据很简单:要改"怎么造",动 __new__;要改"造好了放什么",动 __init__。
下一篇聊 super():它找的到底是不是父类?为什么菱形继承下结果出乎意料。
系列文章:Python 底层设计逻辑|实验数据均为 CPython 3.10.0 / 3.13.14 实测,可复现。