☰
Python面向对象编程入门:类与实例的核心概念与工程实践
2026/9/25 23:31:50 网站建设 项目流程

学 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 实例化一个对象时,实际会发生两件事:

  1. 通过__new__创建出一个“空壳”实例。
  2. 调用__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 = tags

tags=[]这个默认列表在函数定义时只创建一次。如果创建两个实例且都不传tags:

s1 = Student("小明") s1.tags.append("优秀") s2 = Student("小红") print(s2.tags) # ['优秀']

问题发生了:你本意是每个实例拥有独立的tags,结果它们共享了同一个默认列表。原因在于[]是一个可变对象,且只创建了一次。

正确的写法是:

class Student: def __init__(self, name, tags=None): 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, school=None): 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'之类的错误时,按下面的顺序排查最容易定位:

  1. 看报错信息里的属性名:是score、name还是别的。
  2. 看实例是从哪个类创建的:确认它是不是你预期中的类。
  3. 打印instance.__dict__:看这个实例自己有没有这个属性。如果没有,进入下一步。
  4. 打印ClassName.__dict__:看类对象上有没有这个属性。
  5. 检查__init__逻辑:有没有把属性名拼错,是不是只给部分分支赋值了,是不是用了可变默认值。
  6. 检查代码里有没有在实例上动态删除属性:del s1.score会让实例后续访问报错。
  7. 检查继承关系:如果这个类有父类,还要看父类有没有定义该属性。不过这个过程在涉及继承时更容易遇到。

这条链路看起来有点繁琐,但实际执行起来很快。大多数情况下,问题都出在“属性名不一致”或者“属性在__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.py

student.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 术语,而是多写几个“学生管理系统”“订单处理工具”这样的小项目,感受类和实例如何从定义到创建、从单个到多个、从练习到工程,一步步嵌入你的编程习惯里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询