Python 的 __init__ 到底是不是构造函数?拆开 __new__ 先造对象、那次被跳过的初始化和必须返回 None 的规矩

简介: __init__ 不是构造函数,真正造对象的是 __new__。本文用本机 CPython 3.13.14 实测:__new__ 返回非本类实例时 __init__ 被整个跳过、__init__ 返回非 None 直接 TypeError、object.__new__ 多传参数报错、不可变类型只能在 __new__ 里改值(MyInt(5)=105),附 type.__call__ 三步链与五条排错清单

写 Python 类的人几乎人人写过 def __init__(self, ...),教材里也常顺口叫它「构造函数」。但真要问「对象是谁造出来的」,答案不是它。

一句话结论:__init__ 不是造对象的人,它是「拿到一个已经造好的对象,往里填初始状态」的人。真正负责分配实例的是 __new__;把两者串起来的是 type.__call__,中间还有一道 isinstance 检查——__new__ 返回的要不是本类(或子类)实例,__init__ 会被整个跳过。

下面全部数据来自本机 CPython 3.13.14 实测。

一、先看调用顺序和那个 id

class A:
    def __new__(cls, *a, **kw):
        print('  A.__new__  cls=', cls.__name__, 'args=', a)
        return super().__new__(cls)
    def __init__(self, x):
        print('  A.__init__ x=', x, 'id(self)=', id(self))
        self.x = x

a = A(7)

实测输出:

  A.__new__  cls= A args= (7,)
  A.__init__ x= 7 id(self)= 2207591220624
  a.x = 7 | type(a) = A

两点值得记:

  • __new__ 先拿到全部参数,包括后面要传给 __init__ 的那份(这里 args=(7,))。它不是无参的。
  • __init__ 里的 self 不是新造的,就是 __new__ 返回的那个对象。

再做一个更直白的验证:

class D:
    def __new__(cls):
        o = super().__new__(cls)
        print('  D.__new__  created id=', id(o))
        return o
    def __init__(self):
        print('  D.__init__ received id=', id(self))
D()
  D.__new__  created id= 2207591220960
  D.__init__ received id= 2207591220960

两个 id 完全一样。所以 __init__ 的签名里写 self 而不是 return obj,是因为对象早就在那儿了,它只负责改。

二、__new__ 返回别的东西,__init__ 直接不跑

这是整套机制里最反直觉、也最能说明「谁才是造物主」的一条:

class B:
    def __new__(cls, *a, **kw):
        print('  B.__new__ -> returns an int (not a B)')
        return 42
    def __init__(self, x):
        print('  !! B.__init__ SHOULD NEVER RUN')

b = B(9)

实测:

  B.__new__ -> returns an int (not a B)
  b = 42 | type(b) = int | __init__ called? NO

B(9) 得到了一个整数,而不是 B 的实例,__init__ 一次都没执行。

原因是 type.__call__ 在调完 __new__ 之后会做一次检查:只有返回值是当前类(或子类)的实例,才继续调 __init__。返回个 int,这一步直接短路,把 42 原样交回调用方。

这个行为不是 bug,是被明确利用的能力——工厂函数、缓存命中返回已有对象、单例、按参数返回不同子类,全靠它。

三、__init__ 的返回值被硬性限制

class C:
    def __init__(self):
        return 1
C()
TypeError: __init__() should return None, not 'int'

__init__ 只能返回 None(不写 return 就是 None)。因为调用方要的是 __new__ 造好的那个对象,不需要 __init__ 再交一个回来。这条限制反过来又印证了它的定位:它是修改器,不是构造器。

顺带一提,__new__ 没有这个限制——它必须返回点什么,否则 type.__call__ 拿不到对象,__init__ 也就无从谈起。

四、object.__new__ 的参数规矩

object.__new__(A, 1)
TypeError: object.__new__() takes exactly one argument (the type to instantiate)

object.__new__ 只接受一个 cls。多传参数为什么报错?因为 CPython 有个保守判断:如果你重写了 __new__ 又重写了 __init__,多余参数会被放行;如果只重写了其中一个,多传参数就被认为「大概传错了」而报错。自己写 __new__(cls, *a, **kw) 时就不会撞这条。

五、真正需要 __new__ 的场景:不可变类型

普通类里改属性随时可以,但 int、str、tuple 这些不可变类型的值在实例诞生那一刻就定了,__init__ 里再改已经晚了。想给它们做子类并改值,只能在 __new__ 里动手:

class MyInt(int):
    def __new__(cls, v):
        return super().__new__(cls, v + 100)

print(MyInt(5))

实测输出:

  MyInt(5) = 105

5 进 __new__ 变成 105 后直接由 int.__new__ 造出实例,__init__ 全程没有插手余地。这是 __new__ / __init__ 分工最典型的用途,也是「教材把 __init__ 叫构造函数」最容易误导人的地方——对不可变类型,它连改都改不了。

六、把 type.__call__ 的链子摊开

A(7) 发生的事,用伪代码大约是这样:

def type_call(cls, *args, **kwargs):
    obj = cls.__new__(cls, *args, **kwargs)      # 1. 分配 / 造对象
    if isinstance(obj, cls):                     # 2. 检查返回值类型
        cls.__init__(obj, *args, **kwargs)       # 3. 填初始状态(必须返回 None)
    return obj

三个位置对应三种常见扩展点:

想干什么 该动哪里
控制实例怎么被创建、返回什么类型、做缓存/单例 __new__
给已创建的实例填属性 __init__
拦截整个调用过程(含参数预处理) 元类的 __call__
子类被定义时自动登记 __init_subclass__

顺带一个容易混的点:__init_subclass__ 是在类被定义时触发的,跟实例创建无关;别把它和 __init__ 归成一类。

七、几个实用写法

单例(靠 __new__ 缓存):

class OnlyOne:
    _inst = None
    def __new__(cls):
        if cls._inst is None:
            cls._inst = super().__new__(cls)
        return cls._inst

按参数返回不同子类(工厂):

class Shape:
    def __new__(cls, kind):
        if kind == 'circle': return super().__new__(Circle)
        return super().__new__(Square)

注意这类写法里 __init__ 会被跳过或重复调用,得自己把状态初始化放对地方,别指望它自动兜底。

八、排错清单(实测踩过的五条)

  1. __init__ 里 return 了别的东西 → TypeError: __init__() should return None。把值赋给 self.xxx,别 return。
  2. 重写 __new__ 忘了 return → 得到 None,后续 isinstance 检查失败,__init__ 不跑,调用方拿到 None。
  3. __new__ 返回了别的类型,__init__ 神秘不执行 → 不是 bug,是第 2 步的 isinstance 检查短路了。
  4. object.__new__(cls, extra) 报「takes exactly one argument」 → 用 def __new__(cls, *a, **kw) 自己接住参数。
  5. 给 int/tuple 子类在 __init__ 里改值无效 → 值在 __new__ 阶段已固定,改到 __new__ 里去。

九、小结

把这句话记住就够了:__new__ 造对象、__init__ 填状态、type.__call__ 串场,中间那道 isinstance 决定 __init__ 有没有资格跑。

所以下次有人问「Python 的构造函数是哪个」,比较准确的回答是:造对象的是 __new__;__init__ 是初始化器。C++ / Java 里那个「构造即分配」的直觉,在 Python 这里被拆成了两步,多出来的一步恰好就是很多高级写法(单例、工厂、不可变子类、元类)的入口。


运行环境:本机 CPython 3.13.14。本文所有输出均由 d53-probe.py 实测取得,未引用外部基准数据。

相关文章
|
8天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
3732 37
|
8天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
3298 10
|
3天前
|
云安全 人工智能 安全
|
9天前
|
iOS开发 Docker MacOS
【2026实测】Docker Desktop下载、安装、汉化、配置教程(附官网安装包)
Docker Desktop 是 Docker 官方推出的桌面容器管理工具,集成引擎、CLI、图形界面与 Kubernetes,支持 Win/macOS/Linux。2026 最新版 4.88.1 强化 ARM 原生运行、镜像加速及资源可视化,附中文汉化与国内镜像配置指南。
1702 6
|
9天前
|
安全 算法 定位技术
《高性价比人生指南》完整原版:普通人用最少的资源,过好这一生的底层逻辑~How To Live Better
📖《高性价比人生指南》是一份基于实证、拒绝焦虑的生存手册,倡导以“人生时薪”为视角,重新计算时间、金钱与精力的成本与收益。它不承诺逆袭,只教人算清账:省下的钱若耗掉时间便亏;买体验比买物品更值;睡眠是最高杠杆的健康投资;关系贵在情绪价值而非表面热络;学习重在改变行为而非自我安慰。核心理念:用低成本、高回报的微小行动,换取长期的选择权——不是成为谁,而是掌控自己人生的主动权。
2036 4
|
云安全 安全 网络安全
|
22天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
3913 55
|
14天前
|
人工智能 JSON Linux
【保姆级】ComfyUI部署教程(下载+安装+配置+工作流,2026最新)
ComfyUI 是一款开源节点式AI生成工具,支持图像、视频、音频及3D内容创作。通过拖拽连线组合模型加载、提示词、采样等节点,构建可复用、易修改的工作流,兼容SD1.5、SDXL、Flux、Qwen Image等主流模型。