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 坑过?评论区聊聊你踩的那个坑。

相关推荐
Mk598784 小时前
让 Agent 长出双手:一个注解搞定工具调用,AgentScope-Java @Tool 实战
后端
小溪学编程4 小时前
将 Bean 放入 Spring 容器中的方式有哪些?
java·后端·spring
Mk598784 小时前
一个 Markdown 文件就是一个员工 —— AgentScope-Java 多智能体编排实战(核心篇)
后端
imDwAaY4 小时前
微信运动每天晚上 10 点推送排行榜,技术上到底有多难?
后端
ServBay4 小时前
PHP 突爆 9.2 分高危漏洞,你升级了吗?
后端·php
学Linux的语莫5 小时前
python中类的常见写法
开发语言·python·状态模式
武子康5 小时前
撤回发生在重建途中,旧索引还能切回来吗
人工智能·后端·agent
码事漫谈5 小时前
双十一,我为什么想买一台编码显示器
后端
垆边人似月.5 小时前
Python中如何使用get函数实现频次查询
服务器·前端·python
AI 编程助手GPT6 小时前
GPT 脚本别只发一个文件:用 uv 把依赖和锁一起交付
人工智能·python·gpt·uv·依赖管理