十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python对象创建揭秘:__new__与__init__的区别、原理与典型应用场景

Python对象创建揭秘:__new__与__init__的区别、原理与典型应用场景 揭开 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. 一个实验看清真相new和init的执行顺序在讲任何理论之前我建议你先跑一个最简单的实验。眼见为实这句话放在程序世界里尤其成立。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, x3)的时候参数并不是直接丢给__init__的。Python 解释器真正的执行链路是这样的把类Demo和所有参数(1, 2, x3)一起交给type.__call__。type.__call__内部先调用Demo.__new__(Demo, 1, 2, x3)。如果__new__返回的实例是Demo类或其子类的实例再调用instance.__init__(1, 2, x3)。所以请注意__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 不可变类型的子类如果你继承的是int、str、tuple、frozenset这些不可变类型想在创建时做额外的定制那就必须在__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__还会反复执行后来我才反应过来对象复用不等于跳过init。type.__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。排查方法是打印 MROclass 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(metaclassMeta): 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里做手脚int、str、tuple这些内置不可变类型的设计目标是对象一旦创建内部状态不可变。在 CPython 的实现里这些类型的数据是在__new__阶段就已经填入的__init__阶段即使你想修改也没有对应的写入口。所以继承 tuple、str 并想在创建时修改数据只能在__new__里做文章。反过来如果你继承的是可变类型如list、dict那__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__并且锁的范围覆盖整个创建过程不会出现锁了new但init里又出问题的情况。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__的定位就不会跑偏了。
返回列表