Python面向对象编程从入门到实战:类、封装、继承与多态详解

发布时间:2026/9/12 6:07:17
Python面向对象编程从入门到实战:类、封装、继承与多态详解 干了这么多年Python要说哪个知识点是新手从入门到进阶路上最容易卡壳的我第一个投票给“面向对象”。说起来奇怪基础语法学得好好的循环、判断、函数都挺顺一到类class就开始懵。网上教程也不少但大部分要么堆概念要么贴一段带注释的代码让你自己品品完还是只会照着抄换了个场景照样不会设计。这篇文章我就用自己实际写项目时的理解方式把Python面向对象整个拆开讲。不讲那些飘在空中的理论而是告诉你每个概念到底解决什么问题、代码为什么这样写、实际项目里怎么用。学完之后你不仅能看懂别人代码里的类还能自己动手设计出结构清晰的类真正把“面向对象”这四个字变成顺手就用的工具。1. 面向对象与面向过程从底层思维差异说起1.1 面向过程像写菜谱一样写代码在聊面向对象之前得先把面向过程这玩意儿看清楚。Python既能写面向过程也能写面向对象甚至很多人前期写的代码名义上是Python实际上完全是面向过程的思路。什么是面向过程就四个字按步照搬。像一本菜谱先放油、再放葱姜蒜、下主料、加调料、出锅装盘每一步操作都是对数据的直接加工。我举一个特别贴近的例子。你写一个学生成绩管理系统面向过程的思路会是这样的# 面向过程风格用函数全局数据结构 students [] def add_student(name, score): students.append({name: name, score: score}) def print_students(): for s in students: print(s[name], s[score]) add_student(张三, 90) add_student(李四, 85) print_students()这种写法最大的特点就是“数据”和“操作数据的行为”是分离的。数据躺在students这个全局列表里操作数据的函数在外部定义每个函数要处理数据时就把它作为参数传进去。这种做法在脚本、小工具、数据分析脚本里其实完全够用没什么不好。但我做了几年项目之后发现一旦业务逻辑变复杂这种写法的维护成本会直线上升。问题出在哪首先是数据和行为分离之后代码的“内聚性”变差了。比如你要加一个功能计算全班平均分那你就得再写一个函数把students传进去遍历。当这种操作越来越多函数也越来越多全局变量也越来越多你很难搞清楚一个数据到底被多少地方修改过。全局变量一旦被某个函数意外修改排查起来非常痛苦。这种经历我相信做过稍微大一点项目的人都懂深夜加班找一个“没有被正确初始化的状态”是真的很折磨人。另一个问题就是数据和行为的割裂导致代码的“复用性”变差。你定义的学生数据结构如果换到另一个考试系统里去用那套函数基本全部要重写。因为你复用的是“函数逻辑”而不是“一个完整的东西”。1.2 面向对象像经营公司一样组织代码面向对象解决的正是上面说的这两个痛点。它的核心思路很简单把相关的数据和对这些数据的操作打包在一起形成一个“自治”的整体。这个整体在Python里就叫“对象”。我常说一句话面向对象并不是什么高深莫测的编程范式它的本质更像是在模拟现实世界。你看现实世界里的任何东西比如一台咖啡机它本身就是一个完整的对象——它有属性水箱水位、温度、豆仓余量它有行为萃取咖啡、打奶泡、自动清洗。你不需要在外部写一堆函数去操作咖啡机的水箱和豆仓你只需要调用它提供的按钮就行。不需要关心内部构造只用调用接口。回到代码里面向对象写出来的学生管理系统是这样的class Student: def __init__(self, name, score): self.name name self.score score def show(self): print(f{self.name}: {self.score}) zhangsan Student(张三, 90) lisi Student(李四, 85) zhangsan.show() lisi.show()从这段代码你能看到Student这个类把“姓名”“成绩”这两个数据和“展示信息”这个行为全部收拢在了一起。之后你拿到任何一个Student对象你想知道怎么展示它直接调用.show()就行不用到处找工具函数。数据不再是“裸奔”的它和你对它的操作绑定在了一起这就是“封装”的雏形。实际项目里这种组织方式带来的好处特别明显。你在代码里看到order Order(id1001, user_id22, total199.0)然后调用order.pay()哪怕你完全不知道Order类内部怎么实现的你都能猜到这段代码在做“给订单付款”。这种可读性是面向过程写法很难达到的。代码的阅读成本变低了维护起来自然轻松。1.3 什么时候该用面向对象这里要先澄清一个误区不是所有代码都得套上类才叫高级。我自己写脚本的时候经常大量的逻辑还是直接用函数写。你让我写个一次性解析日志的小工具我绝对用面向过程清理、提取、统计一溜函数下来完事。那什么时候该切到面向对象我的判断标准有两条。第一条看有没有“具有明确边界的实体”。比如用户、订单、商品、传感器、窗口、连接池、线程池这些都能在现实世界里找到对应是“东西”而不是“动作”。有这种实体就值得用类去建模。第二条看状态之间有没有强关联和生命周期。如果你发现很多函数都在操作同一个数据结构而且这个数据结构的状态在整个流程里反复被改变那你就应该在它的外面包一个类把这些操作收进去用方法去管理状态的变迁。我再补充一个反面信号当你写函数时发现参数列表越来越长为了让一个函数知道太多关于数据结构的细节你不得不传一堆关联参数进去。这时候你就该醒一醒了把这些参数绑定成一个类会让代码清爽得多。说白了面向对象不只是为了“像现实世界”更是为了“降低复杂系统的维护成本”。2. 类和对象从零搭建你的第一个类2.1 类是什么对象又是什么很多教程喜欢说“类就是模板对象就是模板造出来的具体实例”。这个说法方向没错但过于抽象。我更喜欢用“图纸”和“房子”来类比。类就是你脑子里关于“一栋房子”的设计图纸它定义了这栋房子要有几间卧室、几个卫生间、坐落在哪个方向、窗户开多大。对象则是依据这份图纸实际建造出来的那一栋具体的房子它真实占了地皮、有具体的门牌号。放在代码里翻译一下。你写一个class Dog:这只是定义了“狗”这个概念它告诉你一条狗都有哪些属性和方法但不占据具体内存。当你执行my_dog Dog(sizesmall)这一行时Python才真正在内存里为这只具体的狗分配了空间my_dog就是一个对象。理解这个区别非常关键因为实际的业务系统里你几乎总是“定义类”一次然后“实例化对象”无数次。比如你开发一个爬虫定义好了class NewsSpider:之后可能同时跑50个爬虫任务去抓不同站点那就是50个Spider对象。类只有一套逻辑对象却有50份状态互不干扰。另外要说一下Python里万物皆对象。你看整数、字符串、列表、字典它们其实都是对象。比如hello.upper()这个方法调用其实就是字符串对象在调用它自己的方法。你之所以没感觉是因为那些类的定义早就写好了。当你自己定义一个类时你和那些写标准库的人做的其实是同一件事只不过你的类服务于你的业务而已。2.2 __init__与self构造与对象自我的秘密新手对面向对象的第一道坎几乎都卡在__init__和self上。我们先说__init__。它的中文名叫“初始化方法”或者“构造方法”严格说构造方法是__new__但那是进阶话题先不展开。只要一个类被实例化了__init__就会被自动调用它的主要作用就是给刚诞生的对象设置初始状态。class Car: def __init__(self, brand, color白色): self.brand brand self.color color self.running False def start(self): self.running True print(f{self.color}的{self.brand}已启动)你执行my_car Car(特斯拉, 红色)内部流程是这样的Python先为这个对象开辟内存空间然后自动调用__init__(my_car, 特斯拉, 红色)把地址赋给my_car。注意这里的第一个参数self就是那个刚创建的对象本身。你写self.brand brand相当于说“把我这个对象自己的brand属性设置成传入的brand值”。那self到底能不能省略不能。Python里没有隐式的this所有的实例方法都必须显式地接收实例本身。如果你在实例方法里漏了self调用的时候就会报“takes 1 positional argument but 2 were given”之类的错误。这个错误新手一看就懵明明我只传了一个参数为什么说给了两个因为Python把你调用obj.method(arg)自动翻译成了Class.method(obj, arg)第一个参数是对象自己那自然就多了。搞清楚了这个机制你就再也不会被这个报错搞晕了。关于__init__我这里补充一条实际项目里的经验不要在__init__里做太多“重”的事情。有些人喜欢在初始化里直接连接数据库、读取配置文件、启动子线程结果类的实例化过程变得又慢又不稳定。合理的做法是让__init__只负责基本属性的赋值那些重量级操作单独写方法或者用懒加载的方式延迟到真正需要时才做。你在写类的时候一定要让“创建一个对象”这个过程保持轻量、快速、不容易失败。2.3 属性与方法的访问控制Python里并没有真正的“私有”概念这是它与C、Java非常不一样的地方。C/Java里你有一个private关键字严格限制外部访问Python里约定俗成的做法是用下划线。一个下划线_name表示“这是内部实现细节你外部别乱动但我不会阻止你”。两个下划线__name则是“名称重整”name manglingPython解释器会在内部把属性名改成_ClassName__name以此尽量避免外部直接访问。class BankAccount: def __init__(self, owner, balance): self.owner owner self.__balance balance def deposit(self, amount): if amount 0: raise ValueError(存款金额必须为正数) self.__balance amount def get_balance(self): return self.__balance acc BankAccount(小明, 1000) print(acc.__balance) # AttributeError外部直接访问会报错 print(acc.get_balance()) # 正确方式通过方法来访问 print(acc._BankAccount__balance) # 强行访问也能做到但你不应该这么干这样的设计逻辑其实很实际。__balance加上双下划线之后外部代码就没法随手acc.__balance 0把它改掉。你想修改余额就必须走deposit这个方法而方法内部有校验逻辑负数、非法金额会被拦截。这就是“封装”的威力它把不该让外部直接操作的数据隐藏起来只暴露经过检查的安全接口的访问方式。不过我要说实话Python社区对双下划线有个共识就是“别滥用”。如果你只是给内部实现起个名字一个下划线就足够了。双下划线常年在多继承场景下容易引发让你措手不及的行为。核心的封装思想在于“你对外表现什么接口”而不在于“你挡得有多死”。Python的哲学是“大家都是成年人了”靠约定自律而不是靠强制纪律去限制。3. 三大特性封装、继承、多态3.1 封装隐藏细节暴露接口封装这个概念我在前面已经铺垫了不少。落实到代码层面封装的精髓就是把“数据”和“操作数据的方法”绑定在一起并且通过接口对外提供服务。外部调用者只需要知道“方法叫什么、参数是什么、返回什么”完全不需要关心内部的数据结构长什么样、逻辑是怎么实现的。做项目的经验告诉我封装得好不好直接决定了你的代码能撑到多大规模才散架。我举一个例子。假设你在做一个电商系统订单总价的计算规则非常复杂有满减、有会员折扣、有运费规则、有优惠券分摊。如果这些规则全部散落在业务代码里每次调用都要把一堆参数传来传去改一次规则就跟拆炸弹一样不知道会引爆哪里。但如果你把这一切封装在Order类里对外只暴露一个calculate_total()方法业务层一行代码就能拿到结果规则再怎么变也只是类内部的事。class Order: def __init__(self, items, user_levelnormal): self.items items self.user_level user_level def calculate_total(self): raw_total sum(item[price] * item[count] for item in self.items) if self.user_level vip: raw_total * 0.85 # VIP打85折 if raw_total 500: raw_total - 50 # 满500减50 return round(raw_total, 2) order Order([{price: 300, count: 2}], user_levelvip) print(order.calculate_total())你看业务层多干净一句order.calculate_total()就搞定了。这就是封装的价值既保护了内部数据的完整性又降低了调用者的心智负担。将来规则再复杂十倍调用代码基本不用动动的只有类内部。这对大项目的稳定性来说是至关重要的。3.2 继承代码复用的利器继承解决的痛点是代码复用。你在开发中发现两个类有很多相同的属性和方法你不想写两遍那就可以抽象出一个父类让子类去继承它。父类里放公共逻辑子类里放差异化逻辑。class Animal: def __init__(self, name): self.name name def eat(self): print(f{self.name}正在进食) def make_sound(self): raise NotImplementedError(子类必须实现这个方法) class Dog(Animal): def make_sound(self): print(汪汪) class Cat(Animal): def make_sound(self): print(喵喵)这段代码里Dog和Cat都继承了Animal的name属性和eat方法同时各自覆盖override了make_sound方法实现了不同的叫声。这个场景里Animal本身还没法直接实例化因为make_sound会抛异常它是作为一个“抽象基类”存在定义了子类必须遵循的接口规格。但是在实际用继承时我要给你提个醒继承关系一定要符合“is-a”语义。Dog is-a Animal成立。如果你套一个“Car is-a Engine”那就非常别扭因为车不是引擎车拥有引擎has-a。这种时候应该优先使用组合也就是在一个类里包含另一个类的实例而不是继承。初学者容易犯的经典错误就是到处继承最后搞出一条又深又脆的继承链改父类一个方法底下所有子类全部地震。我个人的经验总结成一句话继承要浅组合要宽。能用组合表达的关系优先用组合确实存在明确的is-a关系才考虑继承。还有一个知识点需要注意Python支持多重继承。写法很简单class Flying: def fly(self): print(飞起来啦) class Swimming: def swim(self): print(游起来啦) class Duck(Flying, Swimming): pass但多重继承也是Python里最容易踩坑的地方。多个父类里有同名方法时调用顺序遵循MROMethod Resolution Order方法解析顺序如果你不熟悉MRO的概念紧接着你就会在运行时看到一堆令人困惑的调用结果。后面我会单独讲这个坑。3.3 多态同一个调用不同的行为多态这个词翻译得文绉绉的其实它的核心是“一个接口多种实现”。在Python里多态的实现比Java/C要自然得多因为Python是动态类型语言天然支持鸭子类型。什么叫鸭子类型就是“如果它走起来像鸭子、叫起来像鸭子那它就是鸭子”。你不需要显式声明一个对象必须继承某个类只要它有你要调用的方法就能用。class Dog: def make_sound(self): print(汪汪) class Cat: def make_sound(self): print(喵喵) class AlarmClock: def make_sound(self): print(叮铃铃) def start(animal): animal.make_sound() start(Dog()) # 汪汪 start(Cat()) # 喵喵 start(AlarmClock()) # 叮铃铃你看AlarmClock和Dog、Cat没有任何继承关系但只要它定义了make_sound方法就可以被start函数统一调用。这个特性给代码带来了极大的灵活性。你在写一个函数时不用关心传入的是什么类型的对象只要它保证有某个方法就行。这也是Python被广泛用于快速开发的原因之一。往深了说多态让“面向抽象编程”成为可能。你在设计一个大系统的框架时可以用多态定义“接口约定”具体实现则交给不同的下游模块去填充。比如一个支付系统定义了class Payment:接口里面有个pay()方法支付宝、微信、银行卡各自实现自己的pay()。业务层永远只调payment.pay()至于用的是哪种支付方式由运行时决定。这种做法让系统的扩展性变得非常好新增一种支付方式你只需要新写一个类完全不用动业务层代码。4. 魔法方法让对象拥有“不凡”行为4.1str__与__repr让打印更友好如果你直接print(一个自定义对象)Python默认输出一串类似__main__.Student object at 0x7f8b1c25a4a0的信息这个体验很糟糕。魔法方法__str__就是用来解决这个问题的。定义了它之后print(obj)就会输出你自定义的可读字符串。class Student: def __init__(self, name, score): self.name name self.score score def __str__(self): return f学生{self.name}的成绩是{self.score}分 def __repr__(self): return fStudent(name{self.name}, score{self.score}) s Student(小明, 96) print(s) # 学生小明的成绩是96分 print(repr(s)) # Student(name小明, score96)__str__和__repr__的区别我建议你这样记__str__是给最终用户看的讲究的是可读性、友好度__repr__是给开发者看的讲究的是准确性、可复现性。理想情况下eval(repr(obj))应该能还原出这个对象。在调试代码时repr提供的信息越多越有用。所以很多资深的Python开发者会在类里只写__repr__因为Python在找不到__str__的时候会自动回退到__repr__这样既省代码又能让日志足够详细。4.2 运算符重载与比较方法你肯定用过1 2和hello world这两个做的事情完全不同这就是运算符重载在起作用。Python允许你在自己的类里定义加号、乘号、比较等运算符的行为。最常用的场景是当你需要让对象之间可以直接进行数学运算或比较大小的时候。class Money: def __init__(self, amount): self.amount amount def __add__(self, other): return Money(self.amount other.amount) def __eq__(self, other): return self.amount other.amount def __lt__(self, other): return self.amount other.amount def __repr__(self): return fMoney({self.amount}) wallet1 Money(100) wallet2 Money(50) total wallet1 wallet2 print(total) # Money(150) print(wallet1 wallet2) # True这里__add__定义了的行为__eq__定义了的行为__lt__定义了的行为。定义了__lt__之后你的对象还支持排序比如sorted([wallet2, wallet1])就能按金额从小到大排。这个特性在写业务系统时很常用比如要给订单排序、给商品按价格排序、给任务按优先级排序都可以通过实现这几个魔法方法搞定。补充一个实用技巧如果你的类需要做“相等”判断并且这个类还要放进集合set或者作为字典的键那你还得注意__hash__的一致性。Python的规则很简单定义了__eq__的类哈希值会变成不可用状态unhashable。如果你确实需要让对象可以哈希就要同时定义__hash__。这个点特别隐蔽很多人在用自定义对象当字典键的时候突然报错找了半天才发现是这个原因。解决办法是用对象的不可变属性来生成哈希值并且保证相等的对象哈希值也必然相等。4.3 上下文管理器让资源管理自动收尾提到with关键字你怎么理解最常见的用法是打开文件with open(data.txt, r) as f: data f.read()这段代码的魔力在于无论执行过程中是否抛出异常文件都会在with块结束后被自动关闭。这种机制背后就是上下文管理器协议。你如果需要管理数据库连接、网络会话、操作系统级别的锁或者任何需要“用完后必须收尾”的资源都可以用自定义的上下文管理器来规范化处理。class DatabaseConnection: def __init__(self, url): self.url url def __enter__(self): print(f连接数据库: {self.url}) self.connection fake connection return self.connection def __exit__(self, exc_type, exc_val, exc_tb): print(关闭数据库连接) self.connection None # 返回True表示吞掉异常返回False表示抛出异常 return False with DatabaseConnection(mysql://localhost/mydb) as conn: print(执行查询)__enter__负责在进入with块时执行准备工作__exit__负责离开with块时执行清理工作。__exit__的四个参数分别对应异常类型、异常值、回溯信息、以及第三个参数其实是traceback。如果返回False异常会继续向上抛返回True异常就被吞掉了。日常开发里除非你有特殊理由建议返回False让异常正常传播而不是默默吞掉错误。这里我再分享一个个人偏好Python官方还提供了contextlib模块你可以用contextmanager装饰器把一个普通生成器函数变成上下文管理器。对于简单场景这种写法比写一个完整类要省事得多代码也更直白。两种方式的取舍是需要复用且逻辑复杂就写类一次性使用的临时逻辑用contextmanager更干净。5. 实战案例设计一个图书管理系统5.1 需求分析与类的划分理论讲了一堆不动手写一个完整的东西总觉得不落地。我这就设计一个小型的图书管理系统把前面的概念全都串一遍。需求很简单图书有书名、作者、ISBN、借出状态读者有姓名、借书卡号、最大借书数量限制图书能被借出、归还、查询。增量需求是将来可能加入电子书、杂志等不同类型的资料。这个需求里出现了几个明确的实体对象我决定划分成这几个类类名职责Book图书基础信息与自身状态管理eBook继承Book新增下载链接属性Reader读者信息与借阅行为Library管理图书集合和借阅流程LibraryError自定义异常统一错误输出把Library单独拎出来是有讲究的。Book和Reader只管理自己的数据和简单行为但“这本书被谁借了”“那个读者目前借了几本”这些跨对象业务规则集中放到Library里管理避免Book和Reader互相引用来引进去搞成一团。这就是经典的“中介者”思路用第三方的类来协调两个原本不相关的类之间的交互。5.2 核心代码与关键实现我直接上核心代码然后逐段解释关键设计。class LibraryError(Exception): 自定义异常用于统一捕获业务错误 pass class Book: def __init__(self, title, author, isbn): self.title title self.author author self.isbn isbn self.is_borrowed False def __str__(self): status 已借出 if self.is_borrowed else 可借 return f《{self.title}》 by {self.author} [{status}] def __repr__(self): return fBook(title{self.title}, author{self.author}, isbn{self.isbn}) class eBook(Book): def __init__(self, title, author, isbn, download_url): super().__init__(title, author, isbn) self.download_url download_url def download(self): if self.download_url: print(f开始从{self.download_url}下载《{self.title}》) else: raise LibraryError(下载链接无效) class Reader: def __init__(self, name, card_id, max_borrow3): self.name name self.card_id card_id self.max_borrow max_borrow self.borrowed_books [] def borrow_count(self): return len(self.borrowed_books) class Library: def __init__(self, name): self.name name self.books [] self.readers {} def add_book(self, book): self.books.append(book) def register_reader(self, reader): if reader.card_id in self.readers: raise LibraryError(读者已存在) self.readers[reader.card_id] reader def borrow_book(self, card_id, isbn): reader self.readers.get(card_id) if not reader: raise LibraryError(读者未注册) if len(reader.borrowed_books) reader.max_borrow: raise LibraryError(f读者{reader.name}已达到借阅上限) for book in self.books: if book.isbn isbn and not book.is_borrowed: book.is_borrowed True reader.borrowed_books.append(book) return f{reader.name} 借走了《{book.title}》 raise LibraryError(该书不存在或已被借出) def return_book(self, card_id, isbn): reader self.readers.get(card_id) if not reader: raise LibraryError(读者未注册) for book in reader.borrowed_books: if book.isbn isbn: book.is_borrowed False reader.borrowed_books.remove(book) return f{reader.name} 归还了《{book.title}》 raise LibraryError(该读者没有借阅这本书)几个细节我要特意解释。第一eBook用super().__init__(...)调用了父类的__init__这是继承时标准的初始化姿势。super()返回的是一个特殊对象它帮你找到合适的父类并且正确处理好MRO链路。新手容易犯的错是在子类里重写__init__时忘了调用super().__init__()结果父类的属性根本没有初始化运行到一半就报“缺属性”错误。第二Library里的borrow_book方法写了完备的校验逻辑读者未注册、借阅达到上限、图书不存在或已借出全部抛出统一的LibraryError。这样设计的好处是调用方只需要一个try/except就能捕获所有业务错误不会出现“有些错误是异常有些错误是返回值”这种尴尬情况。第三注意状态管理都封装在各自的对象里。book.is_borrowed的修改直接发生在Book对象内部reader.borrowed_books列表的维护也局限在Reader对象和Library协调层里。外部调用者完全不需要手工去修改这些东西只要调Library的方法就行。5.3 测试与运行代码能不能跑的验证思路写完类之后必须得测试一下才能验证设计是否合理。我一般随手写一段测试代码模拟完整流程library Library(先锋图书馆) library.add_book(Book(三体, 刘慈欣, 9787536692930)) library.add_book(eBook(Python编程, 张三, 9787111558422, http://example.com/a.pdf)) alice Reader(Alice, C001, max_borrow2) library.register_reader(alice) print(library.borrow_book(C001, 9787536692930)) print(library.borrow_book(C001, 9787111558422)) # 打印好好的再试第三次就应该报借阅上限 try: library.borrow_book(C001, 9787536692930) except LibraryError as e: print(捕获异常:, e) print(library.return_book(C001, 9787536692930)) for book in library.books: print(book)运行结果应该符合预期前两次借阅成功第三次因为达到借阅上限被拦截归还之后再遍历所有图书发现状态都恢复正常。这一段小小的测试其实就验证了封装的有效性、继承的扩展性、多态的可用性和异常处理的完整性。你在自己做类设计时也建议照着这个思路先跑通主流程再测试各种异常分支。6. 常见问题与排坑实录6.1 类变量和实例变量的“隐形共享”这是Python笔试面试里特别爱问的一个坑。你把变量写在类体里它是类变量所有实例共享这一个变量你把变量写在__init__里的self上它是实例变量每个实例各自独有一份。如果变量是不可变类型整数、字符串类变量被实例修改时不会互相影响但如果是可变类型列表、字典问题就来了class Student: courses [] # 类变量所有实例共享 def __init__(self, name): self.name name self.courses.append(语文) # 直接修改共享列表 a Student(小明) b Student(小红) print(a.courses) # [语文, 语文] print(b.courses) # [语文, 语文]课程的列表变成了全体学生共享的课程表这显然不是你要的效果。正确的写法是把可变类型放进__init__里class Student: def __init__(self, name): self.name name self.courses []判断标准很简单如果一个属性描述的是“这个类的每一个实例各自拥有”的状态那就要放在__init__里初始化如果描述的是“这个类所有实例共通”的状态而且是不可变类型那才适合放在类体里。6.2 默认参数是可变对象的致命陷阱这个坑和上面那个是孪生兄弟。很多新手在定义__init__或方法时为了省事直接写def __init__(self, items[])然后每次新增一个对象发现所有对象都共享了同一个列表。原因在于Python函数的默认参数只被初始化一次之后每次调用如果没有显式传值都用的是同一个对象。class Cart: def __init__(self, items[]): self.items items cart1 Cart() cart2 Cart() cart1.items.append(苹果) print(cart2.items) # [苹果]因为两个购物车共享同一个列表正确的做法是用None作为默认值在函数体内部分配新对象class Cart: def __init__(self, itemsNone): self.items items or []这样每次Cart()都会创建一个全新的列表。这个坑在平时写小例子时不明显一旦在并发场景或者批量创建对象时踩到后果非常严重。类似的道理也适用于字典、集合等任何可变对象。6.3 多重继承的方法解析顺序MRO我在前面提到过Python支持多重继承但这也带来了Diamond问题。一个经典的菱形继承结构class A: def who(self): print(A) class B(A): def who(self): print(B) class C(A): def who(self): print(C) class D(B, C): pass d D() d.who() # 结果是B为什么调用的是B而不是C或APython采用C3线性化算法计算MROD.__mro__打印出来依次是D、B、C、A、object。它保证的规则是子类永远在父类之前多个父类按照声明顺序排列。这里D声明时写的是class D(B, C)所以B排在C前面调用who时就优先命中B的版本。这个顺序看起来很自然但换交叉继承时就不一定好猜了。我的建议是再复杂的多继承场景只要出现菱形结构最好明确重写一个方法来统一行为而不是寄希望于默认的解析结果。尤其是不要试图利用MRO的透明性去实现某些“高级技巧”代码可读性会变得极差过俩月你自己都不一定看得懂。6.4 使用super()的正确姿势super()这个函数新手的认知往往只停留在“调用父类的__init__”但在多重继承的环境里super()并不只是简单调用“父类”它是沿着MRO链路向下走的。所以如果你只有一个父类怎么写都行但如果有多层继承用super().xxx()就比直接写Parent.xxx(self)安全得多。一个经典的反例类C继承A和BA继承BaseB继承BaseBase的__init__接收一个参数。如果A的__init__写死Base.__init__(self)B也写死Base.__init__(self)那么C实例化时Base会被初始化两次而且参数传递很混乱。如果都用super().__init__(...)那么每个类在整个MRO链路里只被调用一次顺序严格按照C3线性化。这是Python官方推荐的用法。当然super()也不是银弹它要求整条继承链的方法签名保持一致否则参数传着传着就对不上了。所以在设计继承体系时尽量让__init__的参数列表在各层之间保持兼容否则建议用更明确的调用方式。6.5 封装、继承、多态的误用警示最后我想专门聊一聊“过度设计”的问题。我在代码评审时见过很多把简单问题复杂化的情况没几个业务逻辑非要绕三层层继承抽象类套抽象类属性全部写成私有强行提供getter/setter但内部什么校验都没有看到哪个类有公共行为就拼命往上提取共同点结果把耦合度也带上去了。面向对象的设计哲学它的核心是“高内聚、低耦合”不是“类越多越好、继承越深越高级”。一个类应该只承担一个清晰的职责一个方法应该意志明确地完成一件事一段继承关系应该真实地反映“is-a”关系。如果你发现自己在花大量时间纠结怎么划分类、要不要抽父类那很可能你是在为设计而设计。写代码的第一目标永远是“正确、清晰、可维护”设计模式也好、面向对象特性也好都是服务于这个目标的工具不是目的。我在实际项目里的习惯是分三步走第一先用最简单的方式实现功能第二梳理代码里有没有重复的“数据行为”组合如果有就抽成类第三只针对真正变化频繁的部分引入抽象和继承。用这个节奏来推进代码既不会变成面条代码也不会被过度设计压得喘不过气。7. 从入门到精通的进阶路径建议7.1 知识框架把零散点串成网其实学到这儿你已经把Python面向对象的核心地图都点亮了。我自己在带新人时最怕看到的就是今天学一个魔法方法、明天看一个设计模式学了一堆零散的知识点但脑子里没有框架。我建议你把面向对象的知识点按层级串成一张网最底层是类和对象、属性和方法第二层是封装、继承、多态三大特性第三层是魔法方法、上下文管理器、装饰器这些进阶语法第四层才是设计模式、元类、描述符这些工程化内容。每一层知识的出现都有它要解决的问题你先知道它解决什么问题再用技术细节去填充学习效率会高很多。7.2 刻意练习用重构驱动理解纸上得来终觉浅面向对象尤其如此。我有一个屡试不爽的练习方法在这里分享给你找一个你已经用面向过程写完的小工具哪怕是个简单的待办列表尝试用面向对象重构一遍。重构时不要去改功能只改组织方式。你会发现在重构的过程中你对“类应该怎么划分”“哪些数据要封装”“接口要暴露什么”这些问题会产生非常具体的体感这比看一百篇文章都管用。日常练习时还可以刻意做这几个动作给类新增一个魔法方法、把某个逻辑从外部函数改成类的方法、把两个相似的类合并成一个带继承的结构、把继承结构改成组合结构。练得多了哪些场景适合面向对象、哪些场景适合简单的函数你自然就心中有数了。7.3 阅读源码站在巨人的肩膀上如果只推荐一种学习面向对象的途径我会推荐你读标准库源码。Python标准库里有大量高质量类设计的例子比如collections模块里的namedtuple、defaultdictpathlib里的Pathasyncio里的事件循环json工具的序列化器。这些源码是官方维护的风格统一、注释到位、适合阅读。你可以带着问题去读“这个类解决了什么问题”“它用了继承还是组合”“它的魔法方法是怎么用的”读完之后再结合本文的内容去验证你会发现面向对象的知识在真实项目里是怎么被老法师玩出花来的。写在最后——一个过来人的嘱咐把这篇内容从头到尾看完你已经不是“面向对象”的门外汉了。作为写了这么多年Python的人我想说一句真心话语法和特性永远是简单的真正难的是设计判断。什么时候该把逻辑收进一个类什么时候该让两个对象解耦什么时候该引入继承这些判断力完全依靠大量的实践和踩坑积累。你如果现在写代码时还觉得“这个类要不要建、建了又不知道放什么方法”别着急这太正常了。我建议你从今天开始找出自己最近写的一个小脚本按本文的思路动手重构一遍。动手之前默默问自己三个问题这里有哪些“实体”每个实体有哪些属性和行为实体之间怎么交互回答完这三个问题再下笔你的类设计会比凭感觉写出来的清晰十倍。编程这条路最不缺的就是“看懂了”的人最缺的是“写得出来”的人。去写吧。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询