YAOTU INSIGHTS

Python面向对象编程入门:彻底理解类与实例的核心区别

Python面向对象编程入门:彻底理解类与实例的核心区别
刚接触 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 访问属性时触发 AttributeErrorAttributeError 通常意味着实例上没有这个属性。常见原因有三个一是属性拼写错误。例如__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 fStudent(name{self.name}, age{self.age})此时再执行s Student(张三, 18) print(s)输出Student(name张三, age18)这对日常调试和日志输出很有帮助建议在每个自定义类中都实现__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类增加新方法和新属性比如加入邮箱、电话、班级排名等看看实例独立性在这过程中是如何体现的。实际运行代码比只看教程更容易加深理解。