Python面向对象编程:类、实例、封装、继承、多态与属性

学完模块之后,接下来要进入 Python 中非常重要的一部分:面向对象编程

很多初学者第一次接触面向对象时,会觉得它比函数、列表、字典这些内容“更抽象”。因为前面的知识更多是在学习语法和常见数据结构,而面向对象开始要求我们换一种思路去组织程序:不再只想着“按步骤执行哪些函数”,而是开始思考“系统里有哪些对象、它们有什么状态、能做什么事”。

在 Python 中,面向对象并不是一个“可选的高级话题”,而是非常核心的能力。原因很简单:

  • Python 本身就是一门高度面向对象的语言;
  • 你平时使用的字符串、列表、字典,其实都是对象;
  • 很多框架、库和工程项目,都会大量使用类、实例、继承和多态。

本文会围绕下面这些核心问题展开:

  • 什么是类,什么是实例;
  • 如何定义对象的数据和行为;
  • 什么是封装,为什么要做访问限制;
  • 继承和多态到底解决了什么问题;
  • 如何判断对象类型;
  • 实例属性和类属性有什么区别。

理解这一章之后,你会发现:面向对象并不是“比函数更难的写法”,而是一种更适合组织复杂程序的抽象方式。

一、什么是面向对象编程?

面向对象编程,英文是 Object Oriented Programming,通常简称 OOP

如果用一句话概括它的核心思想,那就是:

把程序看成一组对象之间的协作,而不是一串从上到下执行的步骤。

1.1 面向过程和面向对象的差别

先看一个很简单的例子:如果我们想描述一个学生,并打印他的成绩。

面向过程的写法可能是这样:

1
2
3
4
5
6
def print_score(name, score):
print(f'{name}: {score}')


print_score('Alice', 95)
print_score('Bob', 82)

这种写法没有问题,尤其在逻辑简单时很直接。

但如果程序越来越复杂,围绕“学生”会有越来越多的数据和行为,比如:

  • 学号
  • 班级
  • 成绩
  • 修改成绩
  • 打印信息
  • 判断是否及格

这时,继续把所有信息拆成一堆函数参数,就会越来越难管理。

面向对象的思路则是:

  • 先抽象出“学生”这种类型;
  • 让每个学生对象自己保存自己的数据;
  • 让对象自己处理和自己相关的行为。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student:
def __init__(self, name, score):
self.name = name
self.score = score

def print_score(self):
print(f'{self.name}: {self.score}')


alice = Student('Alice', 95)
bob = Student('Bob', 82)

alice.print_score()
bob.print_score()

这里程序的组织方式就变了:

  • Student 描述“学生是什么”;
  • alicebob 是具体的学生对象;
  • 每个对象都带着自己的数据和方法。
1.2 面向对象最核心的抽象

面向对象最核心的两个概念是:

  • 类(Class):一种抽象模板;
  • 实例(Instance):根据类创建出来的具体对象。

可以把它理解为:

  • 类像“图纸”;
  • 实例像“按图纸造出来的具体物品”。

例如:

  • Student 是类;
  • alice = Student('Alice', 95) 创建出来的 alice 是实例。

后面整章内容,基本都会围绕这两个概念展开。


二、类和实例:面向对象的起点

2.1 如何定义一个类

在 Python 中,使用 class 关键字定义类:

1
2
class Student:
pass

这里的 Student 就是一个最简单的类。

pass 表示先占一个位置,说明这个类当前还没有具体内容。

虽然它很简单,但这个类已经可以用来创建对象了:

1
2
3
4
5
6
7
8
9
10
class Student:
pass


s1 = Student()
s2 = Student()

print(s1)
print(s2)
print(s1 == s2)

这里:

  • Student() 会创建一个实例;
  • s1s2 是两个不同对象;
  • 即使它们来自同一个类,也不是同一个实例。
2.2 给实例绑定属性

类创建出来后,我们可以给实例绑定属性:

1
2
3
4
5
6
7
8
9
10
class Student:
pass


s = Student()
s.name = 'Alice'
s.score = 95

print(s.name)
print(s.score)

这样当然可以,但有个明显问题:

  • 每次创建实例后,都要手动一条条绑定属性;
  • 容易忘记;
  • 不同实例的结构可能不统一。

所以更常见的做法,是通过构造方法在创建对象时就初始化属性。

2.3 __init__():实例初始化方法

在 Python 中,类里最常见的方法之一就是 __init__()

1
2
3
4
class Student:
def __init__(self, name, score):
self.name = name
self.score = score

它的作用是:当实例被创建时,自动执行初始化逻辑。

使用方式:

1
2
3
4
5
alice = Student('Alice', 95)
bob = Student('Bob', 82)

print(alice.name)
print(bob.score)

这里要注意几个点:

  1. __init__() 的第一个参数总是 self
  2. self 表示“当前实例本身”;
  3. 调用 Student('Alice', 95) 时,不需要手动传 self,Python 会自动传入。
2.4 self 到底是什么?

很多初学者第一次看到 self 都会疑惑:

为什么定义方法时要写 self,调用时却不用传?

原因是:

  • self 代表当前实例;
  • 方法是定义在类里的;
  • 当你通过实例调用方法时,Python 会自动把实例本身传进去。

例如:

1
2
3
4
5
6
7
class Student:
def print_name(self):
print(self.name)


alice = Student('Alice', 95)
alice.print_name()

本质上类似于:

1
Student.print_name(alice)

所以可以把 self 理解为“方法正在操作的那个对象自己”。

2.5 给类增加行为:定义方法

对象不只有数据,还应该有行为。行为通常用方法表示。

1
2
3
4
5
6
7
8
9
10
class Student:
def __init__(self, name, score):
self.name = name
self.score = score

def print_score(self):
print(f'{self.name}: {self.score}')

def is_passed(self):
return self.score >= 60

使用:

1
2
3
4
5
6
alice = Student('Alice', 95)
bob = Student('Bob', 59)

alice.print_score()
print(alice.is_passed())
print(bob.is_passed())

这时你会发现,类的意义已经很明显了:

  • namescore 是数据;
  • print_score()is_passed() 是操作这些数据的行为;
  • 数据和行为被组织在同一个类中。

这就是面向对象里非常重要的思想:封装。


三、封装:把数据和行为放到一起

3.1 为什么说类是一种封装?

在前面的 Student 例子里,学生的数据和操作数据的方法都放在了类中:

1
2
3
4
5
6
7
class Student:
def __init__(self, name, score):
self.name = name
self.score = score

def print_score(self):
print(f'{self.name}: {self.score}')

这就是封装。

所谓封装,简单理解就是:

把相关的数据和处理这些数据的逻辑放在一起,对外暴露清晰的使用方式。

这样做的好处是:

  1. 结构更清晰:看到 Student 就知道和“学生”相关的内容都在这里;
  2. 更容易维护:修改学生逻辑时,不需要满项目找零散函数;
  3. 更符合现实建模:一个对象拥有自己的状态,也有自己的行为。
3.2 “给对象发消息” 的理解方式

面向对象里常常会说:

调用对象的方法,就像在“给对象发消息”。

例如:

1
alice.print_score()

你可以把它理解成:

  • 不是在调用一个孤立函数;
  • 而是在要求 alice 这个对象执行“打印成绩”这个动作。

这种表达方式在大型系统中会很自然,因为对象往往就代表现实中的某种实体或系统中的某种角色。

3.3 封装不是“故意绕远”,而是让复杂系统更稳定

初学者有时会觉得:

直接用函数不是更简单吗?为什么还要放到类里?

答案是:当程序还很小的时候,函数确实常常更直接;但当一个概念拥有大量状态和行为时,类会更适合表达它。

所以面向对象不是为了“显得高级”,而是为了在复杂场景下更好地组织代码。


四、访问限制:为什么不建议外部随便改内部数据?

有了封装之后,就会遇到一个新问题:

对象内部的数据,是不是应该完全暴露给外部?

先看一个例子:

1
2
3
4
class Student:
def __init__(self, name, score):
self.name = name
self.score = score

使用时:

1
2
alice = Student('Alice', 95)
alice.score = -100

语法上完全没问题,但显然这会让对象进入“不合理状态”。

4.1 单下划线与双下划线

在 Python 中,如果你希望某个属性不要被外部随意访问,常见做法是使用双下划线前缀:

1
2
3
4
class Student:
def __init__(self, name, score):
self.__name = name
self.__score = score

这时,类外部直接访问:

1
2
alice = Student('Alice', 95)
print(alice.__name)

通常会报错。

这是因为 Python 会对双下划线名字做名称改写(name mangling),避免它被外部直接访问。

4.2 通过方法读写属性

如果数据不希望直接暴露,就可以通过方法来访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('成绩必须在 0 到 100 之间')

使用时:

1
2
3
4
alice = Student('Alice', 95)
print(alice.get_score())
alice.set_score(88)
print(alice.get_score())
4.3 访问限制的真正意义

访问限制不是“绝对不能碰”,而是为了实现:

  • 控制对象状态是否合法
  • 隐藏内部实现细节
  • 给将来调整实现留下空间

例如,今天成绩直接保存在 __score 中;
以后也许会改成从别的数据结构中计算出来;
只要外部仍通过 get_score()set_score() 访问,调用方式就不一定需要变。

4.4 Python 的“私有”不是绝对私有

需要特别说明的是,Python 并不像某些语言那样强调严格的访问控制。

理论上你仍然可以通过名称改写后的形式访问:

1
alice._Student__score

但这并不意味着你应该这么做。

Python 更强调的是一种约定和设计意图:

  • 公开接口就正常使用;
  • 内部实现尽量不要绕过设计直接访问。

所以这里的重点不是“防君子还是防小人”,而是:通过接口维护对象的一致性。


五、继承:复用和扩展现有类

当多个类之间存在共性时,继承就非常有用。

5.1 什么是继承?

假设有一个通用的 Animal 类:

1
2
3
class Animal:
def run(self):
print('Animal is running...')

现在我们有狗和猫,它们本质上都是动物。那么就可以写成:

1
2
3
4
5
6
class Dog(Animal):
pass


class Cat(Animal):
pass

这里:

  • Animal 是父类、基类或超类;
  • DogCat 是子类。

因为 DogCat 继承了 Animal,所以它们天然就有 run() 方法:

1
2
3
4
5
dog = Dog()
cat = Cat()

dog.run()
cat.run()
5.2 为什么继承有价值?

继承的价值在于:

  1. 复用已有代码
  2. 表达“是一种”的关系
  3. 允许子类在父类基础上扩展或改写行为。

例如:

  • 狗是一种动物;
  • 猫是一种动物;
  • 所以它们可以共享动物的通用行为。
5.3 子类增加自己的方法

子类不只是“继承后原样照搬”,还可以加自己的能力:

1
2
3
class Dog(Animal):
def bark(self):
print('Woof! Woof!')

使用:

1
2
3
dog = Dog()
dog.run()
dog.bark()
5.4 子类覆盖父类方法

如果子类觉得父类的默认行为不够合适,也可以重写它:

1
2
3
4
5
6
7
8
class Dog(Animal):
def run(self):
print('Dog is running...')


class Cat(Animal):
def run(self):
print('Cat is running...')

这样调用时:

1
2
3
4
5
dog = Dog()
cat = Cat()

dog.run()
cat.run()

输出的将不再是父类版本,而是各自子类的版本。

这就引出了面向对象另一个非常关键的概念:多态。


六、多态:同一个接口,不同对象有不同表现

6.1 什么是多态?

多态的核心思想可以概括为:

相同的调用方式,面对不同对象时,表现出不同的行为。

继续看前面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Animal:
def run(self):
print('Animal is running...')


class Dog(Animal):
def run(self):
print('Dog is running...')


class Cat(Animal):
def run(self):
print('Cat is running...')

如果我们写一个函数:

1
2
3
def run_twice(animal):
animal.run()
animal.run()

那么:

1
2
3
run_twice(Animal())
run_twice(Dog())
run_twice(Cat())

虽然函数写法完全一样,但输出会随传入对象不同而变化。

这就是多态。

6.2 多态带来的好处

多态最大的价值,是让代码更具扩展性。

例如以后新增一个类:

1
2
3
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')

原来的 run_twice() 完全不需要修改:

1
run_twice(Tortoise())

这说明函数关注的不是“你到底是狗、猫还是乌龟”,而是:

  • 你是不是能够响应 run() 这个动作。

这使得程序更容易扩展,而不用到处改老代码。

6.3 Python 的多态更灵活:鸭子类型

在很多语言里,多态往往依赖严格的继承体系;但 Python 更灵活。

例如下面这个类并没有继承 Animal

1
2
3
class Timer:
def run(self):
print('Start...')

但只要它有 run() 方法:

1
run_twice(Timer())

这在 Python 中也可以工作。

这体现了 Python 常说的“鸭子类型”思想:

只要你看起来像鸭子、走起来像鸭子、叫起来像鸭子,那你就可以被当成鸭子。

换成编程语言来说就是:

  • 不一定非要关心对象“继承自谁”;
  • 更关心它“能不能完成我需要的接口行为”。

这也是 Python 风格里非常重要的一点。


七、获取对象信息:如何判断一个对象是什么?

写面向对象代码时,经常需要判断某个对象的类型,或者查看它具备哪些能力。

7.1 type():查看对象的精确类型
1
2
3
print(type(123))
print(type('hello'))
print(type(None))

对于自定义类也是一样:

1
2
3
4
5
6
class Student:
pass


s = Student()
print(type(s))

type() 返回的是对象的实际类型。

7.2 isinstance():更推荐的类型判断方式

在面向对象场景里,通常比 type() 更常用的是 isinstance()

1
2
3
4
5
6
7
8
9
10
11
class Animal:
pass


class Dog(Animal):
pass


dog = Dog()
print(isinstance(dog, Dog))
print(isinstance(dog, 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
2
3
4
5
6
7
8
9
10
class Student:
def __init__(self, name):
self.name = name

def hello(self):
print(f'Hello, {self.name}')


s = Student('Alice')
print(dir(s))
7.5 hasattr()getattr()setattr()

Python 还提供了一组和属性操作相关的函数:

1
2
3
4
5
6
7
8
9
10
11
class Student:
def __init__(self, name):
self.name = name


s = Student('Alice')

print(hasattr(s, 'name'))
print(getattr(s, 'name'))
setattr(s, 'age', 20)
print(s.age)

它们分别用于:

  • hasattr():判断对象是否有某个属性;
  • getattr():获取属性值;
  • setattr():设置属性值。

如果属性不存在,getattr() 还可以指定默认值:

1
print(getattr(s, 'gender', '未知'))

这些函数在框架、反射式编程和通用工具函数中很常见。


八、实例属性和类属性:到底该放在哪里?

学习类时,另一个很容易混淆的问题是:

属性到底应该写在 __init__() 里,还是直接写在类里面?

这就涉及 实例属性类属性 的区别。

8.1 实例属性:属于每个对象自己
1
2
3
class Student:
def __init__(self, name):
self.name = name

这里的 self.name 是实例属性。

也就是说:

  • 每个对象都会有自己的 name
  • 不同对象的值互不影响。

例如:

1
2
3
4
5
s1 = Student('Alice')
s2 = Student('Bob')

print(s1.name)
print(s2.name)
8.2 类属性:属于整个类共享
1
2
3
4
5
class Student:
school = 'Python实验学校'

def __init__(self, name):
self.name = name

这里的 school 是类属性。

它的特点是:

  • 属于类本身;
  • 所有实例默认共享这份数据。

使用时:

1
2
3
4
5
6
7
print(Student.school)

s1 = Student('Alice')
s2 = Student('Bob')

print(s1.school)
print(s2.school)
8.3 实例属性会遮蔽同名类属性

这是一个常见考点。

1
2
3
4
5
6
7
8
9
10
class Student:
school = 'Python实验学校'


s = Student()
print(s.school)

s.school = '新的学校'
print(s.school)
print(Student.school)

这里发生了什么?

  • 一开始 s.school 会去类上找 school
  • s.school 赋值后,相当于给实例创建了一个同名实例属性;
  • 这个实例属性会遮蔽类属性。

所以:

  • s.school 变成了 '新的学校'
  • Student.school 仍然保持原值。
8.4 什么时候适合用类属性?

适合放成类属性的内容,一般具有这些特点:

  • 对所有实例都相同;
  • 更像“配置”或“默认值”;
  • 不属于某个单独对象的私有状态。

例如:

  • 学校名称;
  • 默认语言;
  • 计数器初始配置;
  • 常量级别的数据。

而像 namescoreage 这种明显属于每个对象自己的数据,通常就应该放在实例属性里。

8.5 一个常见陷阱:可变类属性

如果类属性是可变对象,例如列表或字典,就要特别小心。

1
2
class Student:
tags = []

这意味着所有实例共享同一个列表。

1
2
3
4
5
s1 = Student()
s2 = Student()

s1.tags.append('优秀')
print(s2.tags)

此时 s2.tags 也会看到同样的内容。

如果你本来希望每个实例有自己的列表,那就应该写到 __init__() 中:

1
2
3
class Student:
def __init__(self):
self.tags = []

这在实际开发里是非常常见的坑。


九、综合示例:用面向对象方式描述学生系统

为了把前面的内容串起来,下面写一个稍微完整一点的小例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Student:
school = 'Python实验学校'

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

def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('成绩必须在 0 到 100 之间')

def print_score(self):
print(f'{self.__name}: {self.__score}')

def is_passed(self):
return self.__score >= 60


class GraduateStudent(Student):
def print_score(self):
print(f'研究生 {self.get_name()} 的成绩是 {self.get_score()}')

使用:

1
2
3
4
5
6
7
8
9
alice = Student('Alice', 95)
bob = GraduateStudent('Bob', 88)

alice.print_score()
print(alice.is_passed())

bob.print_score()
print(isinstance(bob, Student))
print(Student.school)

这个例子中包含了:

  • 类和实例StudentGraduateStudent 以及具体对象;
  • 封装:用 __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 面向对象编程中最基础、最核心的内容:

  1. 类和实例:类是抽象模板,实例是具体对象;
  2. __init__()self:用于初始化对象并表示当前实例;
  3. 封装:把数据和行为组织在同一个类中;
  4. 访问限制:通过双下划线和方法控制对象内部状态;
  5. 继承:让子类复用并扩展父类能力;
  6. 多态:同一个接口在不同对象上呈现不同表现;
  7. 对象信息获取:使用 type()isinstance()dir() 等工具理解对象;
  8. 实例属性和类属性:区分“对象私有状态”和“类共享数据”。

如果说前面的模块这一章是在学习“如何组织多个 Python 文件”,那么这一章就是在学习:

如何把程序中的数据和行为抽象成对象,并让对象之间形成清晰的协作关系。

掌握这部分之后,你就能开始读懂很多真实项目中的类定义,也为下一步学习面向对象高级编程打下了基础。

下一篇继续往后写时,最自然的衔接就是:

  • 面向对象高级编程__slots__@property、多重继承、定制类、枚举类、元类。

参考资料