ARTICLE DETAIL

资讯详情

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

Practical Python 对象模型解密:从字典实现到封装技巧的完整指南

Practical Python 对象模型解密:从字典实现到封装技巧的完整指南 教程文档【免费下载链接】practical-pythonPractical Python Programming (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/pr/practical-python点击查看免费下载本篇技术指南聚焦 Practical Python Programming 课程第五节“Inner Workings of Python Objects”Python 对象的内部工作机制系统讲解 Python 对象系统如何建立在字典之上、属性查找与 MRO 继承解析规则、Mixin 复用模式以及私有属性、property 与__slots__等封装技巧。读完本文你将掌握__dict__/__class__/__mro__等对象的内部结构、super()的正确使用时机并能用 property 和__slots__写出更健壮、更省内存的类。为什么需要理解对象内部机制很多从 C、Java 等语言转向 Python 的程序员会觉得 Python 的类“缺东西”没有private/protected之类的访问控制self参数看起来古怪操作对象有时像“自由大乱斗”。本节课程的价值在于你不必纠结这些内部细节也能高效编程但大多数 Python 开发者都对类的工作方式有基本认知——这正是本节的定位。实际上Python 的对象系统几乎完全是在**字典dictionary**之上加了一层薄薄的封装。字典不仅是 Python 中常用的简单数据结构更是解释器的关键组成部分可以说是Python 中最重要的数据类型。理解这层“字典底座”你就理解了属性赋值、方法调用、继承查找的全部真相。1. 字典与模块一切名字都是字典键字典是“命名字值的集合”例如stock { name : GOOG, shares : 100, price : 490.1 }在模块内部一个字典保存了所有全局变量和函数。假设有文件foo.py# foo.py x 42 def bar(): ... def spam(): ...通过foo.__dict__或内置函数globals()可以查看这个字典{ x : 42, bar : function bar, spam : function spam }也就是说模块命名空间本身就是字典这为后续理解“对象 字典”打下了基础。2. 字典与对象实例数据的真相用户自定义对象同样使用字典来保存实例数据和类成员。事实上整个对象系统大部分就是“套在字典上的一层额外封装”。实例数据保存在__dict__中 s Stock(GOOG, 100, 490.1) s.__dict__ {name : GOOG, shares : 100, price: 490.1 }当你在__init__中给self赋值时就是在填充这个字典class Stock: def __init__(self, name, shares, price): self.name name self.shares shares self.price price于是self.__dict__变成{ name: GOOG, shares: 100, price: 490.1 }每个实例拥有自己独立的私有字典。创建 100 个实例就有 100 个字典在各自保存数据s Stock(GOOG, 100, 490.1) # {name : GOOG,shares : 100, price: 490.1 } t Stock(AAPL, 50, 123.45) # {name : AAPL,shares : 50, price: 123.45 }3. 类成员字典方法与类变量的存放处类的方法存放在另一份独立字典Stock.__dict__中class Stock: def __init__(self, name, shares, price): self.name name self.shares shares self.price price def cost(self): return self.shares * self.price def sell(self, nshares): self.shares - nshares查看Stock.__dict__{ cost: function, sell: function, __init__: function }实例和类通过__class__属性互相链接实例字典存放“每个实例独有的数据”类字典存放“所有实例共享的成员” s Stock(GOOG, 100, 490.1) s.__dict__ { name: GOOG, shares: 100, price: 490.1 } s.__class__ class __main__.Stock4. 属性访问.操作符背后的字典读写日常用.运算符访问数据和方法x obj.name # Getting obj.name value # Setting del obj.name # Deleting这些操作直接对应底层字典的增删改查。修改对象的操作会同步更新底层字典 s Stock(GOOG, 100, 490.1) s.__dict__ { name:GOOG, shares: 100, price: 490.1 } s.shares 50 # Setting s.date 6/7/2007 # Setting甚至可以添加 __init__ 中没有的属性 s.__dict__ { name: GOOG, shares: 50, price: 490.1, date: 6/7/2007 } del s.shares # Deleting s.__dict__ { name: GOOG, price: 490.1, date: 6/7/2007 }读取属性时属性可能存在于两个位置——实例局部字典或类字典因此必须依次查找先查局部__dict__找不到就通过__class__查类的__dict__ s Stock(...) s.name GOOG s.cost() 49010.0正是这个查找方案让class的成员被所有实例共享。本仓库 解决方案 5_8/stock.py 中的Stock类可以随时用于验证上述行为实例goog.__dict__中找不到cost它始终来自Stock.__dict__[cost]。5. 继承__bases__与__mro__5.1 基类存储与查找扩展类可以从其他类继承class A(B, C): ...基类以元组形式保存在每个类上 A.__bases__ (class __main__.B, class __main__.C)逻辑上属性查找过程是先查局部__dict__找不到再查类的__dict__还找不到就通过__bases__逐级向上查基类。5.2 单继承沿树向上首个匹配即止class A: pass class B(A): pass class C(A): pass class D(B): pass class E(D): pass单继承只有一条通向顶端的路径遇到第一个匹配就停止。5.3 MRO预计算的继承链Python 会预计算一条继承链存放在类的__mro__属性中 E.__mro__ (class __main__.E, class __main__.D, class __main__.B, class __main__.A, type object)这条链就叫Method Resolution Order方法解析顺序。查找属性时Python 按 MRO 顺序逐个类查找第一个匹配者获胜。5.4 多继承C3 线性化算法多继承下不存在单一通顶路径class A: pass class B: pass class C(A, B): pass class D(B): pass class E(C, D): passe E(); e.attr的查找顺序怎么定Python 采用协作式多继承cooperative multiple inheritance遵循两条排序规则子类总是先于父类被检查多个父类总是按声明顺序被检查。按此规则排序整个层级即得 MRO E.__mro__ ( class E, class C, class A, class D, class B, class object)底层算法叫C3 线性化算法。具体细节不必深究只需记住层级遵守的排序规则就像“家里着火要疏散”一样——孩子优先然后是父母。5.5 练习 5.5用 MRO 手工定位方法继承在实现上就是“扩展属性搜索过程”。在NewStock(Stock)中__bases__给出直接父类元组__mro__给出全部祖先的搜索顺序 NewStock.__bases__ (class stock.Stock,) NewStock.__mro__ (class __main__.NewStock, class stock.Stock, class object)甚至可以手工模拟实例n上cost()的查找过程 for cls in n.__class__.__mro__: if cost in cls.__dict__: break cls class __main__.Stock cls.__dict__[cost] function cost at 0x101aed5986. 两个不相关的类一段相同的代码Mixin 模式考虑两个毫无关联的类class Dog: def noise(self): return Bark def chase(self): return Chasing! class LoudDog(Dog): def noise(self): # 与下面的 LoudBike 代码相同 return super().noise().upper()以及class Bike: def noise(self): return On Your Left def pedal(self): return Pedaling! class LoudBike(Bike): def noise(self): # 与上面的 LoudDog 代码相同 return super().noise().upper()LoudDog.noise()和LoudBike.noise()的实现代码完全相同这种重复自然吸引软件工程师去消除。Mixin 模式就是“包含一段代码片段的类”它不能孤立使用必须通过继承与其他类混合class Loud: def noise(self): return super().noise().upper() class LoudDog(Loud, Dog): pass class LoudBike(Loud, Bike): pass“响亮”这个能力只实现了一次却在两个完全无关的类中复用——这正是 Python 多继承的主要用途之一。7. 为什么总是用super()重写方法时请始终使用super()class Loud: def noise(self): return super().noise().upper()super()会把调用委托给MRO 上的下一个类。麻烦之处在于你事先不知道下一个类是谁——尤其是在多继承场景下。使用super()而不是硬编码父类名才能保证在任意 MRO 下协作正确。8. 封装技巧私有属性、property、__slots__8.1 现实问题Python 里一切皆开放类的首要职责之一是封装数据与内部实现细节同时对外提供public接口。但 Python 中类与对象几乎一切皆开放可以轻松窥探对象内部可以随意改动没有强制的访问控制如私有类成员。因此 Python 依靠命名约定而非语言强制来表达意图任何以_开头的名字都被视为private——这只是一种编程风格你依然可以访问和修改它class Person(object): def __init__(self, name): self._name 0 p Person(Guido) p._name Guido p._name Dave通用规则无论是变量、函数还是模块名带前导_都表示内部实现。直接使用这类名字通常说明你做错了应当寻找更高级的功能。8.2 简单属性的失控与访问器方法普通类有个惊人特性属性可以被赋予任意值class Stock: def __init__(self, name, shares, price): self.name name self.shares shares self.price price s Stock(IBM, 50, 91.1) s.shares 100 s.shares hundred s.shares [1, 0, 0]若想对s.shares 50抛TypeError一种方案是引入访问器方法class Stock: def __init__(self, name, shares, price): self.name name self.set_shares(shares) self.price price # 包装 get 操作 def get_shares(self): return self._shares # 包装 set 操作 def set_shares(self, value): if not isinstance(value, int): raise TypeError(Expected an int) self._shares value可惜这破坏了所有既有代码s.shares 50必须改写为s.set_shares(50)。8.3 property不破坏源码的受控属性property 提供了替代方案class Stock: def __init__(self, name, shares, price): self.name name self.shares shares self.price price property def shares(self): return self._shares shares.setter def shares(self, value): if not isinstance(value, int): raise TypeError(Expected int) self._shares value普通属性访问现在会触发property下的 getter 和shares.setter下的 setter s Stock(IBM, 50, 91.1) s.shares # 触发 property 50 s.shares 75 # 触发 shares.setter源码无需任何改动新 setter 甚至会在类内部的赋值包括__init__()里的self.shares shares时被调用。一个常见的困惑点是 property 与私有名的关系property 内部虽然用了_shares这样的私有名但类的其余部分property 之外仍可继续使用shares这个名字。property 也适合做计算型数据属性class Stock: def __init__(self, name, shares, price): self.name name self.shares shares self.price price property def cost(self): return self.shares * self.price由此可以去掉多余的括号隐藏“这其实是个方法”的事实 s Stock(GOOG, 100, 490.1) s.shares # 实例变量 100 s.cost # 计算值 49010.08.4 统一访问接口上面的例子展示了如何给对象更统一的接口。如果不这样做对象用起来很让人困惑 s Stock(GOOG, 100, 490.1) a s.cost() # 方法要加 () 49010.0 b s.shares # 数据属性不用加 () 100为什么cost要()、shares却不用property 可以解决这个不一致。语法叫“装饰decoration”它给紧随其后的函数定义施加一个修饰器更详细的说明见 第 7 节高级主题。8.5__slots__限制属性并优化内存__slots__可以限制允许的属性名集合class Stock: __slots__ (name,_shares,price) def __init__(self, name, shares, price): self.name name ...对其他属性赋值会抛出错误 s.price 385.15 s.prices 410.2 Traceback (most recent call last): File stdin, line 1, in ? AttributeError: Stock object has no attribute prices它虽然能预防错误、限制对象用法但真正用途是性能优化让 Python 使用更高效的内存表示。使用__slots__后对象不再维护__dict__因此s.__dict__会报错这正是练习 5.8 要求观察的现象。使用建议来自文档的最终评论不要过度使用私有属性、property、slots 等。它们有特定用途你会在阅读他人代码时遇到但对大多数日常编码并非必需。__slots__最常用于作为数据结构的类能让程序占用内存更少、运行略快其他类应尽量避免。9. 仓库源码佐证从练习到生产级封装课程练习最终形成的 5_8 版 stock.py 是上述全部技巧的综合体——__slots__、property 计算属性cost、带类型检查的shares.setter一应俱全还额外实现了__repr__便于调试class Stock: __slots__ (name,_shares,price) def __init__(self,name, shares, price): self.name name self.shares shares self.price price property def shares(self): return self._shares shares.setter def shares(self, value): if not isinstance(value,int): raise TypeError(Must be integer) self._shares value property def cost(self): return self.shares * self.price def sell(self, nshares): self.shares - nshares配套的 report.py 用列表推导把解析出的字典构造成Stock对象make_report_data中则直接以s.shares、s.price的“统一属性接口”访问数据。把cost从方法改成 property 后依赖方也需要同步调整——6_3 版 pcost.py 已改为portfolio.total_cost属性式访问而更早的 5_8 版 pcost.py 还保留着s.cost()的调用形式二者对比正好演示了“property 改变调用约定”的实际影响。本仓库 7_11 版 typedproperty.py 更进一步用闭包工厂函数动态生成带类型检查的 property并给出String/Integer/Float三个 lambda 快捷方式证明“property 本质是返回描述符的函数对象”——这正是第 7 节“返回函数的函数”思想的实战落地。最后8_1 版 test_stock.py 用unittest验证了封装的效果s.shares 100必须抛出TypeErrors.cost以属性形式等于49010.0sell(25)后shares变为 75。这些测试是“封装被正确实现”的可执行证据。10. 动手练习速览本节配套练习基于第 4 节定义的Stock类先用from stock import Stock导入并创建goog/ibm两个实例练习主题验证要点5.1实例表示检查goog.__dict__、ibm.__dict__的字典内容5.2实例数据修改goog.date 6/11/2007后只有 goog 有dategoog.__dict__[time] 9:45am证明实例就是字典的薄封装直接操作字典并不常见应始终用.语法5.3类的角色goog.cost()来自Stock.__dict__[cost]可显式以Stock.__dict__cost调用Stock.foo 42类变量对全部实例可见但不在实例字典中5.4绑定方法s goog.sell是 bound methods.__func__即Stock.__dict__[sell]s.__self__即 self 实参s(25)等价于s.__func__(s.__self__, 25)5.5继承NewStock(Stock)的__bases__/__mro__展示查找顺序5.6简单 property把cost改为 propertys.cost直接取值注意这会破坏旧版pcost.py需去掉cost()的括号5.7property 与 setters.shares a lot抛出TypeError5.8添加 slots新属性s.blah 42报错且s.__dict__不再存在绑定方法环节值得单独强调调用方法实际包含两步与“绑定方法”机制。s(25)本质上就是s.__func__(s.__self__, 25)——函数与实例在绑定方法中被完整记录执行()时拼装调用。结语理解“对象是字典的薄封装”之后Python 的一切对象行为——属性赋值、方法解析、继承查找、Mixin 复用——都变得可预测、可验证。property 与__slots__则是从“自由”走向“受控”的两件利器前者保持源码兼容地加入校验与统一接口后者以放弃__dict__为代价换取更低内存与更高性能。课程原文的态度值得借鉴知道它们、会用它们但别过度设计。相关章节导航目录 | 上一节类与对象 | 下一节生成器赞分享教程文档【免费下载链接】practical-pythonPractical Python Programming (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/pr/practical-python点击查看免费下载相关推荐Python字典机制终极指南深入理解对象模型的秘密武器 Python字典机制终极指南深入理解对象模型的秘密武器 Python字典是Python编程语言中最重要、最强大的数据结构之一也是理解Python对象模教程文档Practical Python 数据处理实战元组、列表、字典、集合与对象模型完全指南Practical Python 数据处理实战元组、列表、字典、集合与对象模型完全指南 本文基于 Practical Python Programming教程文档Practical Python 对象模型剖析从 __dict__ 到 C3 线性化与 Mixin 模式Practical Python 对象模型剖析从 __dict__ 到 C3 线性化与 Mixin 模式 Python 的对象系统在底层几乎完全建立在字典之上教程文档上一篇智慧树刷课插件终极指南告别手动操作实现全自动学习下一篇DLSS Swapper5分钟掌握游戏性能优化的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表