ClassName() 只是一步?拆开看 __new__ 和 __init__ 各自在干什么

「Python 进阶之路」系列 Day12

写在前面

ClassName(...) 这行代码看起来是"一步创建对象",实际上背后拆成了两个独立的阶段。上一篇讲类变量和实例变量,本质是"属性归谁"的问题;今天这篇往前退一步,讲清楚一个对象到底是怎么"被造出来"的------__new____init__ 各自负责什么,以及这个区分在单例模式、不可变类型子类化里为什么会变得很关键。


一、是什么:对象创建的两个阶段

  • __new__(cls, ...) :真正负责"创造"这个对象,返回一个实例。它的第一个参数是 cls(类本身),本质上是一个隐式的 staticmethod
  • __init__(self, ...) :负责对已经创建好的对象做初始化(设置属性)。第一个参数是 self,此时对象已经存在了
python 复制代码
class Foo:
    def __new__(cls, *args, **kwargs):
        print("1. __new__ 被调用,负责创建实例,cls =", cls)
        instance = super().__new__(cls)
        return instance
    def __init__(self, name):
        print("2. __init__ 被调用,负责初始化,self =", self)
        self.name = name

f = Foo("test")
# 1. __new__ 被调用,负责创建实例,cls = <class '__main__.Foo'>
# 2. __init__ 被调用,负责初始化,self = <__main__.Foo object at 0x...>

验证 __new__ 确实是隐式的 staticmethod

python 复制代码
print(type(Foo.__dict__["__new__"]))   # <class 'staticmethod'>

二、为什么要拆成两步:new 返回值决定一切

flowchart LR A[调用ClassName传参数] --> B[调用new创建对象<br/>cls作为第一个参数] B --> C{new返回的<br/>是不是该类实例} C -->|是| D[调用init初始化<br/>self就是new返回的对象] C -->|否| E[跳过init<br/>直接返回new的结果]

关键点:__new__ 如果不返回该类(或子类)的实例,__init__ 根本不会被调用

python 复制代码
class Bar:
    def __new__(cls, *args, **kwargs):
        print("Bar.__new__ 被调用")
        return "不是Bar的实例,是个字符串"   # 故意返回别的类型
    def __init__(self, *args, **kwargs):
        print("Bar.__init__ 被调用!")   # 这行根本不会执行

result = Bar("随便什么参数")
print(result, type(result))
# Bar.__new__ 被调用
# 不是Bar的实例 <class 'str'>

这解释了为什么要把"创建"和"初始化"拆成两个独立的阶段:__new__ 有权决定这次调用到底要不要真的产生一个新对象、产生哪个类的对象 ,而 __init__ 只是在"确实产生了一个本类实例"这个前提下才会被触发去做初始化。这个控制权正是单例模式、不可变类型子类化这些进阶用法的基础。


三、怎么用

1. 应用一:单例模式

通过重写 __new__,让类只在第一次调用时真正创建实例,之后每次调用都复用同一个:

python 复制代码
class Singleton:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            print("第一次创建,真正调用 super().__new__")
            cls._instance = super().__new__(cls)
        else:
            print("已经有实例了,直接复用")
        return cls._instance
    def __init__(self, value=None):
        print("__init__ 被调用,value =", value)
        if value is not None:
            self.value = value

s1 = Singleton("第一次的值")
s2 = Singleton("第二次的值")
print(s1 is s2)            # True ------ 确实是同一个实例
print(s1.value, s2.value)   # 第二次的值 第二次的值

容易被忽略的细节:即使 __new__ 复用了已有实例,__init__ 每次调用 Singleton(...) 时依然会被执行 ------上面例子里 s1.value 最终变成了"第二次的值",因为第二次调用时 __init__ 又把 value 覆盖了一遍。如果只想在真正创建实例的那一次执行初始化逻辑,需要在 __init__ 里自己加判断(比如检查一个标记位),__new__/__init__ 分离并不会自动帮你避免重复初始化。

2. 应用二:不可变类型子类化

继承 int/str/tuple 这类不可变类型时,值必须在 __new__ 阶段就确定 ,因为对象一旦被创建(__new__ 返回后),这个值就不可能再被修改,__init__ 阶段已经来不及了:

python 复制代码
class PositiveInt(int):
    def __new__(cls, value):
        if value <= 0:
            raise ValueError("必须是正数")
        return super().__new__(cls, value)   # int的值必须在创建时(__new__)就定好

p = PositiveInt(5)
print(p, type(p), p + 1)   # 5 <class '__main__.PositiveInt'> 6

PositiveInt(-1)
# ValueError: 必须是正数

用一个反例验证"__init__ 阶段已经无法修改不可变值":

python 复制代码
class TryModifyInInit(int):
    def __init__(self, value):
        super().__init__()
        print(f"__init__ 里 self 的值已经是 {int(self)},无法在这里再改变它")

t = TryModifyInInit(42)
print(t)   # 42 ------ 值早在 __new__ 阶段(也就是 int.__new__)就已经定死了

这就是为什么校验/初始化不可变类型的值,必须写在 __new__ 里,而不是像普通类那样习惯性地写在 __init__ 里。


四、面试追问

Q1:__new____init__ 的区别是什么?

__new__(cls, ...) 负责真正创建并返回一个实例,第一个参数是 cls,本质是一个隐式的 staticmethod__init__(self, ...) 负责对已经创建好的实例做初始化(设置属性),第一个参数是 self,不需要也不能有返回值。

Q2:对象实例化的完整过程是什么?

调用 ClassName(...) 时,先调用 __new__(cls, ...) 得到一个对象;如果这个对象确实是该类(或其子类)的实例,接下来才会调用它的 __init__(...) 做初始化;如果 __new__ 返回的不是本类实例,__init__ 根本不会被调用,ClassName(...) 的最终结果就是 __new__ 返回的那个对象。

Q3:单例模式怎么用 __new__ 实现?

用一个类变量(如 _instance)记录是否已经创建过实例,在 __new__ 里判断:如果还没创建过,就真正调用 super().__new__(cls) 创建并缓存下来;如果已经创建过,直接返回缓存的那个实例。需要注意的是,即使复用了已有实例,__init__ 在每次调用类名时依然会被执行,如果不想重复初始化,需要自己在 __init__ 里加判断。

Q4:为什么继承不可变类型时经常需要重写 __new__ 而不是 __init__

因为不可变类型(int/str/tuple 等)的值必须在对象创建的那一刻就确定下来,__new__ 返回之后这个对象的值就已经"定型"、无法再被修改,__init__ 阶段这个对象已经创建完毕,任何试图在 __init__ 里修改值的操作都不会生效,所以校验和构造逻辑必须写在 __new__ 里。

Q5:__new__ 不返回该类实例时会发生什么?

__init__ 根本不会被调用。ClassName(...) 这次调用的最终结果,就是 __new__ 返回的那个对象,它可以是任意类型(不一定是 ClassName 的实例),Python 不会强行去调用一个"类型不匹配"的对象的 __init__ 方法。


下一篇预告

Day13 讲多重继承与 MRO------当一个类同时继承多个父类时,方法调用顺序到底是怎么确定的,C3 线性化算法具体在算什么。

相关推荐
学长毕业设计44 分钟前
基于SpringBoot的公益基金管理系统(源码+文档+讲解视频)
java·spring boot·后端
东小西1 小时前
【SAA实战】第 3 篇 · 工具调用全攻略:把业务能力交给 Agent 自己调度
java·后端·spring
东小西1 小时前
【SAA实战】第 4 篇 · Agent 短期记忆:saver 让 Agent 跨轮记得住(threadId 隔离)
java·后端·spring
阿kun要赚马内2 小时前
MySQL 索引基础
后端·mysql
码事漫谈2 小时前
我啥都能做,却不知道做什么了
后端
geovindu2 小时前
CSharp: 万年历
开发语言·后端·c#·.net
傲笑风2 小时前
【openvino】tinybert基于openvino服务化部署(四)
人工智能·python·自然语言处理·nlp·bert·openvino
维基框架3 小时前
WIKI 知识库 v1.1.1 正式发布
人工智能·python
独泪了无痕4 小时前
SpringBoot Event事件机制,轻松实现业务解耦
spring boot·后端·spring
悲且狂4 小时前
SpringBoot项目改造注意事项(旧项目框架复用)
java·spring boot·后端