news 2026/10/11 1:15:45

Python __new__ vs __init__:对象创建机制与实战场景深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python __new__ vs __init__:对象创建机制与实战场景深度解析

如果你是靠 Python 吃饭的人,迟早会撞上__new__。我用 Python 这些年,真正把__new__用明白是在读某些框架源码和写元类代码的时候——之前自以为很懂__init__,结果发现它根本不是创建对象的地方,只是“装修”。__new__才是那个真正“盖房子”的角色,而且它俩在对象生命周期里是一前一后的合作关系。这篇文章我不讲教科书里那些干巴巴的定义,我会直接从“为什么需要两个魔法函数”入手,把调用机制、参数传递、返回值规则拆开揉碎,再配合单例模式、不可变类型缓存、工厂模式几个实战场景,把__new__和__init__之间的关系彻底捋清楚。无论你是准备面试、想读懂开源项目,还是想写出更优雅的框架代码,这篇都值得你花 15 分钟看完。

1. 整体设计思路:为什么对象创建要拆成两步

1.1 Python 对象出生的完整流程

很多人对对象创建的理解是:MyClass()等于调用了__init__。这个理解在“能用”层面没毛病,但在“想进阶”层面是硬伤。真相是,MyClass()触发的是一个双层流转过程:先经过__new__分配内存并返回一个空壳实例,然后这个空壳实例被交给__init__做属性填充。

我打个比方你就明白了。__new__像是开发商把毛坯房交到你手上,这时候房子是空的,墙是白的,水电都没通,但它已经是一套“房子”了。__init__是你请装修队进场,刷墙、铺地板、装灯具,让房子具备真正可居住的功能。一个对象必须先有“毛坯房”这个实体,才能谈“装修”,所以__new__永远先跑,__init__永远后跑。

这个设计不是拍脑袋定下来的。Python 的 C 层实现里,type_call函数负责实例化流程,它的逻辑简单粗暴:先调用tp_new创建实例,如果这个实例是目标类或子类的实例,就再调用tp_init进行初始化;如果不是,就跳过初始化。这个“检查返回实例类型再决定是否初始化”的机制,是理解__new__返回值特殊性的关键。

class Demo: def __new__(cls, *args, **kwargs): print("1. __new__ 执行,创建空壳实例") instance = super().__new__(cls) return instance def __init__(self, *args, **kwargs): print("2. __init__ 执行,填充属性") d = Demo()

运行结果永远是先打印 1 再打印 2,顺序清清楚楚。

1.2 与 Java/C++ 构造函数的本质差异

写过 Java 或 C++ 的人会疑惑:这俩语言里构造函数一步到位,new关键字一出来,内存分配和初始化全干了,为什么 Python 要多此一举拆成两段?

差异的根源在于 Python 的“一切皆对象”哲学。在 C++ 里,new是语言级运算符,你没法拦腰截断它;但在 Python 里,__new__和__init__是普通的方法,可以被重写、被继承、被动态替换。这就赋予了程序员一种罕见的能力:“拦截对象的创建过程本身”。

举个例子,你想创建一个对象,但不想走__init__的参数校验,怎么办?在 Java 里你得写两个构造函数,再搞一个私有静态工厂方法;在 Python 里,你直接在__new__里返回一个object.__new__(cls)就能绕开__init__。灵活度完全不在一个量级。

另外还有一个底层原因:不可变对象的特殊性。像int、str、tuple这种类型,一旦创建就不能改,所以必须在创建阶段(也就是__new__阶段)把值定死。如果让__init__参与,开发者就能通过self.xxx = yy去修改属性,就破坏了不可变约束。所以这些内置类型的__init__往往是个空操作,真正干活的全在__new__里。这也是你继承不可变类型时一定要重写__new__的原因——后面实战部分我会演示。

提示:别把__new__理解成“高级”或“冷门”知识。在 Python 面试进阶题、框架源码、元编程代码里,它出现的频率远超你想象。懂它的人写代码是掌控对象生命周期的,不懂的人只是在使用对象生命周期。

2. 核心细节解析:__new__与__init__调用机制全拆解

2.1 方法签名与参数传递链条

先看两者签名的标准写法:

class Sample: def __new__(cls, *args, **kwargs): print("new 收到的参数:", args, kwargs) return super().__new__(cls) def __init__(self, *args, **kwargs): print("init 收到的参数:", args, kwargs) self.args = args s = Sample(1, 2, name="tom")

运行结果,假设去掉打印干扰,你会发现__new__和__init__收到的args和kwargs完全一致。这个“参数透传”机制是从type_call层原封不动传下来的:调用Sample(1, 2, name="tom")时,参数会先全部传给__new__,然后__new__返回实例后,同样的参数再传给__init__。

注意__new__的第一个参数是cls(类本身),而不是self。因为此刻实例还没创建出来,你怎么可能有self?这是初学__new__最容易精神分裂的地方。而__init__的第一个参数必须是self,它接收的是__new__返回的那个实例。

args参数传递还有一个隐蔽的坑:如果你重写了__new__,在里面消费掉了部分参数,比如super().__new__(cls, 1, 2)传了三个位置参数,但这些多出来的参数也会向下传给__init__。如果__init__的签名不匹配,就会直接TypeError。标准做法是__new__里不要额外添加签名不同的参数,保持透传一致性。

2.2__new__返回值类型如何决定是否调用__init__

这是整篇文章的重中之重,也是最多人栽跟头的地方。规则只有一条:如果__new__返回的实例是当前类(cls)或其子类的实例,Python 会继续调用__init__;如果返回的是其他类型的对象,或者干脆是None,__init__不会被调用。

看这段代码:

class A: def __new__(cls, *args, **kwargs): return object.__new__(cls) # 返回本类实例,会走 __init__ class B: def __new__(cls, *args, **kwargs): return 42 # 返回一个 int,不会走 __init__ a = A() # __init__ 被调用 b = B() # __init__ 不被调用,b 的值是 42,不是 B 实例

这里有个一眼看上去很离谱的信息:B()返回的不是B实例,而是整数 42。type_call检查返回值类型后发现不是cls的实例,直接放弃初始化并把 42 原样返回。这意味着你可以用__new__实现一个“假类”——类名摆在那儿,实例却是别的东西。

这在设计模式里对应着工厂模式:__new__可以根据参数决定返回哪一个子类实例。代码是这样的:

class Shape: def __new__(cls, kind, *args, **kwargs): if kind == "circle": return Circle(*args, **kwargs) elif kind == "square": return Square(*args, **kwargs) else: return super().__new__(cls) class Circle(Shape): def __init__(self, radius): self.radius = radius class Square(Shape): def __init__(self, side): self.side = side

Shape("circle", 5)返回的是Circle实例,Shape("square", 4)返回的是Square实例。因为返回值不是Shape类的实例,所以Shape.__init__会被跳过,但对应子类的__init__会正常执行——因为Circle.__new__最终返回的是Circle实例。这个机制利用好了,写出来的 API 会非常干净。

特别注意:如果__new__返回的实例既不是cls也不是子类实例,__init__直接不执行,且不会报错。这是一个“静默跳过”规则,写代码时一旦没注意,就会出现“为啥我对象属性是空的”这种诡异 bug。

2.3object.__new__与super().__new__的正确姿势

在你自定义类的__new__里,标准写法是return super().__new__(cls),这实际上是沿着 MRO 向上找下一个类的__new__,最终会落到object.__new__(cls)上。object.__new__是 C 层实现的分配器,它执行真正的内存分配,然后返回一个没有绑定任何实例属性的空对象。

为什么不能用return super().__new__(cls, *args)直接把参数也传上去?这也是文档里没细说但实战中必须掌握的:object.__new__只接受cls一个参数(实际上多了也能忽略),你把多余的参数传上去虽然大多数时候不报错,但语义上并不保证安全。规范性写法是只传cls,参数让type_call自动绕过你的__new__继续传给__init__。

另一种极端情况是:某些情况下你不需要走object.__new__,而是从已有的对象复制。比如通过__reduce__反序列化、或者 deepcopy 场景,这种场景就很微妙。我碰到过一个真实案例:某个 ORM 模型做了很多字段校验,直接Model()实例化会导致校验失败,但反序列化时又必须快速重建对象。此时__new__就成了旁路:直接obj = object.__new__(Model)就能创建一个不触发__init__校验逻辑的空对象,再手动填充数据。这个技巧在数据库 Row 对象、Redis 缓存恢复、高效性能优化场景中极为实用。

2.4__init__不能返回值的底层原因

很多人不知道:__init__如果写了return非 None 值,Python 会直接抛TypeError。这是因为type_call在调用完__init__后根本不接收它的返回值——初始化就是原地修改self,不需要返回值。这一段没必要细究,我讲一下它是怎么和__new__协作的就行:__new__的返回值才是实例化的结果,__init__更像是一个“附赠的后处理钩子”。

顺带提一个经典面试题:__new__和__init__哪个先执行?答案是__new__先。但如果让你实现一个“只创建不初始化”的场景,答案就是:在__new__里调用super().__new__(cls)返回实例,然后不执行__init__——利用返回其他对象跳过机制也可以做到。理解了这两层,这道题才算真正答全。

3. 实战实现:五大场景逐一攻破

3.1 场景一:线程安全的单例模式

单例模式是__new__最广为人知的实战场景。教科书写法是定义一个类变量_instance,如果为 None 就创建,否则直接返回。这个写法在单线程下没问题,但多线程环境下会有竞态条件:两个线程同时发现_instance为 None,都去创建,就产生多个实例。

有了__new__,我们可以把单例控制收敛到创建入口。下面给一个带锁的标准实现:

import threading class Singleton: _instance = None _lock = threading.Lock() def __new__(cls, *args, **kwargs): if cls._instance is None: with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self, value): # 注意:单例模式下 __init__ 可能被多次调用! self.value = value

这里有一处隐性坑很多人会踩:由于Singleton(1)和Singleton(2)最终拿到的实例是同一个对象,__init__会被执行两次,value属性会被后调用的那次覆盖。如果你希望单例第一次创建后就固定初始化参数,你需要加一个标志位:

class SingletonFixed: _instance = None _initialized = False def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self, value): if not self._initialized: self.value = value type(self)._initialized = True

这里我用type(self)._initialized而不是self._initialized,是为了写类变量而不是实例属性,避免单例的第二次__init__因为实例属性已经存在而误判。这个细节是实测之后才总结出来的——如果你把_initialized只写在self上,第二次初始化时读取的是实例自己的属性,照样是 True,没问题;但万一混用继承,类变量和实例变量的查找顺序会让你崩溃。

经验:如果你的单例类会被继承,cls._instance的绑定逻辑更复杂,需要给每个子类维护独立实例。可以用一个字典_instances = {},键是类本身,值是实例。这块属于进阶中的进阶,面试时提一句就能镇住场子。

3.2 场景二:不可变对象子类与缓存机制

继承int、tuple这类不可变内置类型时,__init__不会给你修改属性的机会,但你可以在__new__阶段做更多事。比如创建一个“总是取正数”的整数子类:

class PositiveInteger(int): def __new__(cls, value, *args, **kwargs): if value < 0: value = -value return super().__new__(cls, value) p = PositiveInteger(-8) print(p) # 8

注意这里我把super().__new__(cls, value)传入了值参数,因为int.__new__需要知道初始值是多少。这和标准object.__new__不同——内置类型的__new__承担了真正的“赋值”职责。

更进阶的玩法是利用__new__实现对象缓存。类似 Python 对小整数 -5 到 256 的缓存:如果数据已经存在,直接返回同一个实例,避免重复分配内存。例如实现一个根据经纬度复用的坐标对象:

class Coordinate: _cache = {} def __new__(cls, lat, lng): key = (lat, lng) if key in cls._cache: print("命中缓存,返回已有实例") return cls._cache[key] print("未命中缓存,创建新实例") obj = super().__new__(cls) cls._cache[key] = obj return obj def __init__(self, lat, lng): self.lat = lat self.lng = lng a = Coordinate(31.23, 121.47) b = Coordinate(31.23, 121.47) print(a is b) # True

这个模式在生产环境非常有用。比如坐标点、文件句柄、配置项这类“相同参数就应该复用同一个对象”的场景,用__new__在源头拦截性能收益远高于在业务层做判断。

3.3 场景三:参数驱动的工厂模式

前面讲返回值规则时提过工厂模式,这里展开成一个完整例子。假设你要做一个消息系统,根据协议类型返回不同的消息处理器,但对外 API 希望保持Message("json", data)这种简洁形式:

class Message: def __new__(cls, msg_type, *args, **kwargs): if msg_type == "json": return JsonMessage(*args, **kwargs) elif msg_type == "xml": return XmlMessage(*args, **kwargs) else: raise ValueError(f"Unknown message type: {msg_type}") class JsonMessage(Message): def __init__(self, data): self.data = data self.format = "json" def parse(self): return f"json: {self.data}" class XmlMessage(Message): def __init__(self, data): self.data = data self.format = "xml" def parse(self): return f"xml: {self.data}" msg = Message("json", {"key": "value"}) print(msg.parse()) # json: {'key': 'value'} print(type(msg)) # JsonMessage

这个设计的优雅之处在于:调用方根本不需要知道子类存在,只面对Message一个入口。新加一种协议类型时,只需要写一个新子类,__new__里加一个分支,对调用方完全透明。这和 GoF 工厂模式的核心思想一致,但用__new__实现几乎是零成本的——不需要额外的 Factory 类。

3.4 场景四:元类中的__new__与类中的__new__如何区分

很多人一接触元类就被绕晕,因为里面也有__new__。一句话区分:类里的__new__创建类的实例;元类里的__new__创建类本身。前者在实例化时触发,后者在定义类时触发。

class Meta(type): def __new__(mcls, name, bases, namespace, **kwargs): print(f"元类 __new__ 正在创建类 {name}") cls = super().__new__(mcls, name, bases, namespace) return cls class MyClass(metaclass=Meta): def __new__(cls, *args, **kwargs): print("类 __new__ 正在创建实例") return super().__new__(cls) obj = MyClass()

执行顺序是:定义MyClass时触发Meta.__new__,调用MyClass()时触发MyClass.__new__。这个区分点是 Python 元编程的核心分水岭。你在阅读 ORM、Django Form 这类框架源码时,看到__new__第一反应应该是“这是创建类,还是创建实例?”判断错了,整段逻辑就看歪了。

3.5 场景五:用__new__实现不可变属性对象

namedtuple本质上就是通过定制元类加__new__实现的。我们也可以自己做一个FrozenClass:实例创建后属性不可变,但在创建时可设置。思路是在__new__阶段把值写入,在__init__阶段禁用修改。

class FrozenClass: def __new__(cls, name, age): obj = super().__new__(cls) obj._frozen = False obj.name = name obj.age = age obj._frozen = True return obj def __setattr__(self, key, value): if getattr(self, "_frozen", False): raise AttributeError(f"Cannot modify frozen attribute: {key}") super().__setattr__(key, value) f = FrozenClass("Tom", 25) print(f.name) # Tom f.age = 30 # 抛 AttributeError

注意一个细节:我在__new__里先设_frozen = False,再正常设置name和age,最后设_frozen = True。因为__setattr__会拦截所有属性赋值,如果不先把_frozen放开,第一次设置名字时就会把自己锁死。这是写冻结对象最容易犯的错误,我在调试时踩过不下三次。

4. 常见问题与排查技巧:这些坑我替你踩过了

4.1__new__忘写 return 导致__init__不执行

这个错误非常隐蔽:__new__里写了打印日志,但忘了return super().__new__(cls),结果实例化出来的是 None,__init__也没跑,属性全是空的,后续一访问就抛AttributeError。

排查方法其实非常简单:在__new__和__init__的第一行都打印id(self)或self.__class__,对比两次输出的对象地址。正常情况下两次地址相同;若不同或第一次打印就是 None,那铁定是__new__没返回正确类型。

class Broken: def __new__(cls, *args, **kwargs): print("new 里干啥都行,就是没返回") # 手滑了,没有 return b = Broken() print(b) # None

4.2__new__的参数和__init__的参数不一致导致 TypeError

场景是这样的:你给子类__new__增加了参数,做了某些处理后把多余参数吞掉了,结果__init__那边还是按原参数签名等待,两边收获不一致,直接报错。最佳实践是:如果不需要额外参数,保持*args, **kwargs透传;如果必须增加参数,请同时重写__init__并保持签名匹配。

这里还有个特殊情况要提醒:__new__在参数透传时不经过super().__new__(cls, *args)这几个参数,实际上object.__new__会忽略多余参数,但如果你调的是int.__new__这种内置类型,参数就有意义了,千万别省。

4.3__init__重复执行引发的副作用

单例模式下__init__多次调用是最典型的副作用场景。除了用标志位控制,还有一种更干净的做法:把初始化逻辑从__init__移到__new__里,__init__改成空操作。这正好呼应了前面说的“不可变对象在__new__里干活”的思路。

class SingletonClean: _instance = None def __new__(cls, value): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance.value = value return cls._instance def __init__(self, value): pass # 不需要重复初始化

4.4 调试魔法函数调用顺序的速查技巧

我自己的调试习惯是封装一个小工具函数,在__new__和__init__里打印调用栈,或者用sys._getframe().f_code.co_name确认当前执行的是哪个方法。更粗暴一点,直接用 trace:

import sys def trace(name): frame = sys._getframe(1) print(f"[{name}] called from {frame.f_code.co_name}:{frame.f_lineno}") class DebugDemo: def __new__(cls, *args, **kwargs): trace("__new__") return super().__new__(cls) def __init__(self, *args, **kwargs): trace("__init__")

这比盲猜快得多。另一个技巧是在__init__里打印self.__dict__,看属性到底是在哪个阶段填充的——如果__new__执行完self.__dict__是空的,说明属性全是在__init__里加的;如果已经有值了,说明创建阶段就做了手脚。

5. 底层原理补充:type_call就是一切的总指挥

5.1 C 层视角的实例化流程

理解了__new__和__init__的调用规则后,再往下一层看看 Python 解释器到底干了什么。所有MyClass(...)调用最终都汇聚到 C 层的type_call函数,它的大致逻辑如下:

  1. 从类型对象的tp_new槽位取出函数指针(对应__new__)。
  2. 调用它,传入类型本身和调用参数,得到obj。
  3. 检查obj的类型是否与当前类兼容(是本身或子类)。
  4. 如果兼容,从tp_init槽位取出函数指针(对应__init__),再调用一遍同样的参数。
  5. 返回obj。

这个“先检查再初始化”的流程解释了为什么__new__返回值类型如此关键。你看源码时如果看到type_call里的判断语句,一切规则都会变得异常通透。Python 官方文档里对__new__的那句话——“如果它是 cls 的实例,则新实例会像调用__init__(self, ...)一样被初始化”——背后就是这个 C 层逻辑。

5.2 为什么__new__经常和__init__一起被忽略了

新手最容易产生的困惑是:为什么我重写了__new__,但__init__看起来没有被调用?这个问题本质上和“返回类型”有关。只要你的__new__返回的是cls本身或子类实例,__init__一定被调用。如果__init__没跑,必然是你的__new__返回了其他类型,或者是参数在传递途中被消费掉了。下次遇到这种诡异行为,不要直接怀疑解释器,先回头看一眼返回值。

6. 延伸思考:什么时候该用__new__,什么时候该绕开它

写了这么多,必须给个清醒的结论:不是所有场景都需要亲自重写__new__。90% 的业务代码用不到它,强行重写反而增加心智负担。但下面几种信号出现时,你就该考虑它:

  • 需要控制实例的创建数量(单例、池化、缓存)
  • 需要让一个类根据参数返回不同子类实例(工厂)
  • 需要继承int、str、tuple等不可变内置类型
  • 需要在对象创建时绕过或改造属性填充逻辑
  • 在读框架源码,遇到cls = super().__new__(cls)时能看懂

反过来,如果只是给普通类加几个属性,__init__是绝对的正解。把__new__当主菜吃是炫技,容易上火;把它当特殊工具备着,该用时拿出来,才是进阶玩家该有的状态。

动手写代码的时候,建议你先用最小的例子验证__new__返回值的分支行为,再逐步叠加场景。我曾经花了一个下午跑各种边界条件,最后总结出来的就是一句话:__new__管出身,__init__管修为,type_call是那个站在幕后检查出身决定是否让修为生效的裁判。这句话你品透了,Python 对象创建这块就算毕业了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 1:15:42

MOM系统运营实践:从MES到制造运营闭环的落地指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/11 1:15:27

基于PJ85718DM与PIC18F47Q10的嵌入式远程温度监测方案

1. 项目缘起与整体设计思路嵌入式温度监测这个方向&#xff0c;看起来简单&#xff0c;实际上是个"深水区"。我接触过不少做 HVAC&#xff08;暖通空调&#xff09;控制器的团队&#xff0c;十个里面有八个在温度采样这一环踩过坑——要么是本地测温精度不够导致压缩…

作者头像 李华
网站建设 2026/10/11 1:14:18

基于Hadoop的电影推荐系统:数据清洗到ALS落地全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/11 1:13:15

YOLO目标检测实战:飞机鸟类无人机数据集训练与PyQt5界面部署

简介&#xff1a;本资源面向计算机视觉学习者与目标检测工程实践者&#xff0c;提供一套细分类型飞机、鸟类与无人机的YOLOv5检测训练方案&#xff0c;重点解决细粒度识别中机型区分难、样本组织繁琐的问题&#xff0c;适合具备一定深度学习基础、希望快速复现实验或搭建演示系…

作者头像 李华
网站建设 2026/10/11 1:13:04

钢铁产销一体化:用三张表+消息队列打通ERP/MES/LIMS断点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/11 1:13:01

PJ85718DM+STM32F101ZG工业温控方案设计与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华