「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 返回值决定一切
关键点:__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 线性化算法具体在算什么。