Python 的 __new__ 到底管什么?和 __init__ 的分工一次讲透Python后端python知识整理日新计划

结论先行:__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 实测,可复现。

相关推荐
青少儿编程课堂2 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会C/C++考级:并查集连通块与朋友圈问题解析
c++·python·算法·bfs·信息学竞赛
外收内放2 小时前
数据分析(数据排序)
python·学习·数据分析
Pniubi2 小时前
agent处理封号怎么解决误封问题
开发语言·python
小羊没烦恼!2 小时前
浅谈SQL Server中的事务日志(二)----事务日志在修改数据时的角色
前端·数据库·后端·python·web安全·oracle
ss.li2 小时前
rppg阅读笔记 2026.10.15
图像处理·python·计算机视觉
大貔貅喝啤酒3 小时前
Unittest框架使用教程
自动化测试·python
一个简单的TTS开发者3 小时前
中文TTS工具有哪些-8款主流方案横向对比
人工智能·后端·python·indextts
yunfanleo3 小时前
temperature 详解:大模型采样参数原理、三种用法与工程实践
人工智能·python·rpa
CarIise3 小时前
Maven依赖传递与聚合工程
python·r语言·django