Python面向对象编程:类、实例、封装、继承、多态与属性
学完模块之后,接下来要进入 Python 中非常重要的一部分:面向对象编程。
很多初学者第一次接触面向对象时,会觉得它比函数、列表、字典这些内容“更抽象”。因为前面的知识更多是在学习语法和常见数据结构,而面向对象开始要求我们换一种思路去组织程序:不再只想着“按步骤执行哪些函数”,而是开始思考“系统里有哪些对象、它们有什么状态、能做什么事”。
在 Python 中,面向对象并不是一个“可选的高级话题”,而是非常核心的能力。原因很简单:
- Python 本身就是一门高度面向对象的语言;
- 你平时使用的字符串、列表、字典,其实都是对象;
- 很多框架、库和工程项目,都会大量使用类、实例、继承和多态。
本文会围绕下面这些核心问题展开:
- 什么是类,什么是实例;
- 如何定义对象的数据和行为;
- 什么是封装,为什么要做访问限制;
- 继承和多态到底解决了什么问题;
- 如何判断对象类型;
- 实例属性和类属性有什么区别。
理解这一章之后,你会发现:面向对象并不是“比函数更难的写法”,而是一种更适合组织复杂程序的抽象方式。
一、什么是面向对象编程?
面向对象编程,英文是 Object Oriented Programming,通常简称 OOP。
如果用一句话概括它的核心思想,那就是:
把程序看成一组对象之间的协作,而不是一串从上到下执行的步骤。
1.1 面向过程和面向对象的差别
先看一个很简单的例子:如果我们想描述一个学生,并打印他的成绩。
面向过程的写法可能是这样:
1 | def print_score(name, score): |
这种写法没有问题,尤其在逻辑简单时很直接。
但如果程序越来越复杂,围绕“学生”会有越来越多的数据和行为,比如:
- 学号
- 班级
- 成绩
- 修改成绩
- 打印信息
- 判断是否及格
这时,继续把所有信息拆成一堆函数参数,就会越来越难管理。
面向对象的思路则是:
- 先抽象出“学生”这种类型;
- 让每个学生对象自己保存自己的数据;
- 让对象自己处理和自己相关的行为。
例如:
1 | class Student: |
这里程序的组织方式就变了:
Student描述“学生是什么”;alice、bob是具体的学生对象;- 每个对象都带着自己的数据和方法。
1.2 面向对象最核心的抽象
面向对象最核心的两个概念是:
- 类(Class):一种抽象模板;
- 实例(Instance):根据类创建出来的具体对象。
可以把它理解为:
- 类像“图纸”;
- 实例像“按图纸造出来的具体物品”。
例如:
Student是类;alice = Student('Alice', 95)创建出来的alice是实例。
后面整章内容,基本都会围绕这两个概念展开。
二、类和实例:面向对象的起点
2.1 如何定义一个类
在 Python 中,使用 class 关键字定义类:
1 | class Student: |
这里的 Student 就是一个最简单的类。
pass 表示先占一个位置,说明这个类当前还没有具体内容。
虽然它很简单,但这个类已经可以用来创建对象了:
1 | class Student: |
这里:
Student()会创建一个实例;s1和s2是两个不同对象;- 即使它们来自同一个类,也不是同一个实例。
2.2 给实例绑定属性
类创建出来后,我们可以给实例绑定属性:
1 | class Student: |
这样当然可以,但有个明显问题:
- 每次创建实例后,都要手动一条条绑定属性;
- 容易忘记;
- 不同实例的结构可能不统一。
所以更常见的做法,是通过构造方法在创建对象时就初始化属性。
2.3 __init__():实例初始化方法
在 Python 中,类里最常见的方法之一就是 __init__()。
1 | class Student: |
它的作用是:当实例被创建时,自动执行初始化逻辑。
使用方式:
1 | alice = Student('Alice', 95) |
这里要注意几个点:
__init__()的第一个参数总是self;self表示“当前实例本身”;- 调用
Student('Alice', 95)时,不需要手动传self,Python 会自动传入。
2.4 self 到底是什么?
很多初学者第一次看到 self 都会疑惑:
为什么定义方法时要写
self,调用时却不用传?
原因是:
self代表当前实例;- 方法是定义在类里的;
- 当你通过实例调用方法时,Python 会自动把实例本身传进去。
例如:
1 | class Student: |
本质上类似于:
1 | Student.print_name(alice) |
所以可以把 self 理解为“方法正在操作的那个对象自己”。
2.5 给类增加行为:定义方法
对象不只有数据,还应该有行为。行为通常用方法表示。
1 | class Student: |
使用:
1 | alice = Student('Alice', 95) |
这时你会发现,类的意义已经很明显了:
name、score是数据;print_score()、is_passed()是操作这些数据的行为;- 数据和行为被组织在同一个类中。
这就是面向对象里非常重要的思想:封装。
三、封装:把数据和行为放到一起
3.1 为什么说类是一种封装?
在前面的 Student 例子里,学生的数据和操作数据的方法都放在了类中:
1 | class Student: |
这就是封装。
所谓封装,简单理解就是:
把相关的数据和处理这些数据的逻辑放在一起,对外暴露清晰的使用方式。
这样做的好处是:
- 结构更清晰:看到
Student就知道和“学生”相关的内容都在这里; - 更容易维护:修改学生逻辑时,不需要满项目找零散函数;
- 更符合现实建模:一个对象拥有自己的状态,也有自己的行为。
3.2 “给对象发消息” 的理解方式
面向对象里常常会说:
调用对象的方法,就像在“给对象发消息”。
例如:
1 | alice.print_score() |
你可以把它理解成:
- 不是在调用一个孤立函数;
- 而是在要求
alice这个对象执行“打印成绩”这个动作。
这种表达方式在大型系统中会很自然,因为对象往往就代表现实中的某种实体或系统中的某种角色。
3.3 封装不是“故意绕远”,而是让复杂系统更稳定
初学者有时会觉得:
直接用函数不是更简单吗?为什么还要放到类里?
答案是:当程序还很小的时候,函数确实常常更直接;但当一个概念拥有大量状态和行为时,类会更适合表达它。
所以面向对象不是为了“显得高级”,而是为了在复杂场景下更好地组织代码。
四、访问限制:为什么不建议外部随便改内部数据?
有了封装之后,就会遇到一个新问题:
对象内部的数据,是不是应该完全暴露给外部?
先看一个例子:
1 | class Student: |
使用时:
1 | alice = Student('Alice', 95) |
语法上完全没问题,但显然这会让对象进入“不合理状态”。
4.1 单下划线与双下划线
在 Python 中,如果你希望某个属性不要被外部随意访问,常见做法是使用双下划线前缀:
1 | class Student: |
这时,类外部直接访问:
1 | alice = Student('Alice', 95) |
通常会报错。
这是因为 Python 会对双下划线名字做名称改写(name mangling),避免它被外部直接访问。
4.2 通过方法读写属性
如果数据不希望直接暴露,就可以通过方法来访问:
1 | class Student: |
使用时:
1 | alice = Student('Alice', 95) |
4.3 访问限制的真正意义
访问限制不是“绝对不能碰”,而是为了实现:
- 控制对象状态是否合法;
- 隐藏内部实现细节;
- 给将来调整实现留下空间。
例如,今天成绩直接保存在 __score 中;
以后也许会改成从别的数据结构中计算出来;
只要外部仍通过 get_score() 和 set_score() 访问,调用方式就不一定需要变。
4.4 Python 的“私有”不是绝对私有
需要特别说明的是,Python 并不像某些语言那样强调严格的访问控制。
理论上你仍然可以通过名称改写后的形式访问:
1 | alice._Student__score |
但这并不意味着你应该这么做。
Python 更强调的是一种约定和设计意图:
- 公开接口就正常使用;
- 内部实现尽量不要绕过设计直接访问。
所以这里的重点不是“防君子还是防小人”,而是:通过接口维护对象的一致性。
五、继承:复用和扩展现有类
当多个类之间存在共性时,继承就非常有用。
5.1 什么是继承?
假设有一个通用的 Animal 类:
1 | class Animal: |
现在我们有狗和猫,它们本质上都是动物。那么就可以写成:
1 | class Dog(Animal): |
这里:
Animal是父类、基类或超类;Dog、Cat是子类。
因为 Dog 和 Cat 继承了 Animal,所以它们天然就有 run() 方法:
1 | dog = Dog() |
5.2 为什么继承有价值?
继承的价值在于:
- 复用已有代码;
- 表达“是一种”的关系;
- 允许子类在父类基础上扩展或改写行为。
例如:
- 狗是一种动物;
- 猫是一种动物;
- 所以它们可以共享动物的通用行为。
5.3 子类增加自己的方法
子类不只是“继承后原样照搬”,还可以加自己的能力:
1 | class Dog(Animal): |
使用:
1 | dog = Dog() |
5.4 子类覆盖父类方法
如果子类觉得父类的默认行为不够合适,也可以重写它:
1 | class Dog(Animal): |
这样调用时:
1 | dog = Dog() |
输出的将不再是父类版本,而是各自子类的版本。
这就引出了面向对象另一个非常关键的概念:多态。
六、多态:同一个接口,不同对象有不同表现
6.1 什么是多态?
多态的核心思想可以概括为:
相同的调用方式,面对不同对象时,表现出不同的行为。
继续看前面的例子:
1 | class Animal: |
如果我们写一个函数:
1 | def run_twice(animal): |
那么:
1 | run_twice(Animal()) |
虽然函数写法完全一样,但输出会随传入对象不同而变化。
这就是多态。
6.2 多态带来的好处
多态最大的价值,是让代码更具扩展性。
例如以后新增一个类:
1 | class Tortoise(Animal): |
原来的 run_twice() 完全不需要修改:
1 | run_twice(Tortoise()) |
这说明函数关注的不是“你到底是狗、猫还是乌龟”,而是:
- 你是不是能够响应
run()这个动作。
这使得程序更容易扩展,而不用到处改老代码。
6.3 Python 的多态更灵活:鸭子类型
在很多语言里,多态往往依赖严格的继承体系;但 Python 更灵活。
例如下面这个类并没有继承 Animal:
1 | class Timer: |
但只要它有 run() 方法:
1 | run_twice(Timer()) |
这在 Python 中也可以工作。
这体现了 Python 常说的“鸭子类型”思想:
只要你看起来像鸭子、走起来像鸭子、叫起来像鸭子,那你就可以被当成鸭子。
换成编程语言来说就是:
- 不一定非要关心对象“继承自谁”;
- 更关心它“能不能完成我需要的接口行为”。
这也是 Python 风格里非常重要的一点。
七、获取对象信息:如何判断一个对象是什么?
写面向对象代码时,经常需要判断某个对象的类型,或者查看它具备哪些能力。
7.1 type():查看对象的精确类型
1 | print(type(123)) |
对于自定义类也是一样:
1 | class Student: |
type() 返回的是对象的实际类型。
7.2 isinstance():更推荐的类型判断方式
在面向对象场景里,通常比 type() 更常用的是 isinstance()。
1 | class Animal: |
这里第二个结果也会是 True,因为 Dog 继承自 Animal。
这正是 isinstance() 的优点:
- 它会考虑继承关系;
- 更符合面向对象的判断方式。
7.3 判断多个类型
1 | print(isinstance([1, 2, 3], (list, tuple))) |
如果对象属于元组中的任意一种类型,就返回 True。
7.4 dir():查看对象有哪些属性和方法
如果你不确定一个对象支持哪些操作,可以用 dir():
1 | print(dir('hello')) |
它会返回一个列表,里面包含这个对象可用的属性和方法名。
对于自定义对象也一样:
1 | class Student: |
7.5 hasattr()、getattr() 和 setattr()
Python 还提供了一组和属性操作相关的函数:
1 | class Student: |
它们分别用于:
hasattr():判断对象是否有某个属性;getattr():获取属性值;setattr():设置属性值。
如果属性不存在,getattr() 还可以指定默认值:
1 | print(getattr(s, 'gender', '未知')) |
这些函数在框架、反射式编程和通用工具函数中很常见。
八、实例属性和类属性:到底该放在哪里?
学习类时,另一个很容易混淆的问题是:
属性到底应该写在
__init__()里,还是直接写在类里面?
这就涉及 实例属性 和 类属性 的区别。
8.1 实例属性:属于每个对象自己
1 | class Student: |
这里的 self.name 是实例属性。
也就是说:
- 每个对象都会有自己的
name; - 不同对象的值互不影响。
例如:
1 | s1 = Student('Alice') |
8.2 类属性:属于整个类共享
1 | class Student: |
这里的 school 是类属性。
它的特点是:
- 属于类本身;
- 所有实例默认共享这份数据。
使用时:
1 | print(Student.school) |
8.3 实例属性会遮蔽同名类属性
这是一个常见考点。
1 | class Student: |
这里发生了什么?
- 一开始
s.school会去类上找school; - 给
s.school赋值后,相当于给实例创建了一个同名实例属性; - 这个实例属性会遮蔽类属性。
所以:
s.school变成了'新的学校';Student.school仍然保持原值。
8.4 什么时候适合用类属性?
适合放成类属性的内容,一般具有这些特点:
- 对所有实例都相同;
- 更像“配置”或“默认值”;
- 不属于某个单独对象的私有状态。
例如:
- 学校名称;
- 默认语言;
- 计数器初始配置;
- 常量级别的数据。
而像 name、score、age 这种明显属于每个对象自己的数据,通常就应该放在实例属性里。
8.5 一个常见陷阱:可变类属性
如果类属性是可变对象,例如列表或字典,就要特别小心。
1 | class Student: |
这意味着所有实例共享同一个列表。
1 | s1 = Student() |
此时 s2.tags 也会看到同样的内容。
如果你本来希望每个实例有自己的列表,那就应该写到 __init__() 中:
1 | class Student: |
这在实际开发里是非常常见的坑。
九、综合示例:用面向对象方式描述学生系统
为了把前面的内容串起来,下面写一个稍微完整一点的小例子。
1 | class Student: |
使用:
1 | alice = Student('Alice', 95) |
这个例子中包含了:
- 类和实例:
Student、GraduateStudent以及具体对象; - 封装:用
__name、__score表示内部数据; - 访问限制:通过
get_和set_方法操作数据; - 继承:
GraduateStudent继承Student; - 多态:子类重写
print_score(); - 类属性:
school属于整个类共享。
可以说,这已经覆盖了面向对象入门里最核心的一组能力。
十、什么时候适合使用面向对象?
学完这些内容后,一个很现实的问题是:
以后写 Python,是不是所有代码都要写成类?
答案是:不是。
10.1 面向对象适合这些场景
当你的程序中存在下面这些情况时,面向对象通常会很合适:
- 某个概念有明确的“状态 + 行为”;
- 同类对象会被批量创建;
- 多个类之间存在明显的层级关系;
- 你需要封装复杂逻辑;
- 你希望通过继承、多态提高扩展性。
例如:
- 用户系统
- 订单系统
- 图形界面组件
- 游戏角色系统
- Web 框架中的请求、响应、视图对象
10.2 不要为了 OOP 而 OOP
如果只是写一个很短的小脚本,例如:
- 读一个文件;
- 做一次格式转换;
- 执行几个简单步骤;
那直接写函数往往更自然。
所以真正重要的不是“逢代码必建类”,而是理解:
- 简单问题用简单方式解决;
- 复杂对象和复杂状态适合用类组织。
Python 一直强调实用主义,面向对象也应该在合适的地方使用,而不是强行套模板。
十一、学习建议:这一章最该真正搞懂什么?
面向对象这一章看起来概念很多,但真正最关键的只有几件事。
11.1 先彻底理解类和实例
如果你能真正理解:
- 类是模板;
- 实例是具体对象;
self是当前实例;
后面的很多内容都会顺起来。
11.2 把封装理解成“组织方式”,而不是“语法技巧”
封装不只是写几个 get_ / set_ 方法,而是思考:
- 哪些数据属于这个对象;
- 哪些操作应该由它自己完成;
- 外部应该通过什么接口与它交互。
11.3 继承和多态要理解“为什么”,不要只会背定义
继承不是为了偷懒少写几行代码,多态也不是为了让概念显得高级。
它们真正解决的是:
- 共性逻辑如何复用;
- 新类型如何平滑扩展进已有系统;
- 调用者如何少关心具体类型、多关心接口行为。
11.4 实例属性和类属性一定要分清楚
这是实战里最常出错的地方之一。
尤其要记住:
- 可变类属性很容易引发共享状态问题;
- 属于对象自己的数据,通常就写成实例属性。
十二、总结
这一篇我们系统梳理了 Python 面向对象编程中最基础、最核心的内容:
- 类和实例:类是抽象模板,实例是具体对象;
__init__()和self:用于初始化对象并表示当前实例;- 封装:把数据和行为组织在同一个类中;
- 访问限制:通过双下划线和方法控制对象内部状态;
- 继承:让子类复用并扩展父类能力;
- 多态:同一个接口在不同对象上呈现不同表现;
- 对象信息获取:使用
type()、isinstance()、dir()等工具理解对象; - 实例属性和类属性:区分“对象私有状态”和“类共享数据”。
如果说前面的模块这一章是在学习“如何组织多个 Python 文件”,那么这一章就是在学习:
如何把程序中的数据和行为抽象成对象,并让对象之间形成清晰的协作关系。
掌握这部分之后,你就能开始读懂很多真实项目中的类定义,也为下一步学习面向对象高级编程打下了基础。
下一篇继续往后写时,最自然的衔接就是:
- 面向对象高级编程:
__slots__、@property、多重继承、定制类、枚举类、元类。