Python面向对象进阶继承、多态、封装与魔法方法上一篇我们学会了用类来封装数据和操作。这一篇是面向对象的进阶篇——我们将学习三个核心概念继承、多态、封装和更多能让你的类用起来像 Python 原生类型的魔法方法。这些概念听起来可能有些抽象但它们解决的都是真实的编程问题。继承解决十几类相似但不同的对象如何共用代码多态解决不同的对象如何用同一个接口来操作封装解决怎样保护数据不被随意修改。当你需要用面向对象的方式写一个中等规模的项目时这三个概念会自然地从纸面跳到你的手指。一、继承与多态继承复用代码的最强方式问题场景假设你在写一个学校管理系统。有学生、有老师、有行政人员。他们有一些共同的属性都有姓名、年龄、ID但又有各自特有的属性和行为。如果给每种人都写一个独立的类name和age的初始化代码要在三个类里重复三次——我们又回到了被复制粘贴支配的恐惧。继承就是来解决这个问题的把共同的属性和方法放在一个父类里让子类来继承它。# 父类存放共同的属性和方法classPerson:def__init__(self,name,age,person_id):self.namename self.ageage self.person_idperson_iddefintroduce(self):returnf我叫{self.name}{self.age}岁# 子类继承 Person再加上自己特有的东西classStudent(Person):def__init__(self,name,age,student_id,major):super().__init__(name,age,student_id)# 调父类的 __init__self.majormajordefstudy(self):returnf{self.name}正在学习{self.major}classTeacher(Person):def__init__(self,name,age,teacher_id,subject):super().__init__(name,age,teacher_id)self.subjectsubjectdefteach(self):returnf{self.name}老师正在教{self.subject}# 使用sStudent(小明,20,S001,计算机科学)tTeacher(张老师,35,T001,Python程序设计)print(s.introduce())# 我叫小明20岁 ——继承自 Personprint(s.study())# 小明 正在学习 计算机科学 ——Student 自己的方法print(t.introduce())# 我叫张老师35岁 ——继承自 Personprint(t.teach())# 张老师 老师正在教 Python程序设计 ——Teacher 自己的方法super().__init__(...)调用了父类Person的初始化方法。super()是父类的意思——你不用硬编码父类名Python 会自动找到正确的父类。子类继承了父类的所有方法在这里是introduce()同时可以添加自己特有的方法study()、teach()也可以重写父类的方法来改变行为。重写父类方法classStudent(Person):defintroduce(self):# 完全重写——不再调用父类的 introducereturnf学生{self.name}{self.age}岁专业{self.major}sStudent(小明,20,S001,CS)print(s.introduce())# 学生 小明20岁专业 CSPython 在调用s.introduce()时先从Student类本身查找introduce方法——找到了就直接用。如果没找到再去父类Person里找。这就是方法解析顺序MRO的基本规则。isinstance 和 issubclasssStudent(小明,20,S001,CS)print(isinstance(s,Student))# Trueprint(isinstance(s,Person))# True ——子类实例也是父类实例print(isinstance(s,object))# True ——所有类最终都继承自 objectprint(issubclass(Student,Person))# Trueprint(issubclass(Student,object))# Trueprint(issubclass(Person,Student))# False这个特性非常重要——它意味着任何接受Person参数的函数也能接受Student和Teacher。这是多态的基础。图14-1 继承结构Person 作为基类Student 和 Teacher 继承 Person子类拥有父类的属性和方法并可以添加自己特有的内容。多态同一个接口不同的行为多态Polymorphism这个词听起来很吓人但它的意思其实很简单不同类型的对象可以响应同一个方法名各自做出不同的行为。classDog:defspeak(self):return汪汪classCat:defspeak(self):return喵喵classDuck:defspeak(self):return嘎嘎# 多态不管什么动物都用 speak() 这个统一接口defanimal_chorus(animals):foranimalinanimals:print(animal.speak())animal_chorus([Dog(),Cat(),Duck(),Dog()])# 汪汪# 喵喵# 嘎嘎# 汪汪注意Dog、Cat、Duck之间没有任何继承关系——它们只是都定义了speak()方法。在 Python 里多态不依赖继承这和其他静态语言不同它依赖的是鸭子类型——“如果它走路像鸭子叫起来像鸭子那它就是鸭子”。鸭子类型defprint_report(reportable):任何有 .report() 方法的对象都能传进来print(reportable.report())classStudent:defreport(self):return学生报告...classClassroom:defreport(self):return班级报告...classFinancialReport:defreport(self):return财务报告...# 这三个类毫无关系但都能传入 print_reportprint_report(Student())print_report(Classroom())print_report(FinancialReport())Python 不关心你是什么类型——它只关心你有没有我需要的方法。这种设计哲学让 Python 的代码非常灵活也是它易于编写测试和快速原型的原因之一。图14-2 多态调用同一方法 get_role() 在不同类上有不同实现调用方无需关心具体类型只关心对象有没有这个方法。二、封装与性能封装再深入私有属性和属性装饰器双下划线——名称改写单下划线_balance是约定。双下划线__balance是强制执行classBankAccount:def__init__(self,owner,balance):self.ownerowner self.__balancebalance# 双下划线名称改写defget_balance(self):returnself.__balance accountBankAccount(小明,1000)# print(account.__balance) # AttributeError真的不能直接访问了print(account._BankAccount__balance)# 但其实可以通过改写后的名字访问...双下划线会触发 Python 的名称改写机制——__balance被改写成了_BankAccount__balance。这提供了某种程度的强制隔离但它不是真正的访问控制不像 Java 的private。Python 社区倾向于信任开发者——用单下划线约定请不要碰而不是筑墙你不能碰。property让方法看起来像属性classTemperature:def__init__(self,celsius0):self._celsiuscelsiuspropertydefcelsius(self):获取摄氏温度returnself._celsiuscelsius.setterdefcelsius(self,value):设置摄氏温度带校验ifvalue-273.15:raiseValueError(温度不能低于绝对零度)self._celsiusvaluepropertydeffahrenheit(self):华氏温度只读根据摄氏计算returnself._celsius*9/532tempTemperature(37)print(temp.celsius)# 37 ——像读属性一样print(temp.fahrenheit)# 98.6 ——自动计算temp.celsius100# 像写属性一样自动走校验逻辑# temp.celsius -500 # ValueError# temp.fahrenheit 200 # AttributeError ——只读property最优雅的地方在于它让你在保证接口不变的前提下升级内部实现。一开始你可能直接把数据存成属性后来发现需要加校验时加一个property就行——外部调用代码一行不用改。这个特性在大型项目重构中价值连城。__slots__——当你需要节省内存时默认情况下每个 Python 对象都有一个__dict__字典来存储它的实例属性。字典的优点是灵活——你可以在运行时随时给对象添加新属性。但代价是每个对象占用的内存比必要的大。如果你要创建数百万个小对象这个额外开销会很显著。__slots__让你预先声明一个对象可以有哪些属性Python 就不再为该对象创建__dict__从而大幅减少内存占用classPoint:__slots__(x,y)# 只允许 x 和 y 两个属性def__init__(self,x,y):self.xx self.yy pPoint(1,2)p.x3# OK# p.z 4 # AttributeError: Point object has no attribute z对于普通应用来说__slots__不是必需品——绝大多数 Python 程序中的对象数量根本到不了需要优化的量级。但它的存在展示了 Python 设计的一个重要理念先用简单的方式写性能不够了再针对性地优化。默认的__dict__给所有对象提供了最大灵活性而__slots__给性能敏感的代码提供了逃生舱。这个先灵活后优化的哲学贯穿整个 Python 生态。更多魔法方法容器类方法——让你的类支持 len、索引和迭代classPlaylist:def__init__(self,name):self.namename self._songs[]defadd_song(self,song):self._songs.append(song)def__len__(self):len(playlist) 时调用returnlen(self._songs)def__getitem__(self,index):playlist[0] 时调用returnself._songs[index]def__setitem__(self,index,value):playlist[0] new_song 时调用self._songs[index]valuedef__contains__(self,song):song in playlist 时调用returnsonginself._songsdef__iter__(self):for song in playlist 时调用returniter(self._songs)playlistPlaylist(我的最爱)playlist.add_song(晴天)playlist.add_song(七里香)playlist.add_song(稻香)print(len(playlist))# 3 ——__len__print(playlist[0])# 晴天 ——__getitem__print(晴天inplaylist)# True ——__contains__forsonginplaylist:# __iter__print(f-{song})定义了这些方法后你的Playlist对象用起来就像一个内置的列表——len()、索引、in、for循环全部自然工作。这就是 Python 的设计哲学让你的自定义类型感觉像是语言原生的扩展。运算符重载classVector:def__init__(self,x,y):self.xx self.yydef__add__(self,other):v1 v2returnVector(self.xother.x,self.yother.y)def__sub__(self,other):v1 - v2returnVector(self.x-other.x,self.y-other.y)def__mul__(self,scalar):v * 3returnVector(self.x*scalar,self.y*scalar)def__neg__(self):-vreturnVector(-self.x,-self.y)def__abs__(self):abs(v) ——向量的模return(self.x**2self.y**2)**0.5def__bool__(self):bool(v) ——是否为非零向量returnself.x!0orself.y!0def__eq__(self,other):v1 v2returnself.xother.xandself.yother.ydef__repr__(self):returnfVector({self.x},{self.y})v1Vector(3,4)v2Vector(1,2)v3v1v2# Vector(4, 6) ——__add__v4v1-v2# Vector(2, 2) ——__sub__v5v1*3# Vector(9, 12) ——__mul__print(abs(v1))# 5.0 ——__abs__print(bool(v1))# True ——__bool__print(bool(Vector(0,0)))# False三、设计原则与实战多态的实际价值——一个让你少写一半代码的例子多态的价值在被动的阅读中很难感受到需要亲手维护过相似但不完全相同的代码才能体会。假设你在写报表生成功能——用户可以选择 PDF、Excel、HTML 三种格式输出。没有多态时你的代码会是这样ifformatpdf:export_pdf(data)elifformatexcel:export_excel(data)elifformathtml:export_html(data)每当增加一种新格式比如 CSV你需要在所有相关位置加一个新的elif。这看起来不算太糟但当一个系统里有十几个这样的按类型分派的地方时——格式选择、权限判断、地区差异、币种转换——你会发现自己像一个永远在修补屋顶的工人这边的漏洞刚补上那边又开始漏水。多态的解决方案定义一个统一的Exporter接口PdfExporter、ExcelExporter、HtmlExporter都实现export(data)方法。主逻辑里只需要exporter.export(data)——不管是哪种格式调用方式一模一样。增加新格式时新增一个类不需要修改任何已有的调用代码。这就是多态的核心价值——让系统对扩展开放对修改封闭。调用者不需要知道被调用者的具体类型只需要知道它提供了我需要的那个方法。这种设计在大型系统中是无可替代的。属性装饰器深入——当你需要比 getter/setter 更复杂的行为property不只是让方法看起来像属性这么简单。它的真正威力在于它让你在不改变外部接口的前提下对属性的读写行为做任何你想要的增强。比如你想知道一个对象被创建了多少次、某个属性被访问了多少次——在property里加一个计数器外部代码一行不用改。再比如你想给属性值做缓存——第一次访问时计算并存储后续访问直接返回缓存值。这在数据库查询、网络请求、复杂计算等场景中可以节省大量重复开销。Python 社区不鼓励像 Java 那样为每个属性都写 getter 和 setter。在 Python 里属性直接暴露通常是 OK 的——如果以后需要加逻辑用property无缝升级外部调用代码不受影响。这种先简单后增强的渐进式设计是 Python 哲学的重要组成部分。抽象基类定义必须实现的接口有时候你想定义这个类的子类必须实现某些方法否则不应该被实例化。用abc模块fromabcimportABC,abstractmethodclassAnimal(ABC):abstractmethoddefspeak(self):子类必须实现这个方法passclassDog(Animal):defspeak(self):return汪汪classCat(Animal):defspeak(self):return喵喵# animal Animal() # TypeError不能实例化抽象类dogDog()# OK实现了 speak()print(dog.speak())# 汪汪抽象基类在你设计框架、定义插件接口、或者确保团队中的其他开发者遵循约定时非常有用。但 Python 的鸭子类型在很多场景下比抽象基类更灵活——“必须继承换成了有这个方法就行”。多重继承和 MROPython 支持一个类同时继承多个父类classFlyable:deffly(self):return正在飞行classSwimmable:defswim(self):return正在游泳classDuck(Flyable,Swimmable):passduckDuck()print(duck.fly())# 正在飞行print(duck.swim())# 正在游泳如果多个父类有同名方法Python 使用C3 线性化算法来决定调用顺序。你可以通过Duck.__mro__查看方法解析顺序。多重继承是强大的工具但也是代码复杂度的来源——能用组合把一个对象作为另一个对象的属性解决的问题尽量不用多重继承。图14-3 面向对象三大特性继承复用代码、多态统一接口、封装隐藏细节在实际代码中的协同关系。组合优于继承这是面向对象设计里最有名的一条原则。继承表达的是是一个is-a关系——Student是一种Person。而很多场景其实是有一个has-a关系——Car有一个Engine而不是Car是一种Engine。# 用组合代替继承classEngine:defstart(self):return引擎启动defstop(self):return引擎停止classCar:def__init__(self):self.engineEngine()# Car 有一个 Engine而不是 Car 是一种 Enginedefdrive(self):returnf{self.engine.start()}出发carCar()print(car.drive())# 引擎启动出发当你发现自己为了复用几行代码而构建了一个深层继承树停下来想一想——是不是用组合更好组合让代码的耦合度更低每个类之间是合作关系而非血缘关系。面向对象的设计原则——SOLID 简介SOLID 是面向对象设计的五个核心原则的首字母缩写。你不需要现在就完全掌握它们但了解它们的名字和一句话描述会给你未来读源码和做设计时一个有用的思维框架单一职责一个类只做一件事、开放封闭对扩展开放对修改封闭——本文的多态就是最好的例子、里氏替换子类应该能完全替代父类而不引起问题、接口隔离不要强迫使用者依赖它不需要的方法、依赖反转依赖抽象而不是具体实现。每当你为一个设计决策而犹豫时回头想想这五个原则中的哪一个可以帮你做出更好的选择。它们不是教条而是经验——是很多前辈在漫长的软件工程实践中总结出来的什么管用、什么不管用。Python 社区对 SOLID 的态度比 Java 社区宽松得多——Python 崇尚实用主义不会为了遵循原则而过度设计。所以了解 SOLID 但不要膜拜它。把它当作一个参考框架需要时取用不需要时忽略。动手练习形状体系定义Shape抽象基类要求子类实现area()和perimeter()然后实现Circle、Rectangle、Triangle三个子类。写一个函数print_shape_info(shape)接收任意形状打印它的面积和周长。员工管理系统定义Employee基类Developer和Manager子类。Manager 可以管理一组 Developer。实现统一的calculate_bonus()方法不同子类不同计算方式体现多态。自定义序列实现一个Range类行为类似内置range()支持__len__、__getitem__、__iter__、__contains__方法。用组合重构一段继承代码设计一个游戏角色系统——不用继承不要Warrior(GameCharacter)全部用组合GameCharacter包含Weapon、Armor、Skill等对象。让角色可以装备不同武器、护甲每种装备有不同的攻击/防御加成。体验一下组合的灵活性——给角色换装备不需要新写一个类只需要换掉组合的对象。实现一个简单的观察者模式定义EventManager类管理事件订阅和通知EventListener基类定义on_event(event)方法然后创建具体的监听器子类。这个模式在 GUI 编程、消息系统、发布-订阅架构中无处不在。虽然 Python 有专门的设计模式库但自己动手实现一次会让你对多态的理解上一个台阶。MRO 追踪写一个多重继承的场景——class D(B, C)而B和C都继承自A。给每个类添加一个同名方法在所有方法里都加print(ClassName)然后用D()调用这个方法。观察输出顺序然后用print(D.__mro__)验证你的观察。这个菱形继承问题是理解 MRO 最好的实验。继承的边界——为什么组合优于继承是面向对象的黄金法则很多初学者学会继承之后就开始疯狂地构建继承树——Animal→Mammal→Dog→GoldenRetriever。这种生物分类学式的设计在教科书里看起来很合理但在实际项目中往往演变成一场维护灾难。因为需求会变——原来以为所有动物都会叫后来发现鱼不叫原来以为所有鸟都会飞后来发现企鹅不会飞。每次需求变化你都要小心翼翼地修改基类生怕影响所有子类。Go 语言甚至完全没有继承的概念——它用接口和组合来替代并且设计得非常成功。Python 支持继承但 Python 社区的经验法则是能用组合解决的问题绝对不要用继承只有当你确信子类在概念上是父类的一种且这个关系几乎不可能变化时才用继承。组合让类之间通过拥有一个对象来协作耦合度更低更灵活——你可以随时替换组合的对象而不影响现有接口。这句话你现在可能只是一个模糊的印象。但当你未来在设计一个复杂系统时如果发现自己在画继承树画到第三层时开始犹豫——那就停下来试试组合。你会庆幸自己做了这个决定。要点回顾继承让你把共同代码放在基类子类通过super()复用多态让不同类通过相同的方法名协同工作——Python 靠鸭子类型实现封装用_约定和__名称改写保护内部数据property让属性访问更灵活魔法方法让自定义类支持len()、、in、for等 Python 内置操作抽象基类定义必须实现的接口适合框架和插件设计组合“有一个”往往比继承“是一种”更灵活下篇预告下一篇我们进入迭代器和生成器——学会写惰性求值、内存友好的 Python 代码。
14_Python面向对象进阶:继承、多态、封装与魔法方法
发布时间:2026/7/12 23:24:56
Python面向对象进阶继承、多态、封装与魔法方法上一篇我们学会了用类来封装数据和操作。这一篇是面向对象的进阶篇——我们将学习三个核心概念继承、多态、封装和更多能让你的类用起来像 Python 原生类型的魔法方法。这些概念听起来可能有些抽象但它们解决的都是真实的编程问题。继承解决十几类相似但不同的对象如何共用代码多态解决不同的对象如何用同一个接口来操作封装解决怎样保护数据不被随意修改。当你需要用面向对象的方式写一个中等规模的项目时这三个概念会自然地从纸面跳到你的手指。一、继承与多态继承复用代码的最强方式问题场景假设你在写一个学校管理系统。有学生、有老师、有行政人员。他们有一些共同的属性都有姓名、年龄、ID但又有各自特有的属性和行为。如果给每种人都写一个独立的类name和age的初始化代码要在三个类里重复三次——我们又回到了被复制粘贴支配的恐惧。继承就是来解决这个问题的把共同的属性和方法放在一个父类里让子类来继承它。# 父类存放共同的属性和方法classPerson:def__init__(self,name,age,person_id):self.namename self.ageage self.person_idperson_iddefintroduce(self):returnf我叫{self.name}{self.age}岁# 子类继承 Person再加上自己特有的东西classStudent(Person):def__init__(self,name,age,student_id,major):super().__init__(name,age,student_id)# 调父类的 __init__self.majormajordefstudy(self):returnf{self.name}正在学习{self.major}classTeacher(Person):def__init__(self,name,age,teacher_id,subject):super().__init__(name,age,teacher_id)self.subjectsubjectdefteach(self):returnf{self.name}老师正在教{self.subject}# 使用sStudent(小明,20,S001,计算机科学)tTeacher(张老师,35,T001,Python程序设计)print(s.introduce())# 我叫小明20岁 ——继承自 Personprint(s.study())# 小明 正在学习 计算机科学 ——Student 自己的方法print(t.introduce())# 我叫张老师35岁 ——继承自 Personprint(t.teach())# 张老师 老师正在教 Python程序设计 ——Teacher 自己的方法super().__init__(...)调用了父类Person的初始化方法。super()是父类的意思——你不用硬编码父类名Python 会自动找到正确的父类。子类继承了父类的所有方法在这里是introduce()同时可以添加自己特有的方法study()、teach()也可以重写父类的方法来改变行为。重写父类方法classStudent(Person):defintroduce(self):# 完全重写——不再调用父类的 introducereturnf学生{self.name}{self.age}岁专业{self.major}sStudent(小明,20,S001,CS)print(s.introduce())# 学生 小明20岁专业 CSPython 在调用s.introduce()时先从Student类本身查找introduce方法——找到了就直接用。如果没找到再去父类Person里找。这就是方法解析顺序MRO的基本规则。isinstance 和 issubclasssStudent(小明,20,S001,CS)print(isinstance(s,Student))# Trueprint(isinstance(s,Person))# True ——子类实例也是父类实例print(isinstance(s,object))# True ——所有类最终都继承自 objectprint(issubclass(Student,Person))# Trueprint(issubclass(Student,object))# Trueprint(issubclass(Person,Student))# False这个特性非常重要——它意味着任何接受Person参数的函数也能接受Student和Teacher。这是多态的基础。图14-1 继承结构Person 作为基类Student 和 Teacher 继承 Person子类拥有父类的属性和方法并可以添加自己特有的内容。多态同一个接口不同的行为多态Polymorphism这个词听起来很吓人但它的意思其实很简单不同类型的对象可以响应同一个方法名各自做出不同的行为。classDog:defspeak(self):return汪汪classCat:defspeak(self):return喵喵classDuck:defspeak(self):return嘎嘎# 多态不管什么动物都用 speak() 这个统一接口defanimal_chorus(animals):foranimalinanimals:print(animal.speak())animal_chorus([Dog(),Cat(),Duck(),Dog()])# 汪汪# 喵喵# 嘎嘎# 汪汪注意Dog、Cat、Duck之间没有任何继承关系——它们只是都定义了speak()方法。在 Python 里多态不依赖继承这和其他静态语言不同它依赖的是鸭子类型——“如果它走路像鸭子叫起来像鸭子那它就是鸭子”。鸭子类型defprint_report(reportable):任何有 .report() 方法的对象都能传进来print(reportable.report())classStudent:defreport(self):return学生报告...classClassroom:defreport(self):return班级报告...classFinancialReport:defreport(self):return财务报告...# 这三个类毫无关系但都能传入 print_reportprint_report(Student())print_report(Classroom())print_report(FinancialReport())Python 不关心你是什么类型——它只关心你有没有我需要的方法。这种设计哲学让 Python 的代码非常灵活也是它易于编写测试和快速原型的原因之一。图14-2 多态调用同一方法 get_role() 在不同类上有不同实现调用方无需关心具体类型只关心对象有没有这个方法。二、封装与性能封装再深入私有属性和属性装饰器双下划线——名称改写单下划线_balance是约定。双下划线__balance是强制执行classBankAccount:def__init__(self,owner,balance):self.ownerowner self.__balancebalance# 双下划线名称改写defget_balance(self):returnself.__balance accountBankAccount(小明,1000)# print(account.__balance) # AttributeError真的不能直接访问了print(account._BankAccount__balance)# 但其实可以通过改写后的名字访问...双下划线会触发 Python 的名称改写机制——__balance被改写成了_BankAccount__balance。这提供了某种程度的强制隔离但它不是真正的访问控制不像 Java 的private。Python 社区倾向于信任开发者——用单下划线约定请不要碰而不是筑墙你不能碰。property让方法看起来像属性classTemperature:def__init__(self,celsius0):self._celsiuscelsiuspropertydefcelsius(self):获取摄氏温度returnself._celsiuscelsius.setterdefcelsius(self,value):设置摄氏温度带校验ifvalue-273.15:raiseValueError(温度不能低于绝对零度)self._celsiusvaluepropertydeffahrenheit(self):华氏温度只读根据摄氏计算returnself._celsius*9/532tempTemperature(37)print(temp.celsius)# 37 ——像读属性一样print(temp.fahrenheit)# 98.6 ——自动计算temp.celsius100# 像写属性一样自动走校验逻辑# temp.celsius -500 # ValueError# temp.fahrenheit 200 # AttributeError ——只读property最优雅的地方在于它让你在保证接口不变的前提下升级内部实现。一开始你可能直接把数据存成属性后来发现需要加校验时加一个property就行——外部调用代码一行不用改。这个特性在大型项目重构中价值连城。__slots__——当你需要节省内存时默认情况下每个 Python 对象都有一个__dict__字典来存储它的实例属性。字典的优点是灵活——你可以在运行时随时给对象添加新属性。但代价是每个对象占用的内存比必要的大。如果你要创建数百万个小对象这个额外开销会很显著。__slots__让你预先声明一个对象可以有哪些属性Python 就不再为该对象创建__dict__从而大幅减少内存占用classPoint:__slots__(x,y)# 只允许 x 和 y 两个属性def__init__(self,x,y):self.xx self.yy pPoint(1,2)p.x3# OK# p.z 4 # AttributeError: Point object has no attribute z对于普通应用来说__slots__不是必需品——绝大多数 Python 程序中的对象数量根本到不了需要优化的量级。但它的存在展示了 Python 设计的一个重要理念先用简单的方式写性能不够了再针对性地优化。默认的__dict__给所有对象提供了最大灵活性而__slots__给性能敏感的代码提供了逃生舱。这个先灵活后优化的哲学贯穿整个 Python 生态。更多魔法方法容器类方法——让你的类支持 len、索引和迭代classPlaylist:def__init__(self,name):self.namename self._songs[]defadd_song(self,song):self._songs.append(song)def__len__(self):len(playlist) 时调用returnlen(self._songs)def__getitem__(self,index):playlist[0] 时调用returnself._songs[index]def__setitem__(self,index,value):playlist[0] new_song 时调用self._songs[index]valuedef__contains__(self,song):song in playlist 时调用returnsonginself._songsdef__iter__(self):for song in playlist 时调用returniter(self._songs)playlistPlaylist(我的最爱)playlist.add_song(晴天)playlist.add_song(七里香)playlist.add_song(稻香)print(len(playlist))# 3 ——__len__print(playlist[0])# 晴天 ——__getitem__print(晴天inplaylist)# True ——__contains__forsonginplaylist:# __iter__print(f-{song})定义了这些方法后你的Playlist对象用起来就像一个内置的列表——len()、索引、in、for循环全部自然工作。这就是 Python 的设计哲学让你的自定义类型感觉像是语言原生的扩展。运算符重载classVector:def__init__(self,x,y):self.xx self.yydef__add__(self,other):v1 v2returnVector(self.xother.x,self.yother.y)def__sub__(self,other):v1 - v2returnVector(self.x-other.x,self.y-other.y)def__mul__(self,scalar):v * 3returnVector(self.x*scalar,self.y*scalar)def__neg__(self):-vreturnVector(-self.x,-self.y)def__abs__(self):abs(v) ——向量的模return(self.x**2self.y**2)**0.5def__bool__(self):bool(v) ——是否为非零向量returnself.x!0orself.y!0def__eq__(self,other):v1 v2returnself.xother.xandself.yother.ydef__repr__(self):returnfVector({self.x},{self.y})v1Vector(3,4)v2Vector(1,2)v3v1v2# Vector(4, 6) ——__add__v4v1-v2# Vector(2, 2) ——__sub__v5v1*3# Vector(9, 12) ——__mul__print(abs(v1))# 5.0 ——__abs__print(bool(v1))# True ——__bool__print(bool(Vector(0,0)))# False三、设计原则与实战多态的实际价值——一个让你少写一半代码的例子多态的价值在被动的阅读中很难感受到需要亲手维护过相似但不完全相同的代码才能体会。假设你在写报表生成功能——用户可以选择 PDF、Excel、HTML 三种格式输出。没有多态时你的代码会是这样ifformatpdf:export_pdf(data)elifformatexcel:export_excel(data)elifformathtml:export_html(data)每当增加一种新格式比如 CSV你需要在所有相关位置加一个新的elif。这看起来不算太糟但当一个系统里有十几个这样的按类型分派的地方时——格式选择、权限判断、地区差异、币种转换——你会发现自己像一个永远在修补屋顶的工人这边的漏洞刚补上那边又开始漏水。多态的解决方案定义一个统一的Exporter接口PdfExporter、ExcelExporter、HtmlExporter都实现export(data)方法。主逻辑里只需要exporter.export(data)——不管是哪种格式调用方式一模一样。增加新格式时新增一个类不需要修改任何已有的调用代码。这就是多态的核心价值——让系统对扩展开放对修改封闭。调用者不需要知道被调用者的具体类型只需要知道它提供了我需要的那个方法。这种设计在大型系统中是无可替代的。属性装饰器深入——当你需要比 getter/setter 更复杂的行为property不只是让方法看起来像属性这么简单。它的真正威力在于它让你在不改变外部接口的前提下对属性的读写行为做任何你想要的增强。比如你想知道一个对象被创建了多少次、某个属性被访问了多少次——在property里加一个计数器外部代码一行不用改。再比如你想给属性值做缓存——第一次访问时计算并存储后续访问直接返回缓存值。这在数据库查询、网络请求、复杂计算等场景中可以节省大量重复开销。Python 社区不鼓励像 Java 那样为每个属性都写 getter 和 setter。在 Python 里属性直接暴露通常是 OK 的——如果以后需要加逻辑用property无缝升级外部调用代码不受影响。这种先简单后增强的渐进式设计是 Python 哲学的重要组成部分。抽象基类定义必须实现的接口有时候你想定义这个类的子类必须实现某些方法否则不应该被实例化。用abc模块fromabcimportABC,abstractmethodclassAnimal(ABC):abstractmethoddefspeak(self):子类必须实现这个方法passclassDog(Animal):defspeak(self):return汪汪classCat(Animal):defspeak(self):return喵喵# animal Animal() # TypeError不能实例化抽象类dogDog()# OK实现了 speak()print(dog.speak())# 汪汪抽象基类在你设计框架、定义插件接口、或者确保团队中的其他开发者遵循约定时非常有用。但 Python 的鸭子类型在很多场景下比抽象基类更灵活——“必须继承换成了有这个方法就行”。多重继承和 MROPython 支持一个类同时继承多个父类classFlyable:deffly(self):return正在飞行classSwimmable:defswim(self):return正在游泳classDuck(Flyable,Swimmable):passduckDuck()print(duck.fly())# 正在飞行print(duck.swim())# 正在游泳如果多个父类有同名方法Python 使用C3 线性化算法来决定调用顺序。你可以通过Duck.__mro__查看方法解析顺序。多重继承是强大的工具但也是代码复杂度的来源——能用组合把一个对象作为另一个对象的属性解决的问题尽量不用多重继承。图14-3 面向对象三大特性继承复用代码、多态统一接口、封装隐藏细节在实际代码中的协同关系。组合优于继承这是面向对象设计里最有名的一条原则。继承表达的是是一个is-a关系——Student是一种Person。而很多场景其实是有一个has-a关系——Car有一个Engine而不是Car是一种Engine。# 用组合代替继承classEngine:defstart(self):return引擎启动defstop(self):return引擎停止classCar:def__init__(self):self.engineEngine()# Car 有一个 Engine而不是 Car 是一种 Enginedefdrive(self):returnf{self.engine.start()}出发carCar()print(car.drive())# 引擎启动出发当你发现自己为了复用几行代码而构建了一个深层继承树停下来想一想——是不是用组合更好组合让代码的耦合度更低每个类之间是合作关系而非血缘关系。面向对象的设计原则——SOLID 简介SOLID 是面向对象设计的五个核心原则的首字母缩写。你不需要现在就完全掌握它们但了解它们的名字和一句话描述会给你未来读源码和做设计时一个有用的思维框架单一职责一个类只做一件事、开放封闭对扩展开放对修改封闭——本文的多态就是最好的例子、里氏替换子类应该能完全替代父类而不引起问题、接口隔离不要强迫使用者依赖它不需要的方法、依赖反转依赖抽象而不是具体实现。每当你为一个设计决策而犹豫时回头想想这五个原则中的哪一个可以帮你做出更好的选择。它们不是教条而是经验——是很多前辈在漫长的软件工程实践中总结出来的什么管用、什么不管用。Python 社区对 SOLID 的态度比 Java 社区宽松得多——Python 崇尚实用主义不会为了遵循原则而过度设计。所以了解 SOLID 但不要膜拜它。把它当作一个参考框架需要时取用不需要时忽略。动手练习形状体系定义Shape抽象基类要求子类实现area()和perimeter()然后实现Circle、Rectangle、Triangle三个子类。写一个函数print_shape_info(shape)接收任意形状打印它的面积和周长。员工管理系统定义Employee基类Developer和Manager子类。Manager 可以管理一组 Developer。实现统一的calculate_bonus()方法不同子类不同计算方式体现多态。自定义序列实现一个Range类行为类似内置range()支持__len__、__getitem__、__iter__、__contains__方法。用组合重构一段继承代码设计一个游戏角色系统——不用继承不要Warrior(GameCharacter)全部用组合GameCharacter包含Weapon、Armor、Skill等对象。让角色可以装备不同武器、护甲每种装备有不同的攻击/防御加成。体验一下组合的灵活性——给角色换装备不需要新写一个类只需要换掉组合的对象。实现一个简单的观察者模式定义EventManager类管理事件订阅和通知EventListener基类定义on_event(event)方法然后创建具体的监听器子类。这个模式在 GUI 编程、消息系统、发布-订阅架构中无处不在。虽然 Python 有专门的设计模式库但自己动手实现一次会让你对多态的理解上一个台阶。MRO 追踪写一个多重继承的场景——class D(B, C)而B和C都继承自A。给每个类添加一个同名方法在所有方法里都加print(ClassName)然后用D()调用这个方法。观察输出顺序然后用print(D.__mro__)验证你的观察。这个菱形继承问题是理解 MRO 最好的实验。继承的边界——为什么组合优于继承是面向对象的黄金法则很多初学者学会继承之后就开始疯狂地构建继承树——Animal→Mammal→Dog→GoldenRetriever。这种生物分类学式的设计在教科书里看起来很合理但在实际项目中往往演变成一场维护灾难。因为需求会变——原来以为所有动物都会叫后来发现鱼不叫原来以为所有鸟都会飞后来发现企鹅不会飞。每次需求变化你都要小心翼翼地修改基类生怕影响所有子类。Go 语言甚至完全没有继承的概念——它用接口和组合来替代并且设计得非常成功。Python 支持继承但 Python 社区的经验法则是能用组合解决的问题绝对不要用继承只有当你确信子类在概念上是父类的一种且这个关系几乎不可能变化时才用继承。组合让类之间通过拥有一个对象来协作耦合度更低更灵活——你可以随时替换组合的对象而不影响现有接口。这句话你现在可能只是一个模糊的印象。但当你未来在设计一个复杂系统时如果发现自己在画继承树画到第三层时开始犹豫——那就停下来试试组合。你会庆幸自己做了这个决定。要点回顾继承让你把共同代码放在基类子类通过super()复用多态让不同类通过相同的方法名协同工作——Python 靠鸭子类型实现封装用_约定和__名称改写保护内部数据property让属性访问更灵活魔法方法让自定义类支持len()、、in、for等 Python 内置操作抽象基类定义必须实现的接口适合框架和插件设计组合“有一个”往往比继承“是一种”更灵活下篇预告下一篇我们进入迭代器和生成器——学会写惰性求值、内存友好的 Python 代码。