ARTICLE DETAIL

资讯详情

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

Python面向对象编程入门:类与实例的核心概念与工程实践

Python面向对象编程入门:类与实例的核心概念与工程实践 学 Python 基础语法的时候函数已经能帮我们把重复逻辑收起来但一旦开始处理现实数据比如要记录三个学生的姓名、班级和成绩很多人会发现自己还是在用三个列表、三个字典来回折腾。这时候就该接触 Python 面向对象编程里的两个核心概念类Class和实例Instance。我第一次用类的时候最大的感受不是“代码变短了”而是“终于不用自己手动维护数据和函数之间的对应关系了”。这个系列的第一篇我打算只讲类和实例。不碰继承不碰多态也不碰各种魔法方法。原因是很多初学者一开始就被“面向对象三大特性”劝退了但其实真正需要先理解的是类是什么实例是什么它们之间怎么配合。把这些想清楚后面学继承和多态会顺很多想不清楚后面看任何 OOP 项目都会觉得像在看天书。1. 先搞清楚“类”解决的是哪一类重复建模问题1.1 为什么函数还不够状态与行为要一起维护很多人学完函数后会有一个错觉既然函数能封装逻辑那是不是所有程序都可以用函数写完确实可以Python 本身也不强制你用类。但当你开始处理真实业务数据时函数式的自然写法会慢慢变辛苦。举个例子。你要记录两个学生的姓名和分数然后分别计算他们是否及格。用最简单的方式可能会写成这样name1 小明 score1 92 name2 小红 score2 58 print(f{name1} 的分数是 {score1}及格{score1 60}) print(f{name2} 的分数是 {score2}及格{score2 60})两个学生没问题三个也还行但如果有三十个学生呢你大概率会想到用列表或字典students [ {name: 小明, score: 92}, {name: 小红, score: 58}, {name: 小刚, score: 76}, ]这已经比分散变量好很多了。但你会发现和“学生”相关的操作比如“算平均分”“判断是否及格”“根据分数返回等级”并没有和数据结构放在一起。它们是散落在代码里的函数需要你自己去维护“哪些函数可以作用在哪些字典上”。类出现的目的就是把这组数据和操作这组数据的函数收拢成一个整体。数据是“状态”函数是“行为”。类和实例要解决的核心问题不是让代码看起来更高级而是让“状态”和“行为”在代码层面天然绑定在一起。1.2 类像“模板”实例像“按模板造出来的个体”理解类与实例最直观的类比是“模板”和“成品”。类描述的是这一类东西的共同结构。比如“学生”应该有姓名、有班级、有分数还应该有“打印信息”“计算等级”这样的能力。你并不会直接拿着“学生模板”去考试真正坐在考场里的是某个具体的学生。实例就是根据模板创建出来的具体个体。小明是一个实例小红是另一个实例。它们都拥有类里定义的结构但各自的名字、班级、分数都可以不同。这种“模板-成品”的区分正是面向对象建模的基本方式。你不需要为每个学生单独写一套代码只需要定义一次学生类然后创建多个实例。从工程角度看这种模式有一个很直接的好处当你要新增一个字段比如“学号”只需要在类定义里改一处所有实例都会获得这个新结构如果你用一堆字典可能要手动去更新每一处创建字典的代码。注意类和实例不是 Python 独有的概念。Java、C、JavaScript 里都有类似设计但 Python 的类机制更灵活也更容易让初学者误用。所以第一步不是急着写代码而是先养成分清“共同结构”和“个体状态”的习惯。2. 从定义到实例化跑通最小可运行流程2.1 定义一个最简的 Student 类在 Python 里定义一个类用class关键字后面跟着类名。类名通常用大驼峰命名法比如Student、OrderItem、HttpClient。下面是最简单的一个学生类class Student: def __init__(self, name, score): self.name name self.score score def get_name(self): return self.name def get_score(self): return self.score这段代码虽然短但包含了几个非常重要的点__init__是初始化方法也就是创建实例后立即自动执行的方法。self代表“正在创建的实例本身”。self.name name表示给这个具体实例增加一个name属性值为传入的name参数。get_name是一个实例方法它操作的是具体实例的数据。创建实例的写法是“类名加括号”s1 Student(小明, 92) s2 Student(小红, 58) print(s1.get_name()) # 小明 print(s2.get_score()) # 58这里的s1和s2就是两个不同的实例。它们都来自同一个类但各自持有自己的name和score。2.2__init__和self在实例化时做了什么很多初学者对__init__和self有误解会以为__init__是“构造函数”或者self是可以随便换的名字。这里用最容易理解的说法解释一下。在 Python 实例化一个对象时实际会发生两件事通过__new__创建出一个“空壳”实例。调用__init__对这个空壳进行初始化。你可以暂时不深究__new__只需要知道__init__不是用来“创建”对象的而是用来“初始化”对象的。真正创建实例的机制在更底层。self这个名字也不是语法强制要求的“魔法词”。它只是约定俗成的第一个参数名。你完全可以写this、obj或me但绝大多数 Python 代码都使用self所以不要在项目里特立独行。实例方法一旦被调用Python 会自动把“调用这个方法的实例”作为第一个参数传进去。所以s1.get_name()等价于Student.get_name(s1)后者是更底层的写法前者是日常开发使用的语法糖。理解这一点后你再看很多源码里的cls、self就不会觉得奇怪了。2.3 创建实例时最常犯的跳过初始化错误我见过不少初学项目类里明明定义了__init__但创建实例时少传参数导致报错s Student()如果__init__里要求必须有name和score这样写就会抛出TypeError: __init__() missing 2 required positional arguments。这时候如果看到报错不要慌。它其实在告诉你实例化时需要把参数补齐。这个报错是 Python 在保护你避免创建一个没有基本属性的不完整对象。也有一种情况是类里没有定义__init__比如class Student: pass s Student()这样也能创建实例但实例身上没有name也没有score。后续如果直接访问s.name会抛出AttributeError。这种“空类”在实际项目中很少直接使用更多是作为临时占位或后续动态添加属性的场景。所以我的建议是只要类表示的是现实世界里有明确特征的对象就应该在__init__里把核心属性定义出来。这不仅是给 Python 解释器看的也是给阅读代码的人看的。3. 类属性、实例属性和方法绑定三个最容易混淆的基础点3.1 类属性是所有实例共享的实例属性是各自独立的前面例子里的self.name是实例属性。它属于某个具体实例不同实例之间互不影响。但一个类也可以拥有自己的属性称为类属性。类属性直接写在类内部不在任何方法里class Student: school 希望中学 def __init__(self, name, score): self.name name self.score score这里的school就是类属性。如果创建一个实例s1 Student(小明, 92) print(s1.school) # 希望中学你不需要在__init__里给每个实例都写一份self.school 希望中学因为所有实例都能访问类属性。但这里有一个经典的坑如果你通过实例给类属性赋值实际上不是修改类属性而是给这个实例创建了一个和类属性同名的实例属性从而“遮蔽”了类属性。s1.school 第二中学 print(s1.school) # 第二中学 print(s2.school) # 希望中学Student.school仍然是“希望中学”。只有直接修改类属性才会影响所有实例Student.school 第三中学 print(s1.school) # 第三中学前提是 s1 没有自己的 school 属性 print(s2.school) # 第三中学这个机制刚接触时很容易绕晕。记住一个判断标准访问属性时Python 会先找实例自己的__dict__找不到再找类的__dict__。实例属性优先于类属性。3.2 实例方法绑定的底层机制student.get_score()等价于Student.get_score(student)实例方法就是定义在类内部、第一个参数是self的函数。当你通过实例调用它时Python 会自动完成“绑定”。s1.get_score()它和下面的调用是等价的Student.get_score(s1)第一种写法更自然第二种写法能让你看清self的真实身份它就是一个普通参数Python 语法把你平时要手动传递的“对象自身”自动填了进去。理解了这一点后面看类方法classmethod和静态方法staticmethod时就能更快抓住区别实例方法绑定实例类方法绑定类静态方法不绑定任何东西。但这篇教程暂时不展开知道它们存在即可。3.3 用__dict__观察属性存储位置如果你想验证“实例属性存在哪”“类属性存在哪”可以借助__dict__。在 Python 里大多数对象都有一个__dict__用来保存实例属性类对象也有自己的__dict__用来保存类属性。class Student: school 希望中学 def __init__(self, name, score): self.name name self.score score s1 Student(小明, 92) print(s1.__dict__)输出通常是{name: 小明, score: 92}这说明s1这个实例身上只有name和score两个属性。school并不在实例的字典里而是在类的字典里print(Student.__dict__)当你写s1.school时Python 先去s1.__dict__里找school找不到再去Student.__dict__里找找到了。这个过程叫属性查找。实际排查属性问题时__dict__是非常好用的工具。看到AttributeError时先打印实例的__dict__很多时候一眼就能发现问题。4. 日常开发中最常见的四类踩坑与排查链路4.1 在__init__之外给实例属性赋值可能导致部分实例没有这个属性Python 允许你在实例创建后动态添加属性s1 Student(小明, 92) s1.address 北京这样s1就多了一个address属性但s2没有。如果你在某段代码里遍历所有学生并访问address在s2身上就会抛AttributeError。这种灵活性在需要快速原型验证时很方便但一旦进入正式项目它会让属性结构变得不可预测。更好的做法是所有实例属性都在__init__里定义哪怕暂时用不上也先赋一个初始值比如self.address None。这样可以保证所有实例拥有相同的属性结构减少“部分实例有属性部分没有”的问题。4.2 可变默认值会造成状态污染这个坑在函数里已经很有名在类里同样常见。看这个例子class Student: def __init__(self, name, tags[]): self.name name self.tags tagstags[]这个默认列表在函数定义时只创建一次。如果创建两个实例且都不传tagss1 Student(小明) s1.tags.append(优秀) s2 Student(小红) print(s2.tags) # [优秀]问题发生了你本意是每个实例拥有独立的tags结果它们共享了同一个默认列表。原因在于[]是一个可变对象且只创建了一次。正确的写法是class Student: def __init__(self, name, tagsNone): self.name name if tags is None: self.tags [] else: self.tags tags这个规则可以推广到所有可变默认值包括列表、字典、集合。默认参数如果必须是可变值统一用None占位再在方法内创建新对象。4.3 实例属性和类属性同名时的遮蔽我们前面已经提过。这里再展开说明因为它太容易被忽视。如果类属性叫school实例属性也叫school那么通过实例访问school时只会看到实例属性类属性被遮蔽了。class Student: school 希望中学 def __init__(self, name, score, schoolNone): self.name name self.score score if school is not None: self.school school s1 Student(小明, 92, school第二中学) print(s1.school) # 第二中学 print(Student.school) # 希望中学这类代码不是不能写但要注意它会让“实例属性”和“类属性”的关系变得很微妙。如果项目里有人直接修改Student.school也不会影响s1.school。如果没有充分理由尽量不要让实例属性和类属性重名尤其是在业务属性上。4.4 属性访问报错的排查链路当遇到AttributeError: Student object has no attribute score之类的错误时按下面的顺序排查最容易定位看报错信息里的属性名是score、name还是别的。看实例是从哪个类创建的确认它是不是你预期中的类。打印instance.__dict__看这个实例自己有没有这个属性。如果没有进入下一步。打印ClassName.__dict__看类对象上有没有这个属性。检查__init__逻辑有没有把属性名拼错是不是只给部分分支赋值了是不是用了可变默认值。检查代码里有没有在实例上动态删除属性del s1.score会让实例后续访问报错。检查继承关系如果这个类有父类还要看父类有没有定义该属性。不过这个过程在涉及继承时更容易遇到。这条链路看起来有点繁琐但实际执行起来很快。大多数情况下问题都出在“属性名不一致”或者“属性在__init__里没定义”。提醒不要一看到AttributeError就想着用hasattr或getattr加默认值硬扛。先找到根因再决定是补属性还是修调用逻辑。直接吞掉异常会掩盖真正的设计问题。5. 用一个完整的小例子理解“多个实例 统一方法”的工程价值5.1 设计学生类属性、方法、班级人数统计现在我们把前面的知识组合起来写一个稍微完整一点的示例。这个类要支持每个实例拥有姓名、分数提供一个方法打印学生信息提供一个方法计算等级提供一个类属性统计一共创建了多少个学生class Student: school 希望中学 total_count 0 def __init__(self, name, score): self.name name self.score score Student.total_count 1 def get_name(self): return self.name def get_score(self): return self.score def get_grade(self): if self.score 90: return A elif self.score 80: return B elif self.score 60: return C else: return D def print_info(self): print(f{self.name} 的分数是 {self.score}等级是 {self.get_grade()})创建三个实例s1 Student(小明, 92) s2 Student(小红, 58) s3 Student(小刚, 76) s1.print_info() s2.print_info() s3.print_info() print(f一共创建了 {Student.total_count} 个学生)输出会显示三个学生各自的信息最后统计是 3。这里有两个值得注意的点total_count是类属性所以所有实例共享。每次__init__执行时通过Student.total_count 1递增而不是self.total_count 1。如果写成self.total_count 1因为self上找不到这个属性Python 会读取类属性的值并创建一个新的实例属性结果不会影响类属性计数就会失效。get_grade是实例方法它内部访问了self.score所以每个实例调用时返回的是自己的等级。5.2 从单实例到多实例数据为什么不会串很多人学类和实例时最难过去的心理关卡是为什么我创建两个实例它们的属性不会互相覆盖答案在于每个实例都是独立的内存对象。s1和s2虽然都叫“学生”但s1.name和s2.name指向的是不同的字符串对象s1.score和s2.score是不同的整数对象。你修改s1.score不会影响s2.score。实例方法之所以能正确工作靠的是self参数。它像一根管道把“当前实例”传进方法里。方法里的self.score一开始就绑定到了调用它的那个实例身上。这就是类最核心的工程价值你只写一次方法逻辑但可以让它在无数个独立实例上反复运行。数据隔离由实例机制天然提供你不需要自己维护“哪个学生对应哪个成绩”的映射关系。5.3 类作为模块导出的常见组织方式在实际项目中类通常不会和主逻辑写在同一个大文件里。更常见的做法是一个类一个模块或者一类功能一个包。比如project/ ├─ models/ │ ├─ __init__.py │ └─ student.py ├─ main.pystudent.py里定义类class Student: ...main.py里导入from models.student import Student这样做的目的是让代码职责清晰也方便复用。刚开始写练习项目时不必追求复杂的包结构但至少应该保持“类和业务逻辑分离”的意识。如果一个文件里放了五个类还穿插着几百行调用逻辑后续维护会变得很痛苦。等你把类放进模块后一定要记得检查导入路径和__init__.py文件。很多时候导入报错不是类写错了而是模块路径或包名没有配对。6. 什么时候用类什么时候别硬凑以及学完这步之后往哪走6.1 适合使用类的几个信号不是所有代码都要用类。用一个判断标准如果一段代码里同时存在“一组相关的数据”和“操作这些数据的函数”那么把这些数据和行为封装成类是值得的。适合使用类的信号包括你反复创建结构相似的对象比如多个用户、多个订单、多个配置项。你需要把数据与操作这些数据的方法绑定比如订单金额计算、学生成绩等级判断。你需要为某一类对象维护统一的状态比如总实例数、配置项、连接池。你希望代码能被多种场景复用而不是把逻辑写死在流程里。以订单为例。如果你用字典表示订单计算总价时你可能会写一个calc_total(order_dict)函数然后每次都传字典进去。如果字段名写错或者调用者传了一个不完全的字典函数内部可能报出很隐晦的错误。但如果你定义Order类并在__init__里强制要求order_id、amount、status等字段那么一个结构不完整的对象根本无法创建。这属于“用类型约束数据形状”的价值。6.2 暂时不需要用类的场景反过来如果只是临时处理几个值或者脚本只有几十行就不必硬凑类。比如name 小明 score 92这种情况用类反而会增加理解成本。又比如你只是在数据分析脚本里调用 pandas 处理 CSV并不需要为每一行数据定义类你只需要在 DataFrame 层面操作。还有一个常见误区为了“面向对象”而把每个函数都塞进类里。如果一个类里只有一堆方法没有实例状态并且方法之间也不需要共享数据那它更像是一个“函数容器”。这种情况下直接用模块里的普通函数可能更清晰。判断标准很简单这个类有没有“实例属性”如果所有方法都不依赖self上的任何属性那它大概率不需要做成类。6.3 一条 OOP 入门检查清单衔接继承和多态学完类和实例之后在继续往下学之前我建议你先用下面这个清单自检一遍。它也是一个可以复用的思考框架。检查项说明我是否清楚这个类代表“哪一类对象”不明确的话类定义会变成杂物间。实例属性是否都在__init__里定义这能避免部分实例缺少属性的问题。self是否被正确使用实例方法里确实在操作当前实例的数据。类属性与实例属性是否混用明确哪些值应该被所有实例共享哪些值应该各自独立。方法是否需要接收额外参数如果方法不需要self它可能是静态方法如果只需要类属性可能是类方法。是否避免了可变默认值列表、字典、集合默认参数都用None占位。创建多个实例后数据是否互相独立用__dict__验证一下会更直观。这张清单不是让你每写一个类都过一遍而是当你在项目里遇到属性错乱、实例相互影响、报错定位不到问题时可以回来逐项检查。学完类和实例下一步自然是继承和多态。但这里我要多提醒一句继承是为了表达“is-a”关系不是为了复用代码而强行继承。如果一个“学生”类和一个“老师”类只是都有姓名不代表老师应该继承学生。更合理的做法可能是提取一个“人”的基类或者干脆各自独立然后用函数处理公共逻辑。当你对“类属性、实例属性、实例方法、属性查找”这些基础概念形成肌肉记忆后再看继承、抽象基类、描述符、元类这些进阶内容才能理解它们到底在解决什么问题。否则你只是在背语法。回到最开始的场景。当你要记录一组结构相同、状态不同的对象时类不是唯一选项但通常是一个值得优先考虑的选项。它把数据和操作放在一起让你在写代码时更像在描述现实事物而不是在手动管理一堆松散变量。接下来真正要练习的不是背更多 OOP 术语而是多写几个“学生管理系统”“订单处理工具”这样的小项目感受类和实例如何从定义到创建、从单个到多个、从练习到工程一步步嵌入你的编程习惯里。
返回列表