Python 中 __new__ 和 __init__ 到底谁才是构造函数?一文讲透区别与 5 个坑

先给结论:__new__ 才是真正的构造函数,它负责"造出"对象;__init__ 只是初始化函数,它拿到的是已经造好的对象,往里填属性。

很多人写了几年 Python,一直把 __init__ 当成构造函数。这在多数场景下不影响写代码,但一旦遇到不可变类型、单例模式、对象缓存,就会撞上一些怎么调都不对的诡异问题。这篇文章把两者的执行顺序、分工和 5 个高频坑一次讲清楚。

一、一句话区分

python 复制代码
class Demo:
    def __new__(cls, *args, **kwargs):
        print("1. __new__ 造对象", cls)
        return super().__new__(cls)

    def __init__(self, x):
        print("2. __init__ 填属性", x)
        self.x = x

d = Demo(10)

输出顺序永远是:

markdown 复制代码
1. __new__ 造对象 <class '__main__.Demo'>
2. __init__ 填属性 10

关键点:

  • __new__ 是隐式静态方法 (Python 会自动把它当成 staticmethod,尽管你没加装饰器),第一个参数是 cls,必须返回一个实例;
  • __init__ 的第一个参数是 self,也就是 __new__ 返回的那个对象,不允许返回任何值 (返回非 None 会直接 TypeError)。

二、完整调用链:真正调度它们的是 type.__call__

你写 Demo(10) 的时候,实际发生的是:

python 复制代码
Demo(10)
  └── type.__call__(Demo, 10)          # 元类在调度
        ├── obj = Demo.__new__(Demo, 10)   # 第一步:造
        └── if isinstance(obj, Demo):     # 第二步:判断
                obj.__init__(10)          # 是自家实例才初始化
        return obj

注意那个 isinstance 判断------这是理解很多"为什么我的 __init__ 没执行"的关键。

三、坑 1:__new__ 忘了 return,__init__ 静默不执行

python 复制代码
class Bad:
    def __new__(cls, *args, **kwargs):
        print("new")          # 忘了 return

    def __init__(self):
        print("init")         # 永远不会执行

b = Bad()
print(b)                      # None

__new__ 没写 return,默认返回 None。None 不是 Bad 的实例,于是 __init__ 被直接跳过,不报错,安静地返回一个 None。这类 bug 排查起来非常费时间,因为没有任何异常抛出。

四、坑 2:给 object.__new__ 多传参数会炸

python 复制代码
class A:
    def __new__(cls, x):
        return super().__new__(cls, x)   # TypeError!

    def __init__(self, x):
        self.x = x

A(1)

报错类似:TypeError: object.__new__() takes exactly one argument (the type to instantiate)。

正确写法:只把 cls 传下去。

python 复制代码
def __new__(cls, x):
    return super().__new__(cls)          # 只传 cls

参数留给 __init__ 去接,各司其职。

五、坑 3:不可变类型必须用 __new__,__init__ 改不动

这是 __new__ 最刚需的场景。一旦对象已经创建完成,str / int / tuple 这类不可变对象就不能再改 ,__init__ 拿到的 self 已经是成品,塞不进任何东西。

python 复制代码
class PositiveInt(int):
    def __new__(cls, value):
        if value <= 0:
            raise ValueError("必须为正整数")
        return super().__new__(cls, value)

n = PositiveInt(5)
print(n, type(n), n + 10)     # 5 <class '__main__.PositiveInt'> 15
PositiveInt(-1)               # ValueError: 必须为正整数

同样的思路还能做出"带单位的字符串""自动去重的元组",都属于"在造的时候动手脚"的场景。

六、坑 4:单例写法能跑,但别当银弹

python 复制代码
class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, name):
        self.name = name      # 注意:每次实例化都会重新赋值

a, b = Singleton("A"), Singleton("B")
print(a is b)      # True
print(a.name)      # B ------ 被第二次调用覆盖了

三个必须知道的副作用:

  1. __init__ 每次都会跑 ,属性会被后来者覆盖(要避免就得加 hasattr 判断或用 __init__ 幂等设计);
  2. 上面这段代码不是线程安全的,并发下可能造出两个实例(需要加锁或用模块级单例);
  3. 单例会被子类共享 (cls._instance 挂在父类上),子类和父类可能拿到同一个对象。

日常项目里,模块级对象、依赖注入容器通常比手写单例更省心。

七、坑 5:__new__ 返回别的类型,__init__ 直接被跳过

python 复制代码
class Shape:
    def __new__(cls, kind):
        if kind == "circle":
            return Circle()
        return super().__new__(cls)

    def __init__(self, kind):
        print("Shape.__init__")

class Circle:
    pass

s = Shape("circle")     # 不会打印 Shape.__init__
print(type(s))          # <class '__main__.Circle'>

因为返回值不是 Shape 的实例,__init__ 被跳过。这其实是工厂模式 的实现基础:__new__ 可以决定"到底造出什么",这是 __init__ 永远做不到的。

八、什么时候该用 __new__

按经验排序:

场景 用哪个 说明
普通业务对象、加属性 __init__ 99% 的情况
继承 int / str / tuple 并加约束 __new__ 不可变,只能在造的时候动手
单例、对象池、缓存复用 __new__ 控制"是否新建"
工厂:返回不同类型 __new__ 控制"造什么"
元类编程 __call__ / __new__ 更靠底层一层

判断标准就一句:你要动的是"对象怎么被造出来",还是"造完之后填什么"。 前者用 __new__,后者用 __init__。

九、小结

  • __new__(静态、返回实例)= 构造函数,管"从无到有";
  • __init__(实例方法、不能返回值)= 初始化函数,管"从有到全";
  • 只有 __new__ 返回的是当前类的实例时,__init__ 才会被调用;
  • 不可变类型、单例、工厂模式才需要动 __new__,其余一律用 __init__。

你在项目里真的用过 __new__ 吗?是做单例、做不可变类型,还是被某个"忘了 return"的 bug 坑过?评论区聊聊你踩的那个坑。

相关推荐
言乐61 小时前
Python实现关税自动计算模型
开发语言·python·django·virtualenv·pygame
伞伞悦读1 小时前
从零打通 Hermes Agent:Windows + WSL2 + Clash 全链路安装、认证与首次实战
python·语言模型
用户847172102841 小时前
.shot 文件格式规范
前端·后端
happylifetree1 小时前
Python17:核心语法-数据存储与运算-输入与输出
python
梅雅达编程笔记1 小时前
04-Python CSV数据保存与翻页抓取
开发语言·爬虫·python·pandas·数据采集·csv
估值探索者2 小时前
【Python量化策略实战 #04】Donchian 通道假突破太多?用 ATR 阈值过滤跑通真实突破信号
开发语言·python·接口·api接口·数据api接口·股票数据api接口
wang_shu_mo_ran2 小时前
Spring IoC和DI概念篇
java·后端·spring
oooost2 小时前
Jupyter Notebook 常用快捷键速查
ide·python·jupyter
Cheney Pan2 小时前
第4篇 Spring Boot 应用监控:Micrometer 与 JVM 指标
jvm·spring boot·后端·prometheus