ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python面向对象实战:从类、对象到封装继承多态

2026/9/30 15:39:23 拓冰建站 浏览量
Python面向对象实战:从类、对象到封装继承多态 1. 为什么说 Python 面向对象是编程路上必须跨过的一道坎说实话很多 Python 初学者学到函数就已经开始飘了觉得“编程也不过如此”结果一碰到面向对象就懵了。尤其是当你在网上搜索“Python 面向对象”的时候出来的教程要么是教科书式的概念轰炸——“封装、继承、多态”三大特性背得滚瓜烂熟但真正让你写一个项目时依然不知道从哪里下手要么就是纯语法堆砌class、def、self 全认识拼在一起就不知道什么意思了。这篇博文我想换个讲法不跟你整那些虚头巴脑的理论。咱们直接从“为什么要用面向对象”这个问题切入用装修房子、开餐厅这种生活里常见的场景来帮你建立直觉再一步一步把 Python 面向对象的每一个语法点拆开揉碎配合完整可运行的实战案例。不管你是刚学完 Python 基础的纯新手还是已经写过一阵子脚本、想提升代码组织能力的初级开发者这篇文章都能帮你在脑子里搭起一张清晰的面向对象地图。面向对象这套东西不是 Python 独有的Java、C#、C 都有。但 Python 的面向对象有个特点它很灵活甚至有点“任性”。你可以不用接口、不用访问修饰符也能写出能跑的程序。但正因为太灵活了新手反而容易迷失。所以这篇文章的基调是先建立正确的设计思维再讲 Python 的具体语法最后用实战串联起来。2. 面向对象到底在解决什么问题从“做菜流程”到“餐厅管理体系”2.1 面向过程 vs 面向对象两种看世界的角度先想一个问题假如你要写一个程序帮一家餐厅管理菜品和订单。用面向过程的思路你会怎么写可能就是定义几个列表一个存菜名一个存价格一个存库存然后写一堆函数add_dish()、remove_dish()、create_order()、calculate_total()……随着功能越来越多你会发现自己写得很累。每个函数都要记得往哪个列表里塞数据参数一多调用的时候顺序稍微记错程序就崩了。这就像你在厨房里做菜所有食材调料都堆在台面上你今天想做红烧肉得自己记得酱油在哪、冰糖在哪、五花肉买了多少。菜少还好菜一多厨房就会变成灾难现场。面向对象则是另一种思路我把菜品本身当成一个“东西”——它有名字、有价格、有库存它还会自己“报告”自己的状态。我做菜的时候不是在一堆散落的食材里翻找而是把每道菜相关的数据和操作打包在一起我需要红烧肉的时候直接从“红烧肉”这个对象身上拿信息让它自己处理自己该做的事。这就是所谓的“封装”直觉。再往深了说面向对象其实是在模仿人类认识世界的方式。现实中我们不会把“名字、价格、库存”这三个数据在脑子里分开记我们会把它们统一成一个整体概念——“菜品”。面向对象就是让你能把程序里的数据和操作这个数据的方法绑定成一个整体。这就是“类”和“对象”最朴素的意义。2.2 类 vs 对象不是“鸡生蛋”的问题是“图纸与房子”的问题这两个词在教科书里被讲得玄乎其玄其实就是一句话类是设计图纸对象是依据图纸造出来的具体房子。图纸上写着“房子有四堵墙、一个屋顶、若干窗户”——这是类的定义。按照这张图纸盖出来的每一栋具体的房子——比如“我家那栋朝南的三居室”——就是对象。你可以照着同一张图纸盖很多栋房子它们结构相同但具体住的人、里面的装修、朝向可能各不相同。对应到代码里类的定义就是声明“菜品这个东西有哪些属性菜名、价格、库存、有哪些行为描述自己、检查库存”而对象就是具体的某一道菜比如“宫保鸡丁定价 38 元库存 15 份”。这个区分非常重要因为很多人写代码时搞不清楚“我到底是在定义类还是在创建对象”后面就会出现把方法当函数乱调用、搞不清 self 是什么之类的问题。3. 从零搭建你的第一个 Python 类类和对象的语法细节拆解3.1 class、init、self三个核心概念一次讲透直接上代码我写一个最简单的菜品类class Dish: def __init__(self, name, price, stock): self.name name self.price price self.stock stock def describe(self): return f{self.name}价格 {self.price} 元剩余库存 {self.stock} 份 def reduce_stock(self, quantity): if quantity self.stock: raise ValueError(库存不足) self.stock - quantity然后我们创建具体的对象kung_pao Dish(宫保鸡丁, 38, 15) print(kung_pao.describe()) # 宫保鸡丁价格 38 元剩余库存 15 份这段代码虽然短但信息量很大新手最容易困惑的点全在这里面。咱们一个个说。第一init不是什么“构造函数”而更像是“初始化方法”。它的作用是在对象被创建的那一刻告诉 Python“新房子刚盖好的时候屋子里的家具应该怎么摆”。当你写Dish(宫保鸡丁, 38, 15)的时候Python 实际做的事情是先在内存里开辟一块空间然后把这块空间作为参数传给init让里面的代码把 name、price、stock 这些值填进去。第二self 到底是什么这是全球 Python 新手困惑榜单第一名。我的理解方式是这样的self 就是“正在被操作的那个对象自己”。当你调用kung_pao.describe()的时候Python 其实把这个调用翻译成了Dish.describe(kung_pao)——kung_pao 被自动填到了 describe 的第一个参数位置上。所以 self.name 就是在访问 kung_pao 这个对象自己的 name 属性而不是别的对象的。你不需要手动传 selfPython 会自动帮你传。第三为什么不把变量直接定义在类里面比如有人会想直接这样写不行吗class Dish: name 宫保鸡丁 price 38写当然能写但这叫“类属性”它属于类本身而不是某个具体的对象。所有通过这个类创建的对象如果不做特殊处理都会共享这两个值。也就是说饭店里所有菜都叫宫保鸡丁、都卖 38 元这显然不符合现实。用init里通过 self.xxx 赋值的才叫“实例属性”每个对象有自己独立的一份。这个区别建议画个图理解——类属性是贴在大门口的公告实例属性是贴在每间房门口的住户牌。3.2 方法的种类实例方法、类方法、静态方法到底怎么选在 Python 的类里面你能定义的“函数”其实有三种它们的用法和适用场景截然不同。新手经常搞混我就见过不少人把不需要 self 的函数硬写成实例方法然后在调用的时候被参数个数搞崩溃。实例方法是默认的形式第一个参数是 self。它操作的是具体对象的数据比如上面的 describe 和 reduce_stock。绝大多数场景下你写的方法都应该是实例方法。什么时候不需要它如果这个方法根本不需要访问对象的任何属性那它就不该是实例方法。类方法用classmethod装饰第一个参数是 cls代表类本身。它操作的是类级别的数据或者作为“备用构造函数”。比如你想要一种特殊方式创建菜品对象——从字典创建class Dish: def __init__(self, name, price, stock): self.name name self.price price self.stock stock classmethod def from_dict(cls, data): return cls(data[name], data[price], data[stock])调用方式dish_data {name: 麻婆豆腐, price: 28, stock: 20}然后d Dish.from_dict(dish_data)。这里 cls 传的其实是 Dish 这个类本身。好处是如果以后 Dish 被继承成 SpicyDish这个类方法依然能正确创建 SpicyDish 的对象。静态方法用staticmethod装饰第一个参数不需要 self 也不需要 cls基本就是一个放在类里面的普通函数。它存在的意义通常是为了“逻辑归类”——这个方法跟这个类有关系但它不依赖类和实例的任何数据。比如class Dish: staticmethod def is_valid_price(price): return price 0调用方式Dish.is_valid_price(38)。你在类外部定义一个普通函数其实也能实现同样的功能放到类里面更多是为了让代码的归属关系更清晰。什么时候用哪个我总结成一句话要访问对象数据就用自己的实例数据就用实例方法要创建对象或者操作类本身的公共数据就用类方法只是沾点边但啥都不碰就放静态方法。3.3 属性的可见性和 property别再傻傻地直接操作对象内部数据很多人学完init、self 之后会觉得“面向对象也不过如此”——属性直接赋值方法直接调用完事。但这样写代码项目一变大就会出问题。比如你直接让外部代码随意改对象的 stock那谁能保证它不会把库存改成负数这时候就需要用到“属性可见性”和 property 这套机制。Python 没有 Java 那种强制的 public / private 关键字约定俗成是用“下划线规则”单下划线开头的属性比如self._stock表示“这是内部数据外部最好不要直接碰”双下划线开头比如self.__stock则会被 Python 做名称改编实际上变成了_Dish__stock实现一定程度的私有保护。但说实话双下划线这个机制主要用来避免命名冲突真要防君子不防小人靠的还是约定。真正实用的做法是用property装饰器把“取值”和“赋值”封装成方法让外部代码用起来像在直接访问属性一样。比如给 Dish 加一个 stock 的保护逻辑class Dish: def __init__(self, name, price, stock): self._name name self._price price self._stock stock property def stock(self): return self._stock stock.setter def stock(self, value): if value 0: raise ValueError(库存不能为负数) self._stock value这样写有什么好处第一你可以在赋值的时候做校验负库存根本塞不进去。第二以后库存逻辑变了比如要同步通知某个系统你只需改 setter 里的代码外部调用方式完全不用变。这就是“封装”的实战意义——不是藏着掖着而是提供一个受控的入口。注意property 的 setter 方法名必须和 property 装饰的方法名保持一致比如这里都是stock。很多人第一次写会把 setter 名字写成别的结果直接报错。4. 继承与多态让代码“长”出来而不是“改”出来4.1 当两个类高度相似时你的直觉应该是“抽出公共部分”回到餐厅的场景。现在你不光有普通菜品还有“辣味菜品”。辣味菜品除了具有普通菜品的一切属性之外还多了一个“辣度等级”。面向过程的做法是把 Dish 类的代码复制一份改个类名加几个新属性再改改方法——这确实是最快的写法但也是最蠢的。因为你以后改价格逻辑的时候要记得同时改两个地方改漏一个就会出 bug。用继承来解决这个问题class SpicyDish(Dish): def __init__(self, name, price, stock, spice_level): super().__init__(name, price, stock) self.spice_level spice_level def describe(self): base super().describe() return f{base}辣度等级 {self.spice_level} 星这里有几个细节值得仔细说。第一super().init到底是什么意思它是在调用父类 Dish 的初始化方法把 name、price、stock 这三个属性的初始化活儿交给父类去做子类只管处理自己新增加的 spice_level。这样做的好处是以后父类的初始化逻辑变了比如增加了食材清单属性子类不需要改动就能自动继承新的初始化行为。第二方法重写override是怎么发生的SpicyDish 里重新定义了一个同名 describePython 在调用的时候会优先使用子类的版本这就是“多态”的雏形。你不需要关心一个对象到底是 Dish 还是 SpicyDish只要调用 describe()它就会自动找到“最适合”的那个方法。这种“不看对象类型只看对象能力”的设计思路真的会让代码变得非常优雅。第三类什么时候适合继承一个非常实用的判断标准子类在概念上一定是一个“父类的特殊形态”。SpicyDish 是一个 Dish所以它可以继承 Dish。如果你为了复用几个函数硬把完全不相干的类凑成继承关系那代码只会变得莫名其妙。现实中我也见过不少反面案例——有人把 User 和 Order 搞成继承关系就因为两者都有一部分相同的字段结果后面改需求的时候改得痛不欲生。遇到这种情况更合理的做法可能是“组合”让 Order 类里持有一个 User 对象的引用而不是让 Order 继承 User。4.2 多态的精髓用“同一句话”驱动“不同的行为”教科书里最爱讲多态但例子永远是“猫叫喵喵、狗叫汪汪”让人感觉多态就是个花架子。我换一个更贴近实战的场景你的餐厅需要给不同类型的菜品计算“促销价”——普通菜品打九折辣味菜品因为是特色菜打八五折但不参与满减。如果每个菜品搞一个 if/else 来判断类型你每加一种新类型就要改一次这个函数。用多态就能完美避开这个问题。在 Dish 类里定义一个get_promo_price()默认实现是九折class Dish: def get_promo_price(self): return self.price * 0.9在 SpicyDish 里重写这个逻辑class SpicyDish(Dish): def get_promo_price(self): return self.price * 0.85然后你只需要写一个统一的函数def show_promo_price(dish): print(f{dish.name} 促销价 {dish.get_promo_price():.2f} 元)不管你传进来的是 Dish 对象还是 SpicyDish 对象show_promo_price 都能正确工作。如果以后又加了“凉菜类”你只需要新增一个类并重写 get_promo_price调用方一行代码都不用改。这就是多态最大的价值把变化封闭在单个类内部让系统整体保持稳定。4.3 多重继承Python 的特色之一但也是陷阱之一Python 支持一个类同时继承多个父类比如你可以定义一个“即辣又甜”的怪味菜品同时继承 SpicyDish 和 SweetDish。这个功能很强大但新手容易踩坑。最大的坑是“菱形继承问题”——如果两个父类有同名的方法子类到底该用哪个Python 通过 MRO方法解析顺序算法来解决你可以在子类上调用ClassName.__mro__查看方法查找的先后顺序。我个人的建议是除非对这个机制有透彻理解否则尽量别碰多重继承。90% 的多重继承场景都可以通过组合或者混入类Mixin来优雅解决没必要给自己挖坑。5. 特殊方法让对象兼容 Python 的“原生语法”5.1str、repr、eq打印、显示、比较全靠它们Python 的类里有很多以双下划线开头和结尾的“特殊方法”它们的作用是让对象能无缝融入 Python 的语言特性。说白了你写的对象应该“表现得像原生类型一样”。拿打印来说。初学者直接 print 一个对象看到的是__main__.Dish object at 0x7f...这是 Python 默认的对象内存地址表示。如果想让它打印得好看就实现__str__def __str__(self): return self.describe()刚才是print(kung_pao.describe())手动调用现在你只需要print(kung_pao)就能得到一样的结果。__repr__和__str__的区别简单说repr面对的是开发人员目标是“无歧义”最好能表示出对象的类型和关键状态str面对的是一般用户目标是“可读易懂”。在交互式环境比如 Jupyter里直接敲变量名显示的就是repr的结果。再说__eq__。默认情况下两个对象只要在内存里不是同一个用比较就返回 False。但现实里如果两道菜名字一样我们就认为它们是同一道菜该怎么办这样写def __eq__(self, other): if not isinstance(other, Dish): return NotImplemented return self.name other.name这样两个不同对象只要名字相同就返回 True。这在去重、排序、集合操作里特别实用。5.2len、getitem、iter让对象像列表一样被使用如果菜品类里要管理一个“菜品列表”你当然可以用 Python 原生的 list但如果你想做更多自定义逻辑可以自己实现一个 DishCollection 类并实现len和getitem这样它就可以直接支持len()函数操作和索引访问collection[0]。更进阶的实现__iter__之后这个对象就可以直接放进 for 循环里遍历。这一整套设计思想Python 官方称之为“鸭子类型”——如果一个类实现了列表该有的那些方法那它就可以被当作列表来用而不需要真的继承 list。这也是 Python 面向对象灵活性的集中体现关键不在于你是什么而在于你能做什么。6. 综合实战用面向对象设计一个菜品管理系统说了这么多概念如果不落一次地基本等于白看。我在这里用一个相对完整的实战项目把前面讲过的知识点串一遍。需求很简单做一个命令行版的菜品管理系统支持添加菜品、查看所有菜品、创建订单、展示所有订单。先定义最底层的实体类。菜品和订单各自管好自己class Dish: def __init__(self, name, price, stock): self.name name self.price price self.stock stock def reduce_stock(self, quantity): if quantity self.stock: raise ValueError(f「{self.name}」库存不足当前库存 {self.stock}) self.stock - quantity def __str__(self): return f「{self.name}」 {self.price} 库存 {self.stock} class Order: def __init__(self, order_id, dish, quantity): self.order_id order_id self.dish dish self.quantity quantity self.total_price dish.price * quantity def __str__(self): return f订单 {self.order_id}: {self.dish.name} x {self.quantity}总价 {self.total_price} 元再定义管理类负责统筹所有的菜品和订单。为什么还需要管理类因为菜品和订单是“实体”而系统逻辑比如创建订单时扣减库存、生成订单编号需要一个服务层来承接。这就是面向对象设计里常说的“职责分离”class RestaurantSystem: def __init__(self): self.dishes [] self.orders [] self._next_order_id 1 def add_dish(self, name, price, stock): dish Dish(name, price, stock) self.dishes.append(dish) print(f已添加菜品: {dish}) def list_dishes(self): for i, dish in enumerate(self.dishes, start1): print(f{i}. {dish}) def create_order(self, dish_index, quantity): dish self.dishes[dish_index - 1] dish.reduce_stock(quantity) order Order(self._next_order_id, dish, quantity) self.orders.append(order) self._next_order_id 1 print(f下单成功: {order}) def list_orders(self): for order in self.orders: print(order)接着是主程序入口def main(): system RestaurantSystem() while True: print(\n1. 添加菜品) print(2. 查看菜品) print(3. 创建订单) print(4. 查看订单) print(5. 退出) choice input(请选择操作: ) if choice 1: name input(菜名: ) price float(input(价格: )) stock int(input(库存: )) system.add_dish(name, price, stock) elif choice 2: system.list_dishes() elif choice 3: system.list_dishes() idx int(input(请输入菜品编号: )) qty int(input(请输入数量: )) try: system.create_order(idx, qty) except ValueError as e: print(e) elif choice 4: system.list_orders() elif choice 5: print(感谢使用) break if __name__ __main__: main()这个系统麻雀虽小五脏俱全。它有三个不同的类每个类各有明确的职责对象之间有清晰的协作关系——RestaurantSystem 持有了 Dish 和 Order 对象的引用订单创建时会调用 Dish 的库存扣减方法这就是对象与对象之间的“消息传递”。你现在把这段代码跑起来试着添加几个菜、下几单再想想如果用面向过程该怎么写对比一下代码的可读性和后续扩展性很多事情就通了。更进一步你可以试着给 Dish 添加__lt__小于比较让菜品按价格排序或者把 Dish 变成数据类使用dataclass装饰器这些在此刻已经不难了。7. 新手最容易踩的坑和排查思路实录7.1 卡住九成新手的“Missing self”问题这个报错信息我见得太太太多了TypeError: describe() takes 1 positional argument but 2 were given。什么意思呢你定义了一个方法describe(self)按理说它只接收一个参数 self但 Python 却说“给了 2 个参数”。根本原因是你创建对象的时候把括号写漏了然后直接把类当对象用了比如写了kung_pao Dish然后调用kung_pao.describe()。这时候 kung_pao 是类本身而不是对象调用 describe 的时候 Python 会自动把 kung_pao 当作 self 传进去所以你其实传了“类 手动填的参数”两个东西自然报错。排查思路很简单看报错的是实例还是类检查创建对象时有没有写括号。7.2 父类初始化忘调用的连锁反应子类里重写了init但忘了调用super().__init__()虽然不会直接报错但继承过来的那些属性就根本没有被初始化。等你要访问 name 的时候蹦出来一个AttributeError: SpicyDish object has no attribute name。这个错也特别常见。我的建议是在写子类的init时第一行就写 super().init(...)形成肌肉记忆。想想看父类帮你把杂事都办好了你何必再自己重新做一遍7.3 可变默认参数这个经典大坑在类的方法定义里如果默认参数用了可变对象比如def __init__(self, name, ingredients[]): self.ingredients ingredients这个列表只会被创建一次所有不传 ingredients 的对象会共享同一个列表。某个对象往里面添加了东西其他对象也会“看到”。这是 Python 语言层面的经典陷阱解决办法是改写成ingredientsNone然后在方法内部判断并新建列表。一定要在心里刻下这句话不要用可变对象作为默认参数值。7.4 调试面向对象代码的万能方法vars 与 dir在调试的时候很多人只会 print 整个对象得到的永远是一串内存地址。我在这里分享两个调试利器。第一个是内置函数vars(obj)它能把对象的所有实例属性以字典形式返回一眼看清当前状态。比如print(vars(kung_pao))会输出{name: 宫保鸡丁, price: 38, stock: 15}。第二个是dir(obj)它列出对象的所有属性和方法用来排查“为什么我调用这个方法找不到”特别好用。这两个函数在什么时候有用就是当你觉得“我的代码逻辑明明没问题”但程序行为就是不对的时候先看对象里的数据是不是跟想象中一样往往能瞬间发现问题。8. 面向对象设计的一些实用心得这篇文章光是语法细节已经写得很长了但在收尾之前我还想结合这些年写 Python 的实际经验分享几条方向性的建议这也是任何一本语法书里都不会直接告诉你的事。第一不要为了面向对象而面向对象。如果你的程序只是一个几十行的数据转换脚本全程用函数完全没问题。强行套一层 class只会让代码看起来更臃肿。判断标准很简单你写代码时有没有反复在多个函数之间传递同一组相关的数据如果有那就该考虑用类把它们包起来如果只是流程化的调用函数就够用了。第二类的大小要克制。一个类一旦超过几百行基本就该拆分了。我看到很多新手喜欢把所有逻辑都塞到一个类里结果那个类变成了一个巨大的“上帝对象”谁都要依赖它改哪都会出事。记住一个原则类应该是一个清晰、单一的概念而不是一个大杂烩。如果一个类同时要管数据库、管订单、还要管界面展示把它拆成三个类才是正道。第三先写需求再画类图最后写代码。我在带新人的时候发现很多人拿到需求第一件事就是打开编辑器开始写 class。其实正确的顺序应该是先把需求里出现的名词圈出来这些名词大概率就是类再把动词找出来这些动词大概率就是方法最后再看名词和动词之间的关系决定是继承、组合还是调用。我举一个例子需求里出现“凉菜”“热菜”“汤”这些词很可能需要把 Dish 作为基类再派生出几个子类出现“下单”“取消订单”“结账”这些动作大概率要放进 RestaurantSystem 或者 OrderService 里。这套流程走得越熟练你设计出来的类就越自然。第四阅读优秀的源代码是提升面向对象设计能力最快的捷径。比如走一遍 requests、flask 这类优秀开源项目的源码去看它们是怎么组织类的。不用全读懂光是感受“哦原来这个功能是单独抽出一个 Mixin 类来处理的”“原来这个上下文管理器是实现了enter和exit”就比你闷头自己琢磨强一百倍。到这里Python 面向对象的核心体系已经给你搭起来了从类和对象的直觉认知到 init、self 的硬核细节再到封装、继承、多态三大特性的实战运用最后通过一个完整的菜品管理系统把知识串了一遍。如果你跟着文章把代码敲了一遍我建议你再做几个小扩展试试给菜品加一个分类再给订单加一个时间字段然后实现按时间排序的功能——这些平时遇到的需求正好是你检验面向对象掌握程度的试金石。