刚接触 Python 面向对象编程时,大多数人最先卡住的地方,不是语法本身,而是“类”和“实例”这两个词到底在描述什么。看过很多教程,代码也能运行,但心里总觉得隔了一层。出现这种感觉很正常,因为面向对象不是 Python 独有的概念,而是一套组织代码的思想;要真正理解它,得先从“为什么要这样设计”开始,而不是直接背语法。
这篇教程会围绕“类”和“实例”展开,先从概念入手,用通俗的方式解释两者的关系,再逐步拆解class语法、__init__方法、self参数、实例属性、类属性,最后通过一个完整的学生信息管理小项目,把前面所有知识点串起来。无论你是刚学完 Python 基础语法,还是已经能用函数写脚本,都可以在阅读中跟着代码动手实验,真正搞懂面向对象的第一环。
1. 为什么要学习类和实例
1.1 从函数式代码到面向对象代码
初学 Python 时,我们习惯了这样写代码:定义一组函数,再从上到下调用它们。这种写法叫做“面向过程”,它把程序拆成一个个步骤,数据在函数之间传来传去。小规模脚本这样写没有问题,但当数据量变大、逻辑变复杂时,函数式写法会逐渐暴露出问题。
举个例子,如果要管理一个学生的信息,利用面向过程的思路,有两种常见实现方式。一种是分别用两个变量存储姓名和成绩:
name = "张三" score = 85 print(name, score)这个写法简单直接,但学生多了以后就非常痛苦:每个学生都要定义一组变量,还需要手动维护它们之间的对应关系。另一种方式是用字典:
student1 = {"name": "张三", "score": 85} student2 = {"name": "李四", "score": 92} print(student1["name"], student1["score"])用字典比散装变量好一些,数据和字段有了归属,但仍然存在问题:不同学生字典里的键名需要保持一致,同一个字典中允许添加任何数据,而且“学生”这个抽象概念并没有真正变成代码里的一种类型。你只是把两个字典放在一起,它们在语义上并没有绑定关系。
这时就需要“类”登场。类可以把“学生”抽象成一种数据类型,并且规定这种类型拥有哪些属性(比如姓名、年龄、成绩)和哪些行为(比如自我介绍、计算平均分)。之后每创建一个具体的“学生”,都是通过同一个类生成的,这就比手写字典更加规范、可靠。
1.2 类和实例的关系
很多初学者把“类”和“实例”混在一起理解,实际它们是两个不同层次的概念。
类,是一张“设计图”或“模板”。它描述了一类事物共同的特征和行为,但不具体到某一个人或某一个对象。比如“学生”是一个类,它规定了任何学生都应该有姓名、有年龄、能介绍自己,但它不会规定“这个学生的名字一定是张三”。
实例,是根据类这个模板创建出来的“具体对象”。张三、李四、王五,都是“学生”这个类的实例。每个实例拥有类中定义的属性和方法,但属性值可以各自不同,谁也不会影响谁。
可以这样类比:类就像“汽车设计图纸”,实例就是根据图纸生产出来的一辆辆实车。图纸上说明了汽车应该有方向盘、四个轮子、能启动、能刹车,但图纸本身不能开;真正能上路的是具体的车。不同的车可能有不同的颜色、不同的车牌号,但它们的设计来源是同一张图纸。
在 Python 中,创建实例的方式非常统一——调用类。形式看起来就像调用一个函数:
student1 = Student() student2 = Student()虽然现在Student还是空类,但你已经能理解这种写法的语义:student1和student2是Student类生成的两个实例。
2. 环境准备与版本说明
由于本文是 Python 基础面向对象教程,不依赖特殊的第三方库,因此环境准备非常轻量。
建议使用 Python 3.8 及以上版本。面向对象的核心语法(class、__init__、self)在 Python 3 中变化不大,但如果你的电脑上同时安装了 Python 2 和 Python 3,请务必确认自己在运行 Python 3 环境。Python 2 已经在 2020 年停止维护,而且语法上也有一些差异,不建议在新项目中使用。
在命令行中查看当前版本:
python --version如果输出类似Python 3.10.12或更高版本,说明环境没有问题。
开发工具方面,你既可以使用 PyCharm、VS Code 这类专业 IDE,也可以使用 Python 自带的 IDLE。由于本文示例大多只有几十行代码,使用哪种工具差别不大。本文推荐 VS Code,配合 Python 插件后,代码补全和调试体验都比较友好。
建议先创建一个项目目录,比如python_oop_demo,把示例代码按文件保存。这样在后续调试中,文件路径和运行方式都会更加清晰。
3. 定义类:语法与核心概念
3.1 最简单的类
先来看 Python 中最简单的类定义方式。在项目目录下新建一个demo.py文件,输入以下代码:
# 文件路径:python_oop_demo/demo.py class Student: pass student1 = Student() print(student1) print(type(student1))使用class关键字定义一个类,类名是Student,后面不需要加括号(不需要继承时)。类体目前是空的,所以用pass占位,避免语法错误。Student()表示创建这个类的一个实例,并将它赋值给变量student1。
运行结果大致如下:
<__main__.Student object at 0x7f9a3b0a5d30> <class '__main__.Student'>第一行输出里,__main__.Student object说明student1是Student类创建出来的一个对象,后面的十六进制地址是它在内存中的位置。第二行用type()查看类型,结果也是Student。
这个最简单的类没有任何属性和方法,但已经可以创建实例了。它的意义在于:Python 中的类本身是一种可实例化的类型,Student()这种写法已经体现了“调用类创建实例”这一思想。
3.2init方法的作用
上面定义的Student类没有任何数据,但在实际场景中,我们希望每个学生实例都自带姓名、年龄等信息。此时就需要在类中定义__init__方法。
__init__是一个特殊方法,也叫初始化方法,它会在实例创建后被自动调用。它并不是 Python 的构造函数(真正的构造过程由__new__和解释器协作完成),但对初学者来说,可以简单把它理解成“创建实例时用来设置初始状态的方法”。
示例:
class Student: def __init__(self, name, age): self.name = name self.age = age student1 = Student("张三", 18) print(student1.name) print(student1.age)这段代码中,__init__接收三个参数:self、name、age。创建Student("张三", 18)时,Python 会自动完成两件事:先创建这个实例,然后把实例作为第一个参数传给self,再把字符串"张三"和整数18分别传给name和age。于是,实例内部就保存了这两个属性。
运行结果:
张三 18这里最关键的理解点是self.name = name这行代码:左侧的self.name表示“把name这个属性绑定到当前实例上”,它属于某个具体的对象;右侧的name是初始化方法接收到的参数。两者同名是 Python 社区的习惯写法,但不是强制要求,你完全可以写成:
def __init__(self, student_name, student_age): self.name = student_name self.age = student_age但绝大多数项目会使用同名参数,代码更简洁,可读性也更好。
3.3 self 参数到底怎么理解
self是 Python 面向对象中最容易让新手困惑的参数。很多初学者会问:为什么定义方法时必须写第一个参数self,调用时又不用传?
原因其实很简单:一个类可以创建很多实例,同一个方法可能被不同实例调用。当实例调用方法时,Python 需要知道“当前是哪个实例在调用”,这样才能访问到该实例自己的属性。self就是当前实例本身的引用。
看一个对比示例:
class Student: def __init__(self, name, age): self.name = name self.age = age def introduce(self): print(f"我叫{self.name},今年{self.age}岁") student1 = Student("张三", 18) student2 = Student("李四", 20) student1.introduce() student2.introduce()运行结果:
我叫张三,今年18岁 我叫李四,今年20岁introduce方法内通过self.name访问属性。当student1.introduce()执行时,Python 会把student1作为self传入;当student2.introduce()执行时,传入的就是student2。因此同一个方法,在不同实例上调用,得到的结果截然不同。
self这个名字并不是 Python 关键字,换成this或其他标识符也能运行,但 PEP 8 风格指南和整个 Python 社区都约定使用self,不建议修改。
4. 创建实例与访问成员
4.1 实例化:类名加括号
创建实例的语法非常简洁,就是在类名后面加括号,并按照__init__方法指定的参数传入数据。
student = Student("王五", 22)这里的student就是Student类的实例。可以把它理解成“按图纸生产出了一台具体的车”,数据已经填充到位。
如果__init__方法要求传入多个参数,创建实例时就必须提供对应的实参,否则会报TypeError。
# 错误示例 student = Student()TypeError: Student.__init__() missing 2 required positional arguments: 'name' and 'age'这个错误提示非常清晰,它告诉我们需要补上name和age两个位置参数。
4.2 访问实例属性
创建实例后,可以使用点号.来访问实例属性,也可以修改属性值。属性的访问和修改都通过点语法完成,这与读取普通变量的方式不同,因为它明确了属性的所有者。
student = Student("王五", 22) print(student.name) student.age = 23 print(student.age)输出:
王五 23这里需要注意的是,实例属性一旦被赋值,就会一直保存在该实例上,除非你显式删除它。修改某个实例的属性,不会影响其他实例,因为每个实例都维护着自己独立的数据。
4.3 调用实例方法
实例方法就是在类中定义的普通函数,只是第一个参数是self。调用时同样使用点号:
student.introduce()在方法内部,可以读取或修改实例属性。还是那句话:所有属性都通过self来操作。
如果方法不需要访问实例数据,也可以定义成不需要self的方法吗?严格来说,实例方法的第一个参数必须是self,即使方法内部没有使用它,也要声明这个参数。否则调用时会报参数数量不匹配的错误,这一点在后面的常见错误部分会详细说明。
4.4 多个实例之间相互独立
面向对象最直观的感受,来自多个实例之间的独立性。同一个类创建出来的不同实例,拥有相同的属性结构,但属性值互不干扰。
student1 = Student("张三", 18) student2 = Student("李四", 20) student1.age = 25 print(student1.age) print(student2.age)输出:
25 20可以看到,修改student1的age,并不会影响student2。这说明类的“模板”只有一份,而实例的“数据”是各自保存的。如果在前面的代码中没有理解这一点,到了后面学习类属性和实例属性的区别时,会更容易混淆。
5. 类属性与实例属性的区别
5.1 类属性
定义在类体内部、但在任何方法之外的变量,叫做类属性。它属于“类本身”,所有实例共享同一个类属性。
class Student: school = "CSDN大学" def __init__(self, name, age): self.name = name self.age = age这里的school是一个类属性。可以通过类名直接访问:
print(Student.school)也可以通过实例访问:
student = Student("张三", 18) print(student.school)两种方式输出都是CSDN大学。类属性适合保存“所有实例都相同”的数据,比如学校名称、班级编号、默认地区等。
但有一个非常容易踩坑的点:虽然实例能读取类属性,但如果通过实例给同名属性赋值,Python 不会修改类属性,而是会为这个实例动态创建一个新的实例属性,覆盖掉类属性的访问。
student = Student("张三", 18) student.school = "另一所大学" print(student.school) print(Student.school)输出:
另一所大学 CSDN大学也就是说,student.school = "另一所大学"只是给student这个实例添加了一个school实例属性,类属性本身没有变化。如果希望通过实例修改类属性,应当使用Student.school = "xxx"这样的写法。
5.2 实例属性
实例属性通常定义在__init__方法中,使用self.属性名的形式赋值。每个实例都拥有自己的一份,互不干扰。
实例属性的特点是“见人下菜碟”。同一个学生类,张三的年龄是 18,李四的年龄是 20,它们各自的age属性是独立的。这符合真实世界的认知:不同的人有不同的年龄,而不是全班级所有人共用一个年龄。
student1 = Student("张三", 18) student2 = Student("李四", 20) print(student1.age) print(student2.age)输出:
18 205.3 动态绑定属性
Python 类与静态语言有一个明显区别:它允许给一个实例动态添加类定义中没有的属性。这个特性称作“动态绑定”。
student = Student("张三", 18) student.phone = "13800000000" print(student.phone)这段代码可以正常运行,虽然Student类中没有phone属性,但实例仍能拥有它。优点是灵活,缺点是容易带来维护成本——当你遍历所有学生时,可能有些实例有phone,有些没有,调用时稍不注意就会触发AttributeError。
在实际项目中,我建议把“必须存在的属性”都写在__init__中,动态属性只用于临时状态或特殊场景,不要把它当作常规功能频繁使用。
6. 完整实战:学生信息管理系统雏形
6.1 需求分析
为了把前面零散的知识点整合起来,这里做一个“学生信息管理系统雏形”。需求并不复杂,但非常适合练习类和实例:
- 每个学生有姓名、年龄和课程成绩列表。
- 能向学生实例中添加课程成绩。
- 能计算该学生的平均分。
- 能生成一段自我介绍文本。
- 系统维护一个班级学生列表,可以打印所有学生的信息。
这个需求天然适合用类来实现,因为“学生”是一个标准的数据类型,姓名、年龄、成绩是它的属性,计算平均分、自我介绍是它的行为。
6.2 定义 Student 类
新建student.py文件,写入以下代码:
# 文件路径:python_oop_demo/student.py class Student: school = "CSDN大学" def __init__(self, name, age): self.name = name self.age = age self.scores = [] def add_score(self, score): """添加一门课程成绩""" if not isinstance(score, (int, float)): raise ValueError("成绩必须是数字") if score < 0 or score > 100: raise ValueError("成绩必须在 0 到 100 之间") self.scores.append(score) def average_score(self): """计算平均分,没有成绩时返回 0""" if not self.scores: return 0 return sum(self.scores) / len(self.scores) def introduce(self): """生成自我介绍文本""" return f"我叫{self.name},今年{self.age}岁,来自{self.school}"这里有一个细节值得注意:school是类属性,所有学生共享;scores是实例属性,在__init__中初始化为空列表,每个学生各自维护自己的成绩。
add_score方法中做了简单的参数校验:只有数字类型且范围在 0 到 100 之间才允许加入成绩列表。这比直接self.scores.append(score)更稳妥,也展示了类方法在实际项目中应该如何承担数据校验的职责。
6.3 编写测试代码
在同一个目录下新建main.py,用于创建学生实例并验证功能:
# 文件路径:python_oop_demo/main.py from student import Student students = [] s1 = Student("张三", 18) s1.add_score(85) s1.add_score(92) s1.add_score(78) students.append(s1) s2 = Student("李四", 20) s2.add_score(90) s2.add_score(88) students.append(s2) s3 = Student("王五", 19) s3.add_score(66) s3.add_score(75) s3.add_score(82) students.append(s3) for student in students: print(student.introduce()) print(f"课程成绩:{student.scores}") print(f"平均分:{student.average_score()}") print("-" * 30)使用from student import Student导入类,然后分别创建三个学生实例,调用方法添加成绩,最后用for循环统一打印。
6.4 运行与结果
在项目目录下运行:
python main.py预期输出:
我叫张三,今年18岁,来自CSDN大学 课程成绩:[85, 92, 78] 平均分:85.0 ------------------------------ 我叫李四,今年20岁,来自CSDN大学 课程成绩:[90, 88] 平均分:89.0 ------------------------------ 我叫王五,今年19岁,来自CSDN大学 课程成绩:[66, 75, 82] 平均分:74.33333333333333 ------------------------------这个示例中,每个student对象都有自己的name、age、scores,调用同一个introduce方法却输出了不同内容,这再次验证了实例独立性的概念。同时,类属性school被所有实例一致输出,说明它确实是共享数据。
7. 常见错误与排查思路
在类和实例的学习过程中,有几类错误出现的频率非常高。我把现象、原因和解决思路整理了以下表格,再逐个展开。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
TypeError: bark() takes 0 positional arguments but 1 was given | 实例方法定义时漏写了self | 在方法定义中补上self参数 |
AttributeError: 'Student' object has no attribute 'scores' | 属性没有在__init__中初始化,或拼写错误 | 检查属性名,并在初始化方法中初始化 |
AttributeError: 'Student' object has no attribute 'name' | 创建实例后立刻访问了未定义的属性 | 确认是否调用了__init__,是否传参正确 |
| 所有实例共享同一个可变对象 | 把列表或字典作为类属性 | 可变状态应放在__init__中,作为实例属性 |
7.1 方法定义漏写 self
初学者最容易犯的错误,是定义实例方法时忘记写self。
class Dog: def bark(): print("汪汪")调用时:
dog = Dog() dog.bark()Python 会报错:
TypeError: bark() takes 0 positional arguments but 1 was given报错信息看起来很奇怪:明明调用时没有传参,为什么说给了 1 个参数?原因就在于,实例方法调用时,Python 会自动把实例本身作为第一个参数传入。类中定义的bark不接受任何参数,自然就报错了。
解决方案很简单,加上self即可:
class Dog: def bark(self): print("汪汪")7.2 访问属性时触发 AttributeError
AttributeError 通常意味着实例上没有这个属性。常见原因有三个:
一是属性拼写错误。例如__init__里写的是self.name,但访问时写成了self.nmae。
二是属性根本没有定义。例如在__init__中没有初始化scores,却在某个方法里直接调用self.scores.append(),就会报错。
三是对象类型不对。假如变量被重新赋值成了其他类型,再访问原来的属性,也会报 AttributeError。排查时一定要先确认变量的真实类型。
class Student: def __init__(self, name, age): self.name = name self.age = age s = Student("张三", 18) print(s.scores)运行:
AttributeError: 'Student' object has no attribute 'scores'原因是scores属性从未赋值。可以考虑在__init__中初始化:
def __init__(self, name, age): self.name = name self.age = age self.scores = []7.3 构造函数参数没有传全
如果__init__中定义了多个参数,实例化时漏传,会报TypeError。
s = Student("张三")TypeError: __init__() missing 1 required positional argument: 'age'这个错误通常容易排查:先看类定义里的__init__,再对比调用处,确认参数数量和顺序是否一致。
7.4 可变对象作为类属性的坑
如果把列表或字典定义成类属性,并希望通过实例来修改它,会出现“所有实例共享同一份数据”的问题。
class Student: scores = [] def __init__(self, name): self.name = name def add_score(self, score): self.scores.append(score) s1 = Student("张三") s2 = Student("李四") s1.add_score(85) s2.add_score(90) print(s1.scores) print(s2.scores)输出:
[85, 90] [85, 90]看起来很奇怪:明明只有s1添加了 85,为什么s2也有?因为scores是类属性,所有实例共享同一个列表,s1往列表中添加数据,s2看到的是同一个列表内容。
解决办法是把可变对象放在__init__中,变成实例属性:
class Student: def __init__(self, name): self.name = name self.scores = [] def add_score(self, score): self.scores.append(score)这样每个实例拥有自己独立的scores列表,互不干扰。
8. 最佳实践与工程建议
8.1 命名规范
类名通常使用大驼峰命名法,也就是每个单词首字母大写,例如Student、BankAccount、OrderService。实例名和属性名使用小写下划线命名法,例如student1、self.name、average_score。方法名同样使用小写下划线。
class BankAccount: def __init__(self, owner_name, balance): self.owner_name = owner_name self.balance = balance def get_balance(self): return self.balance这样的命名习惯能让代码一看到名字就明白类型和用途,减少阅读成本。
8.2 使用str让对象可读
直接print(student)输出的是<__main__.Student object at 0x...>,不便于调试。可以通过定义__str__方法,控制对象被print时显示的文本。
class Student: def __init__(self, name, age): self.name = name self.age = age def __str__(self): return f"Student(name={self.name}, age={self.age})"此时再执行:
s = Student("张三", 18) print(s)输出:
Student(name=张三, age=18)这对日常调试和日志输出很有帮助,建议在每个自定义类中都实现__str__。
8.3 封装:属性访问控制
Python 没有真正的私有属性,但约定以单下划线_开头的属性表示“内部使用,不要直接访问”。双下划线__开头会触发名称改写,但也不是绝对私有。更多是一种团队协作中的约定。
class Student: def __init__(self, name, age): self._name = name self._age = age如果需要限制属性的读取或修改逻辑,可以配合@property装饰器实现。这里先不展开,后续教程会单独讲解,但你应该养成一个习惯:不希望外部随意修改的数据,在命名上就要有所区分。
8.4 合理设计类,避免万能类
面向对象的核心价值是“高内聚、低耦合”。每个类应该只负责一件事。比如“学生”类负责学生本身的属性和行为,而不应该把成绩统计、文件读写、数据库连接全部塞进去。
如果你发现一个类里有太多互不相关的功能,应该考虑拆分类。例如把“学生”和“成绩管理”分离,让每个类的职责更清晰。这样代码更容易测试,也更容易维护。
9. 总结与后续学习路线
本篇文章围绕 Python 面向对象编程的第一个核心主题展开,介绍了类与实例的定义、__init__初始化方法、self参数、实例属性与类属性的区别、动态属性绑定,以及一个完整的学生信息管理示例。通过这个示例,你应该能直观感受到“类”作为模板和“实例”作为具体对象的关系。
学完类和实例后,下一步建议按以下顺序继续深入:
- 方法:实例方法、类方法(
@classmethod)、静态方法(@staticmethod)的区别。 - 继承:子类如何复用父类的代码,
super()的用法。 - 多态:同一行为在不同类上的不同表现。
- 封装与
@property:如何控制属性的读写逻辑。 - 特殊方法:
__repr__、__eq__、__lt__、__call__等。
面向对象编程并不神秘,它本质上是为了让程序结构更接近人对现实世界的认知方式。类和实例是进入这扇门的第一步,也是最重要的一步。你可以回到第一部分,动手把示例代码重新敲一遍,尝试给Student类增加新方法和新属性,比如加入邮箱、电话、班级排名等,看看实例独立性在这过程中是如何体现的。实际运行代码,比只看教程更容易加深理解。