Python对象创建揭秘:__new__与__init__的区别、原理与典型应用场景
2026/9/11 10:56:48 网站建设 项目流程

揭开 Python 对象创建的真相:init只是"装修",new才是"盖房"

我先说一个很多人一开始都会搞错的事实:Python 里真正的构造函数其实是__new__,而不是你天天在写的__init__。这句话不是抠字眼,而是理解整个 Python 对象模型的关键。

很多教材和教程习惯把__init__叫作"构造函数"或"初始化方法",导致大量 Python 开发者以为obj = SomeClass()这个过程就是从__init__开始的。实际上,当解释器执行SomeClass()这句话时,它先干的第一件事是调用SomeClass.__new__,由__new__去分配内存、创建并返回一个"空壳"实例,然后解释器才会拿着这个"空壳"去调用__init__,往里面填充属性。也就是说,__init__充其量是装修队,__new__才是真正把毛坯房盖起来的人。

这不仅仅是概念上的区别。我见过不少 Python 开发者因为在__new____init__的理解上出了偏差,写出来的代码在单例、缓存、不可变类型继承、元类协作等场景下拿到的对象完全不是自己想要的。这篇文章我打算从一个实践者的角度,把这两个方法从调用顺序、职责边界、典型应用、再到各种坑和排查思路全部讲透。无论你是刚入门 Python 的初学者,还是写了几年 Python 想深入理解对象模型的进阶者,这篇内容应该都能让你少走一些弯路。

1. 一个实验看清真相:newinit的执行顺序

在讲任何理论之前,我建议你先跑一个最简单的实验。眼见为实,这句话放在程序世界里尤其成立。

class Demo: def __new__(cls, *args, **kwargs): print("1. __new__ 被调用") print(f" cls = {cls}") instance = super().__new__(cls) print(f" 创建的实例: {instance}") return instance def __init__(self, *args, **kwargs): print("2. __init__ 被调用") print(f" self = {self}") demo = Demo() print(f"最终得到对象: {demo}")

这段代码的输出结果非常直观:

1. __new__ 被调用 cls = <class '__main__.Demo'> 创建的实例: <__main__.Demo object at 0x7f...> 2. __init__ 被调用 self = <__main__.Demo object at 0x7f...> 最终得到对象: <__main__.Demo object at 0x7f...>

可以看到,__new__先执行,__init__后执行,而且__init__接收到的self,就是__new__返回的那同一个实例对象。这个顺序是 Python 语言层面规定死的,谁也没法改变。

1.1 传参逻辑:参数是怎么在两者之间流转的

当你写下Demo(1, 2, x=3)的时候,参数并不是直接丢给__init__的。Python 解释器真正的执行链路是这样的:

  1. 把类Demo和所有参数(1, 2, x=3)一起交给type.__call__
  2. type.__call__内部先调用Demo.__new__(Demo, 1, 2, x=3)
  3. 如果__new__返回的实例是Demo类(或其子类)的实例,再调用instance.__init__(1, 2, x=3)

所以请注意,__new____init__接收到的除了第一位的cls/self不同之外,后面的位置参数和关键字参数是完全相同的,这就是为什么很多自定义__new__的方法签名会写成def __new__(cls, *args, **kwargs),它必须用*args, **kwargs来接住所有参数,再决定怎么用。

这里就埋着一个非常常见的 bug:如果你在__new__里消化掉部分参数,但没有在返回值上做任何处理,__init__依然会收到完整的一组参数,很容易报出__init__() takes 2 positional arguments but 3 were given之类的错误。换句话说,你想在__new__里"截胡"参数,就得连__init__的签名一起改,或者干脆用*args, **kwargs统一兜底,否则两边很容易对不上。

1.2 一个反直觉现象:init可能被完全跳过

不卖关子,直接说结论:new返回的不是当前类的实例时,init不会被调用

打个比方,你叫了个装修队去给新房装修,结果施工队进错小区、装的是别人家的房子,那你自然不会为这个房子买单。Python 的逻辑也类似:__init__是为"属于当前类的实例"做初始化的,如果__new__返回了一个别的类的对象,那__init__就被跳过了。

看这个例子:

class Foo: pass class Bar: def __new__(cls): return Foo() # 返回了另一个类的实例 def __init__(self): print("这里不会执行") b = Bar() print(type(b)) # <class '__main__.Foo'> print(isinstance(b, Bar)) # False

控制台不会打印这里不会执行,因为type.__call__发现Bar.__new__返回的不是Bar的实例,于是直接放弃调用Bar.__init__。这种写法在日常业务中不常见,但在某些元编程场景、代理模式、动态创建对象的框架里确实会用到。

需要特别说明的是:__init__是否执行,唯一判断标准就是new返回的实例是否为当前类的实例。如果__new__返回的是同一个类的旧实例(比如缓存复用),那__init__依然会被调用。这一点在实现单例和对象池时特别容易踩坑,后面我会专门展开。

2. 两人的本质区别:分配者 vs 初始化者

如果你只记住一句话,那就是:new负责创建对象,init负责初始化对象。但在实际工程里,这两个角色的边界远比字面上复杂。

2.1new的职责范围

__new__是 Python 对象创建链路中的第一环,它的核心职责是:

  • 接收类对象cls(注意不是实例self)。
  • 决定要不要创建一个新实例,或者直接返回一个已有的实例。
  • 如果要创建新实例,通常调用super().__new__(cls)来真正分配内存。
  • 返回一个对象,这个对象不一定是cls的实例。

从底层实现角度看,super().__new__(cls)至少要做这些事:分配内存块、设置对象的类型指针、初始化引用计数等。这是一套非常底层的操作,在 Python 层我们基本不应该去碰它,也不应该自己手动去"模拟"对象分配。

2.2init的职责范围

__init____new__之后被调用,它的任务是给实例填充属性、检查参数合法性、建立对象的不变式(invariant)。也就是说,它做的事情是把一个"空壳"变成一个"有意义的对象"。

举个例子,定义一个User类:

class User: def __init__(self, name, age): if not isinstance(name, str): raise TypeError("name 必须是字符串") if age < 0: raise ValueError("age 不能为负数") self.name = name self.age = age

这里的__init__做了参数校验和属性赋值。而__new__完全没有参与这些逻辑。你甚至可以完全不写__new__,Python 默认会调用object.__new__帮你把空壳准备好,然后紧接着调用你的__init__

2.3 关键差异一览表

维度newinit
第一个参数cls(类对象)self(实例对象)
返回值必须返回一个对象(通常为 cls 实例)必须返回 None(返回其他值会触发 TypeError)
调用时机创建实例的第一步创建实例的第二步
默认实现object.new分配内存object.init什么都不做
主要用途控制对象创建、实现单例、不可变类型、元编程初始化属性、校验参数
能否手动调用可以,cls.__new__(cls)一般不手动调用
方法类型本质是特殊的静态方法(隐式传 cls)实例方法

这个表格里有两个容易忽略的细节:

第一,__new__在 Python 3 中其实是一个"特殊的静态方法"。它不像普通类方法那样强制绑定cls,但当你写Demo()时,解释器会把类传进去作为第一个参数。如果你在类体里用__new__这个名字定义方法,它会被 Python 特殊处理成静态方法。

第二,__init__被要求必须返回None,如果它返回了非None值,Python 3 会直接报错:TypeError: __init__() should return None, not 'xxx'。我当年第一次踩到这个错误时非常困惑,后来才明白这是语言层面的强制约定。

2.4 一个类比:盖房子和装修房子

如果非要打比方,__new__就像施工队把毛坯房盖好,交付给你一把钥匙;__init__就像你拿到钥匙之后,往房子里买家具、刷墙、装水电。有些场景下你不需要装修(可以没有__init__),但你不能没有毛坯房;而大多数情况下,你会同时需要这两个环节。

不过这个类比有一个不太准确的地方:__new____init__在语言层面是一对固定搭档。你写不写__new__,Python 都会走一遍object.__new__给你创建实例;写不写__init__,Python 也会调用object.__init__(只是它什么都不做)。所以更准确地说,它们是"默认存在、可按需覆盖"的两个钩子(hook)。

3. 什么时候必须碰new:四个典型应用场景

很多人学完这两个方法后最大的困惑是:那我到底什么时候需要自己写__new__?说实话,大多数日常业务代码根本不需要碰__new__。但如果你遇到下面这四类情况,__new__就是你的救命稻草。

3.1 单例模式与资源复用

最经典的应用就是单例模式。比如数据库连接池、日志句柄、配置管理器这类对象,整个程序只需要一份,重复创建既浪费资源又可能产生状态不一致。

class ConfigManager: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance

在我实际的项目里,这个模式有一个很重要的变体:线程安全问题。Python 的__new__在多线程并发下并不是原子操作,如果两个线程同时进入if cls._instance is None,可能各自执行super().__new__(cls),结果创建了两个不同的实例。这在高并发服务里属于致命 bug,排查起来又比较隐蔽。正确的做法是用threading.Lock包一层,或者干脆用模块级变量来实现单例,这个我后面会给出完整模板。

3.2 不可变类型的子类

如果你继承的是intstrtuplefrozenset这些不可变类型,想在创建时做额外的定制,那就必须在__new__里动手,因为对象一旦创建完成就无法修改了。

举个实际例子,假设你要实现一个"只能存储偶数的元组":

class EvenTuple(tuple): def __new__(cls, iterable): evens = [x for x in iterable if x % 2 == 0] return super().__new__(cls, evens) et = EvenTuple([1, 2, 3, 4, 5, 6]) print(et) # (2, 4, 6)

注意这里不能__init__里做这个过滤,因为tuple是不可变类型,走到__init__阶段时内容已经没法改变了。同理,自定义带单位的时间对象、带精度的Decimal子类、限制取值范围的int子类,逻辑都应该写在__new__里。

3.3 自定义元类协作,或者阻止实例化

元类(metaclass)控制的是"类的创建",而__new__控制的是"实例的创建"。当你和元类配合时,__new__常常是关键切入点。

还有一种场景:你想定义一个"工具类",它只提供静态方法,不允许被实例化。可以直接用__new__把这个口子掐死:

class Utils: def __new__(cls): raise TypeError("Utils 是纯静态工具类,不能实例化") @staticmethod def add(a, b): return a + b

这样任何Utils()的调用都会抛出 TypeError。虽然用模块级函数也能实现类似效果,但有些团队在 API 设计上希望有类作为命名空间,这个技巧就会很实用。

3.4 实现"返回已有对象"的工厂逻辑

__new__返回的可以是已经存在的对象,这就让"创建接口"变成了"获取接口"。比如缓存相同参数的实例:

class Circle: _cache = {} def __new__(cls, radius): if radius not in cls._cache: instance = super().__new__(cls) cls._cache[radius] = instance return cls._cache[radius] def __init__(self, radius): self.radius = radius

这样相同半径的Circle就是同一个对象,省去了重复分配内存的开销。类似的思路也可以用于数值运算的常量复用、枚举对象、Flyweight 模式等。

4. 实战长文:两个坑与完整排查链路

这一节我想完整还原一次我实际遇到的排查过程,因为它几乎把__new____init__相关的几个经典坑全部串起来了,对理解这两个方法非常有帮助。

4.1 背景:缓存对象为什么"初始化"了一百次

有一次我在做一个资源管理模块,要求对相同asset_id只创建一个资源对象,后续重复请求直接复用。我最初的实现是这样的:

class Asset: _cache = {} def __new__(cls, asset_id): if asset_id not in cls._cache: obj = super().__new__(cls) cls._cache[asset_id] = obj return cls._cache[asset_id] def __init__(self, asset_id): print(f"初始化 {asset_id}") self.asset_id = asset_id self.loaded = False

结果测试的时候我发现,即使同一个asset_id被请求了一百次,控制台也打了一百次初始化 xxx。我当时第一反应是缓存代码写坏了,但检查了半天发现_cache确实在复用对象,id(cache_obj)也相同。这就奇怪了:对象是同一个,为什么__init__还会反复执行?

后来我才反应过来:对象复用不等于跳过inittype.__call__的规则是,只要__new__返回的实例是当前类的实例,就会调用__init__。它才不管你这个实例是不是新建的呢。换句话说,缓存复用的是"同一个对象",但 Python 层面依然要求你重新走一遍初始化过程。

4.2 定位问题:用 print 和 type() 还原调用链条

排查的时候,我在__new____init__第一行分别加了print,并在缓存命中的分支里打印了id(obj)

class Asset: _cache = {} def __new__(cls, asset_id): if asset_id not in cls._cache: obj = super().__new__(cls) print(f"新建对象 id={id(obj)}") cls._cache[asset_id] = obj else: print(f"复用对象 id={id(cls._cache[asset_id])}") return cls._cache[asset_id] def __init__(self, asset_id): print(f"__init__ 被调用 asset_id={asset_id}") self.asset_id = asset_id self.loaded = False

输出结果清楚地告诉我:复用对象这一行打出来了,但紧接着__init__ 被调用也照样打出来了。这就锁定了问题根源——不是缓存逻辑错了,而是我对__new____init__的协作规则理解不完整。

4.3 解决方案:用标志位控制"只初始化一次"

既然 Python 的规则是"只要返回当前类实例就调用init",那我们就自己加一个标志位,让__init__内部感知到"这个对象已经初始化过了"。

class Asset: _cache = {} def __new__(cls, asset_id): if asset_id not in cls._cache: obj = super().__new__(cls) obj._initialized = False cls._cache[asset_id] = obj return cls._cache[asset_id] def __init__(self, asset_id): if getattr(self, "_initialized", False): print(f"跳过 {asset_id} 的重复初始化") return self.asset_id = asset_id self.loaded = False self._initialized = True print(f"完成 {asset_id} 的初始化") a1 = Asset("robot1") a2 = Asset("robot1") print(a1 is a2) # True

这次输出就符合预期了:第一次打印完成 robot1 的初始化,第二次打印跳过 robot1 的重复初始化

这个例子给我的教训是:new决定"你是哪个对象",init决定"这个对象的状态是什么"。当你复用同一个对象时,状态可能已经存在了,因此要不要重新赋值,是一个业务设计问题,而不仅仅是语言机制问题。如果你设计的是不可变快照类,那确实每次复用都要重新赋值;如果你设计的是资源池、连接池,那通常希望只初始化一次。这两种诉求,都得靠自己在__init__里控制。

4.4 后续坑:把"跳过初始化"写复杂了

解决了标志位问题后,我一度想让_initialized的检查逻辑更通用,于是写了一个带参数的判断函数,结果反而把代码搞复杂了。后来我才意识到,这个场景其实用 Python 内置的__new__配合一个类变量就够了,甚至更简单:

class Asset: _cache = {} _initialized_assets = set() def __new__(cls, asset_id): if asset_id not in cls._cache: obj = super().__new__(cls) cls._cache[asset_id] = obj return cls._cache[asset_id] def __init__(self, asset_id): if asset_id in self._initialized_assets: return self._initialized_assets.add(asset_id) self.asset_id = asset_id self.loaded = False

_initialized_assets集合来记录哪些asset_id已经初始化过,比给每个实例塞一个_initialized标志位更清晰,也更容易排查。当然这两种方式都行,关键是理解背后的逻辑,而不是机械地抄代码。

5. 其他容易踩的坑与排查速查表

除了上面那个完整的排查链路,还有几个容易踩的坑值得单独交代。

5.1 坑一:重写了new但忘了 return

这是新手最容易犯的错误。你写了__new__,结果忘了return,函数会隐式返回None,然后 Python 发现__new__没有返回实例,就不会调用__init__,最后你的变量拿到的是None,调用任何方法都会 AttributeError。

class Broken: def __new__(cls): pass # 忘了 return super().__new__(cls) b = Broken() print(b) # None

排查方法很简单:用type(b)看一眼类型,如果显示NoneType,十有八九就是这个问题。

5.2 坑二:init返回了非 None 值

前面说过,__init__如果 return 了非 None 对象,Python 3 直接报错:

class Bad: def __init__(self): return 42 Bad() # TypeError: __init__() should return None, not 'int'

有些场景下你想在__init__里提前退出,写一句裸return是完全合法的(返回 None)。但如果手滑写成了return None之外的值,就炸了。记住:在__init__里想提前中断,直接裸return即可。

5.3 坑三:多继承下 super().init的参数混乱

这个问题常出现在多继承 + 覆盖__init__的场景。super().__init__()会沿着 MRO(方法解析顺序)走,如果某个父类的__init__不认识子类传进来的关键字参数,就会抛unexpected keyword argument

排查方法是打印 MRO:

class C(A, B): pass print(C.mro())

然后逐个确认每个类的__init__签名是否兼容。这种问题最容易发生在第三方库的基类上,你在子类里新加了一个参数,结果链路上某个父类__init__并不知道这个参数。

5.4 坑四:多线程下的单例被创建了多次

我在第 3 节说过,纯__new__实现的单例在多线程下并不安全。解决方式有两种:

第一种,加锁 + 双重检查:

import threading class Singleton: _instance = None _lock = threading.Lock() def __new__(cls): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance

这里用了双层检查(double-checked locking),能有效避免每次获取单例都加锁带来的性能损耗。第一次不加锁判断,是为了最快速路径;加锁后再次判断,是为了防止并发下重复创建。

第二种,利用模块导入的天然单例特性,这是我最推荐的方式:

# config.py class _Config: pass config = _Config()

其他模块from config import config拿到的永远是同一个对象,因为 Python 模块只会被导入一次。这个方案实现最简单,也不容易写错,大多数项目用它就够了。

5.5 坑五:和元类混用时,init被静默绕过

如果你自己实现了一个元类并覆盖了__call__,那么__init__是否被调用完全由你的__call__逻辑决定。默认的type.__call__会"new 之后自动 init",但你的自定义__call__不会帮你自动做这件事。

class Meta(type): def __call__(cls, *args, **kwargs): obj = cls.__new__(cls, *args, **kwargs) # 注意:这里没有调用 obj.__init__ return obj class Demo(metaclass=Meta): def __init__(self): print("Demo.__init__ 执行了") self.x = 1 d = Demo() print(hasattr(d, 'x')) # False

这种坑在写 ORM、RPC 框架时经常遇到,因为框架设计者会想重写__call__来控制实例创建流程,但很容易忘掉__init__这一环。

5.6 排查速查表

最后把常见现象、可能原因和解决思路整理成一张表,方便你遇到问题时快速检索。

现象可能原因排查/解决思路
创建对象后,变量是 None__new__没有返回实例检查__new__是否有return super().__new__(cls)
init没被调用__new__返回的不是当前类实例,或元类__call__被重写检查__new__返回类型;检查元类__call__是否调用了__init__
init被重复调用单例/缓存模式下的对象复用时重新初始化初始化逻辑加标志位;或用集合记录已初始化对象
multiprocessing/multi-thread 下实例不同并发创建没有加锁使用threading.Lock+ 双重检查,或模块级单例
unexpected keyword argument多继承下某个父类__init__不兼容打印MRO,逐一核对__init__签名
__init__() should return None__init__返回了非 None 值把返回值去掉,或改为裸return
继承 tuple/str 后无法修改内容不可变类型创建阶段就定死了__new__中处理数据后再传给super().__new__

6. 进阶认知:call在中间扮演什么角色

理解了__new____init__,建议再往上走一层,看看类实例化这个过程在解释器层面到底是怎么被调度的。这个进阶认知能帮你把前面的知识点串成一张完整的图。

6.1 类对象调用实例化的入口是call

当我们写obj = Foo()时,本质上是在调用Foo.__class__.__call__(Foo)。而Foo.__class__默认就是type,所以执行的是type.__call__。官方文档对这个方法的逻辑界定可以简化为:

def type___call__(cls, *args, **kwargs): obj = cls.__new__(cls, *args, **kwargs) if isinstance(obj, cls): obj.__init__(*args, **kwargs) return obj

这就是整条链路的"总控":先__new__,再判断是不是同类实例,是就__init__,最后返回。

理解了这一点,前面很多现象就有了解释:

  • 为什么__new__返回 None 时__init__不执行?因为isinstance(None, cls)是 False。
  • 为什么__new__返回其他类的实例时__init__不执行?还是因为isinstance判断不成立。
  • 为什么自定义元类重写__call__后,__init__可能被绕过?因为你亲手改变了总控逻辑。

6.2 不可变类型为什么只能在new里做手脚

intstrtuple这些内置不可变类型的设计目标是:对象一旦创建,内部状态不可变。在 CPython 的实现里,这些类型的数据是在__new__阶段就已经填入的,__init__阶段即使你想修改也没有对应的写入口。所以继承 tuple、str 并想在"创建时"修改数据,只能在__new__里做文章。

反过来,如果你继承的是可变类型如listdict,那__init____new__都可以往对象上塞属性,但通常建议把属性初始化都放__init__,因为它在对象建立完成后执行,语义上更清晰,也不容易出错。

6.3 用new实现条件初始化:什么时候该跳过、什么时候不该跳过

回到对象池、缓存这类场景,你应该考虑的不只是"能不能跳过init",而是"应不应该跳过init"。举例来说:

  • 如果你缓存的对象是不可变快照,复用旧实例时直接返回就行,重新初始化反而可能丢状态。
  • 如果你缓存的对象是可变资源(连接、会话),复用的时候可能希望重置状态,那__init__执行反而是好事,只是要小心重复初始化带来的副作用。

这个决策本质上是一个业务设计问题。语言机制只负责提供"可以跳过/不可以跳过"的规则和检查点,具体策略由你来定。

7. 实操总结:我看过最好的两个代码模板

写到这里,把核心要点再收拢一下,顺便分享两个我平时用得最多的模板,都是经过生产环境验证的。

7.1 元类完整版单例模板

比在__new__里直接加锁更彻底的方案是,在元类层面把__call__锁住:

import threading class SingletonMeta(type): _instances = {} _lock = threading.Lock() def __call__(cls, *args, **kwargs): if cls not in cls._instances: with cls._lock: if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls]

这个模板比直接在__new__里加锁更彻底,因为super().__call__会完整走一遍__new__ -> __init__,并且锁的范围覆盖整个创建过程,不会出现"锁了newinit里又出问题"的情况。

7.2 对象池缓存模板

如果不想在__init__里写一堆标志位判断,可以这样设计对象池:

class Asset: _cache = {} _initialized_ids = set() def __new__(cls, asset_id): if asset_id not in cls._cache: obj = super().__new__(cls) cls._cache[asset_id] = obj return cls._cache[asset_id] def __init__(self, asset_id): if asset_id in self._initialized_ids: return self._initialized_ids.add(asset_id) self.asset_id = asset_id self.loaded = False def load(self): if not self.loaded: print(f"从磁盘加载 {self.asset_id} ...") self.loaded = True return self

这种写法把"对象创建"和"对象状态初始化"拆得很干净:__new__只管返回那个唯一的对象,__init__只管在第一次见到这个asset_id时做初始化。代码的可读性和可排查性都比把标志位塞在实例属性里更好。

7.3 给新手和老手各说一句

对刚接触 Python 的人来说,你不需要急着用__new__,先把调用顺序和isinstance判断逻辑搞清楚就够用。对老手来说,碰到对象创建、缓存复用、元类协作的问题时,先想清楚三个问题再动手:__new__返回什么?__init__要不要执行?状态由谁维护?这三个问题的答案确定了,方案基本就出来了。

我自己最大的体会是:Python 的面向对象机制并不是"想要什么就有什么",而是"有一套默认行为,你可以在特定节点插入自己的逻辑"。__new____init__就是这套默认行为留给我们最核心的两个插入点。你把这套规则摸透了,很多看起来玄学的 Python 行为,其实一条print就能看穿。

提示:如果你在调试时发现对象创建行为很诡异,先在__new____init__第一行加print看调用顺序。这个办法虽然土,但比对着抽象文档猜要高效得多,十次有九次能在两分钟内定位问题。

最后一个我在实际项目里反复验证过的技巧:如果你只是想要单例/缓存,优先考虑模块级变量,而不是__new__。模块级单例实现起来只要一行代码,零锁、零状态,而且天然线程安全。__new__这套机制真正不可替代的场景,是继承不可变类型、与元类协作、以及"根据参数返回不同对象"这种动态工厂语义。想明白这一点,你对__new__的定位就不会跑偏了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询