十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python面向对象编程入门:深入理解类与实例

Python面向对象编程入门:深入理解类与实例 很多同学学 Python 学到函数之后就开始接触“面向对象”这三个字。翻开资料满屏都是“封装、继承、多态”这样的概念一看就困打开代码又是class、__init__、self满天飞照着敲都敲不明白。如果这正是你现在的状态不用着急这篇文章就是来帮你跨过这道坎的。本文是 Python 面向对象编程系列教程的第一篇核心围绕两个最基础也最重要的概念——类和实例。我会先解释“为什么要引入类”再把类的定义、__init__初始化方法、self参数的原理、实例的创建与使用逐步拆解最后用一个完整的“学生信息管理”案例把整个过程串起来并附上常见报错的排查思路。无论你是刚学完 Python 基础语法的初学者还是想系统梳理 OOP 概念的开发者这篇文章都能给你一套能照着敲、敲完能跑、跑完能理解的完整教程。1. 什么是面向对象编程1.1 从面向过程说起在正式介绍类和实例之前我们先把“面向过程”和“面向对象”的差别说清楚。面向过程编程的核心思路是“按步骤办事”程序被拆成一个个函数每个函数完成一个操作数据在函数之间传来传去。写一个小脚本或者几十行的工具函数时这种写法非常直接逻辑一眼就能看明白。但程序规模变大以后问题就来了。比如你要维护一个员工管理系统需要记录每个员工的姓名、工号、薪资还要提供计算涨薪、打印信息等功能。如果只用变量和函数来组织你得同时维护好几个列表或者用字典嵌套来模拟“一个员工”的概念。一旦员工数量变多、操作变复杂代码里到处是这样的索引访问改一个数据结构就要连带改十几处函数维护成本迅速上升。这时候我们需要一种新的组织方式把“数据”和“操作这些数据的函数”绑定在一起形成一个整体。这个整体在 Python 里就是“对象”而定义这个对象的模板就是“类”。面向对象编程Object-Oriented Programming简称 OOP正是以对象为核心来组织代码的编程范式它的核心思想是把现实世界中的实体抽象成程序中的对象每个对象有自己的属性数据和行为方法。1.2 什么是类什么是实例类和实例是 OOP 中最基础的一对概念很多初学者在这里容易混淆。你可以用“模具”和“产品”来理解它们的关系类就是模具、是设计图它定义了对象应该有哪些属性、哪些方法但不代表某个具体对象。好比一张汽车设计图图纸上画了轮子、发动机、车灯但图纸本身不能开而实例是根据类创建出来的具体对象是真实存在的东西。根据同一张汽车设计图可以造出很多辆具体的汽车每一辆车的颜色、车牌号都可以不同但它们都拥有图纸规定的结构和功能。在 Python 中我们用class关键字定义一个类用“类名加一对括号”来创建实例。每创建一个实例就相当于按照设计图生产了一个具体的对象。实例之间是独立的修改一个实例的属性不会影响其他实例但因为它们来自同一个类所以拥有相同的属性和方法结构。这里有一个细节值得提前说明在 Python 里“一切皆对象”所以类本身也是一个对象程序运行时会生成一个类型对象。但日常开发中我们谈论“实例”的时候通常指的是通过类创建出来的业务对象而不是类本身。这个区别先不用深究后面学到元类metaclass时会再展开。1.3 面向对象解决了什么问题简单总结OOP 主要带来三个好处。第一是数据与行为绑定属性和操作这些属性的方法放在一起代码结构更清晰。比如员工类里既存姓名薪资又提供计算涨薪的方法调用方不需要关心数据具体存在哪个变量里。第二是代码复用类可以继承公共逻辑写在父类里子类只需要补充自己特有的部分这一篇先不展开继承但你要知道这正是类存在的意义之一。第三是抽象与扩展使用者只需要知道对象对外提供哪些方法不需要关心内部实现后续修改内部逻辑时只要对外接口不变调用方代码就无需改动。不过也要提醒一句面向对象不是银弹。简单脚本用函数式写法可能更简洁强行套一层类反而显得啰嗦。实际项目中应该根据问题复杂度选择合适的组织方式而不是为了 OOP 而 OOP。2. 环境准备与版本说明2.1 Python 版本要求本文所有示例都基于 Python 3语法在 Python 3.6 以上版本都能正常运行。如果你还在用 Python 2请务必先升级到 Python 3因为 Python 2 已经停止官方维护而且类定义的语法和字符串处理方式都有明显差异。如果你电脑上还没装 Python 环境可以到 Python 官网下载对应系统的安装包。安装时记得勾选“Add Python to PATH”这样命令行才能直接使用python命令。这里再强调一句本文面向的是类与实例的基础语法不涉及dataclass、property这类进阶特性所以你不用特意追求最新版本版本需要根据你的项目实际情况调整我们以常见环境为例重点演示配置思路。2.2 开发工具选择写 Python 代码最简单的工具是系统自带的文本编辑器加命令行但对于初学者更推荐使用带语法高亮和自动补全的 IDE。比较常见的选择包括工具特点适用场景PyCharm功能强大社区版免费正式项目开发、调试VS Code轻量配合 Python 插件日常脚本、学习练手IDLEPython 自带零配置快速验证小段代码Jupyter Notebook逐格运行适合演示数据分析、教学演示本文示例都比较短你直接用任何一款编辑器都能运行。关键是要学会在终端中执行 Python 文件因为后面的示例需要你亲眼看到运行结果。2.3 示例代码的组织方式建议你新建一个文件夹专门存放学习代码比如python_oop_demo。文章中的代码我会标注清楚文件路径方便你逐个对照练习。比如第一个示例放在python_oop_demo/01_class_basic.py第二个放在02_instance_use.py后面实战案例对应03_student_manager.py。每个文件独立运行互不干扰也方便你随时回看。3. 类的定义从最小的例子开始3.1 使用 class 关键字定义类先来看 Python 中最简单的类定义# 文件路径python_oop_demo/01_class_basic.py class Student: pass这段代码定义了一个名为Student的类。类体里目前只有一个pass表示这个类暂时不包含任何属性和方法。虽然它什么都没做但它已经是一个合法的 Python 类了你可以用它来创建实例。stu1 Student() print(stu1) print(type(stu1))运行结果大致如下__main__.Student object at 0x000001F8A2B4A3C8 class __main__.Student第一行告诉我们stu1是一个Student类型的对象后面的十六进制数字是它在内存中的地址每次运行可能不同。第二行用type()查看类型结果正是Student。这里有两个新手容易忽略的点。第一类名建议使用大驼峰命名法也就是每个单词首字母大写比如Student、BankAccount这是 Python 社区公认的风格。第二定义类不要求必须有__init__方法没有它类依然可以创建实例只是创建出来的实例不会自动初始化任何业务属性。3.2 给类添加属性想让类真正有意义我们需要给类添加属性和方法。先看最简单的属性添加方式class Student: school Python University这里的school叫类属性它属于类本身所有实例共享同一个值。访问类属性有两种方式print(Student.school) # 通过类名访问 stu1 Student() print(stu1.school) # 通过实例访问两种方式输出结果一样都是Python University。不过要注意虽然实例能访问类属性但如果你通过实例给同名属性赋值Python 会创建一份新的实例属性而不会修改类属性这个区别我们在后面的小节详细展开。3.3init初始化方法在实际业务中类属性通常用来描述“所有对象共享的默认值”而每个实例独有的数据一般通过__init__方法在创建实例时初始化。class Student: def __init__(self, name, age): self.name name self.age age__init__是一个特殊方法它会在你创建实例时自动调用。方法名前后各有两个下划线这类方法在 Python 中被称为“魔术方法”或“特殊方法”它们有固定的语义Python 解释器会在特定时机自动调用它们。__init__的语义就是“对象创建后立刻执行初始化操作”。注意虽然__init__经常被翻译成“构造函数”但它和 Java、C 里的构造器并不完全一样。严格来说__init__只负责初始化工作对象实际的内存分配发生在__new__方法里。日常开发中我们通常只在__init__里做属性赋值和初始状态设置这种说法上的差异不影响使用但能帮你更准确地理解 Python 对象的创建流程。4. self 到底是什么4.1 self 的传递过程self是 Python 类方法中第一个参数的名字。很多初学者看到self会觉得很困惑为什么每个方法都要写它写别的名字行不行先说结论self不是 Python 的关键字它只是一个约定俗成的参数名改成this、obj甚至aaa都能运行但社区强烈建议使用self因为这样代码可读性最好。那self的作用是什么呢当你在类中定义方法class Student: def __init__(self, name, age): self.name name self.age age def introduce(self): print(f我叫 {self.name}今年 {self.age} 岁)然后创建实例并调用方法stu1 Student(小明, 18) stu1.introduce()实际上stu1.introduce()在执行时Python 会自动把stu1作为第一个参数传给introduce方法。也就是说等价于下面的写法Student.introduce(stu1)这个过程是 Python 面向对象机制的核心——实例方法之所以能访问到“自己”的数据正是因为self指向了调用该方法的实例对象。你完全可以把self理解成“当前这个实例的引用”。4.2 self 与实例属性在上面的示例中self.name name这一行尤其关键。右边的name是__init__方法的参数它来自创建实例时传入的实参左边的self.name是把值绑定到当前实例上生成实例属性。如果不加self直接写name name那只是定义了一个局部变量方法结束后就消失了实例上并不会保存这个数据。# 错误示例没有使用 self 保存数据 class Student: def __init__(self, name, age): name name # 这只是局部变量 age age # 这只是局部变量 stu1 Student(小明, 18) print(stu1.name) # AttributeError: Student object has no attribute name这个例子是初学者极易踩的坑。错误信息AttributeError: Student object has no attribute name给出了非常明确的提示stu1这个对象上没有名为name的属性。问题就出在__init__没有把参数保存到实例上。记住一句话想在实例上保存数据就必须使用self.属性名 值的形式。5. 实例的创建与使用5.1 创建多个独立实例类是模板模板的意义就在于可以批量创建实例。我们继续用Student类演示# 文件路径python_oop_demo/02_instance_use.py class Student: def __init__(self, name, age): self.name name self.age age def introduce(self): print(f我叫 {self.name}今年 {self.age} 岁) stu1 Student(小明, 18) stu2 Student(小红, 19) stu1.introduce() stu2.introduce()运行结果我叫 小明今年 18 岁 我叫 小红今年 19 岁两个实例拥有各自独立的name和age。修改stu1.name不会影响stu2.name因为它们是两个不同的对象各自保存了一份属性数据。为了验证这一点可以试试stu1.name 大明 print(stu1.name) # 大明 print(stu2.name) # 小红5.2 实例属性和类属性的区别在前面我们提到过类属性这里用一个对比示例来加深理解class Student: school Python University # 类属性 def __init__(self, name, age): self.name name # 实例属性 self.age age stu1 Student(小明, 18) stu2 Student(小红, 19) # 先看共享类属性 print(stu1.school) # Python University print(stu2.school) # Python University # 通过实例修改其实创建了新的实例属性 stu1.school Another University print(stu1.school) # Another University print(stu2.school) # Python University print(Student.school) # Python University可以看到通过stu1.school Another University赋值后stu2.school和Student.school依然保持原值只有stu1拿到了新值。原因是Python 在实例上查找属性时会先找实例属性找不到再去类属性里找当你给实例赋值时Python 不会改类属性而是直接在当前实例上创建一份新的属性从此这个实例的查找结果就不再落到类属性上了。这个机制在开发中有时候会带来隐蔽的坑。比如类属性是一个列表你通过实例方法修改列表内容比如self.tags.append(...)由于没有重新赋值你改的还是类属性指向的那个列表对象所有实例都会看到变化。如果你不希望出现这种共享就应该把这类数据放到__init__里定义为实例属性。5.3 给实例动态添加属性Python 的实例属性并不要求全部在__init__中定义你也可以在实例创建之后动态添加stu3 Student(小刚, 20) stu3.score 99 # 动态添加属性 print(stu3.score)这种做法在快速原型开发中偶尔会用到但在正式项目中不太推荐。原因是随意添加属性会让对象的数据结构变得不可预期别人阅读代码时很难一眼看出对象到底有哪些字段。更合理的做法是把所有属性在__init__中显式定义即使暂时用不到也可以用None或空值先占位让对象结构一目了然。6. 实例方法、类方法与静态方法这一节本来可以放到后面再讲但因为很多初学者在了解了类和实例之后会马上被classmethod、staticmethod这类装饰器弄晕所以我在这里先做一个简单的区分。实例方法是我们前面一直在写的方法第一个参数是self它绑定到具体的实例上可以访问实例属性一个类里最常用的是实例方法。类方法使用classmethod装饰第一个参数是cls它绑定到类本身可以访问类属性但不能直接访问某个具体实例的属性。静态方法使用staticmethod装饰它和普通函数基本一样既不接收self也不接收cls只是恰好被放在类的命名空间里。下面是一个对比示例class Student: school Python University def __init__(self, name, age): self.name name self.age age def introduce(self): print(f我叫 {self.name}今年 {self.age} 岁) classmethod def get_school(cls): return cls.school staticmethod def is_adult(age): return age 18 print(Student.get_school()) # 类方法通过类名调用 print(Student.is_adult(18)) # 静态方法通过类名调用这一节先做到“知道有这三种方法、知道它们大概有什么区别”就可以了具体的应用场景会在后续文章中深入讲解。你现在只要记住操作实例数据用实例方法操作类相关状态用类方法和实例无关的辅助功能用静态方法。7. 完整实战案例学生信息管理接下来我们用一个完整的案例来串联前面所有知识点。这个案例是一个简化版的学生信息管理功能包括创建学生、添加成绩、计算平均分、打印学生信息。7.1 需求分析与类设计先把需求拆成数据和行为两个维度数据实例属性姓名、年龄、成绩列表行为实例方法添加成绩、计算平均分、打印个人信息类的结构设计如下类名Student 实例属性 name: 姓名 age: 年龄 scores: 成绩列表初始为空 实例方法 add_score(score): 添加一门成绩并校验成绩范围 get_average(): 计算平均分 introduce(): 打印个人信息这里的设计思路是把学生这个实体抽象成一个类学生自身的姓名、年龄和成绩属于数据而添加成绩、计算平均分这些操作属于行为。数据和行为放在一起调用方就不需要关心成绩列表是怎么维护的只需要调用方法即可。7.2 编写完整代码# 文件路径python_oop_demo/03_student_manager.py class Student: school Python University def __init__(self, name, age): self.name name self.age age self.scores [] def add_score(self, score): if score 0 or score 100: print(f成绩 {score} 无效请输入 0 到 100 之间的数值) return self.scores.append(score) print(f已为 {self.name} 添加成绩{score}) def get_average(self): if not self.scores: return 0 return sum(self.scores) / len(self.scores) def introduce(self): avg self.get_average() print(f姓名{self.name}年龄{self.age}平均分{avg:.2f}) if __name__ __main__: stu Student(小明, 18) stu.add_score(85) stu.add_score(92) stu.add_score(78) stu.introduce() stu.add_score(105) # 测试非法成绩7.3 运行与验证在终端中执行cd python_oop_demo python 03_student_manager.py预期输出已为 小明 添加成绩85 已为 小明 添加成绩92 已为 小明 添加成绩78 姓名小明年龄18平均分85.00 成绩 105 无效请输入 0 到 100 之间的数值这个案例虽然简单但已经包含了一个类的基本要素__init__初始化属性、实例方法操作属性、方法之间的互相调用、入参校验。你可以在__main__下面继续创建其他学生实例观察不同实例之间数据互不干扰的效果。7.4 案例中的工程细节有几个细节值得展开说明。第一add_score做了成绩范围校验这是对输入数据进行约束的好习惯避免脏数据进入对象内部这类校验在真实项目中通常会配合异常抛出而不是简单地打印提示。第二get_average在成绩列表为空时返回 0避免了除零错误这是一种防御性编程思路实际项目中也可以根据业务需求返回None或抛出异常。第三introduce方法内部调用了get_average体现了方法之间可以相互协作把职责拆细。最后if __name__ __main__:保护了测试代码让这个文件既能独立运行也能被其他模块导入而不会意外执行测试逻辑这是 Python 工程里非常基础但很重要的约定。8. 常见问题与排查思路8.1 忘记写 self初学者最容易犯的错误是定义方法时漏掉self参数class Student: def __init__(self, name): self.name name def introduce(): # 错误漏掉 self print(f我叫 {self.name})调用时 Python 会把实例自动作为第一个参数传入导致参数数量不匹配报错信息通常是TypeError: introduce() takes 0 positional arguments but 1 was given解决办法就是在方法定义中加入self。关键是理解Python 在调用实例方法时总会把实例本身作为第一个参数传进去所以方法的第一个位置必须留出来。8.2 AttributeError: xxx object has no attribute yyy这个错误前面提到过通常有两种原因在__init__中忘了用self.属性名 值保存数据导致实例上根本没有这个属性。代码里访问了一个从未在实例上定义的属性可能是拼写错误也可能是动态添加属性的顺序问题。排查顺序是先看__init__里是否初始化了该属性再看调用处属性名是否拼写正确。对于动态添加属性的场景要确认属性在访问之前已经被赋值。8.3 类属性被意外修改当类属性是可变对象比如列表、字典时实例方法里的“修改”可能会影响所有实例。例如class Student: scores [] # 类属性所有实例共享 def add_score(self, score): self.scores.append(score) s1 Student() s2 Student() s1.add_score(90) print(s2.scores) # [90]s2 受到了影响如果你希望每个学生都有独立的成绩列表应该把scores初始化放到__init__中。这正是前文案例里采用的做法。这个问题的本质是对“可变对象作为类属性”的理解不到位建议遇到共享数据异常时优先检查是否为可变类属性。8.4 关于init的返回值__init__不应该返回任何值如果写了returnPython 会抛出TypeError: __init__() should return None。如果你需要控制创建实例时的返回值应该使用__new__方法但那是更高级的主题日常开发很少直接覆盖它。记住__init__的职责是“初始化”不是“生产对象”。8.5 常见错误速查表问题现象常见原因解决思路TypeError: ... takes N positional arguments but M were given方法漏写 self 或参数个数不匹配检查方法定义确认第一个参数为 selfAttributeError: X object has no attribute y属性未初始化或拼写错误检查init和调用处属性名多个实例共享同一份列表可变对象被定义为类属性把可变对象放到init中初始化init() should return Noneinit中存在 return 返回值删除init中的 return如果你遇到其他报错建议先把完整的错误信息复制出来重点看错误类型和出错行数再回到上面的常见场景里对照。多练几次之后你会发现自己能很快定位问题根源。9. 最佳实践与工程建议9.1 类与方法的命名规范Python 社区的命名约定是 PEP 8 规范的核心内容。类名使用大驼峰命名法例如Student、BankAccount、UserProfile方法名和属性名使用小写下划线例如get_average、add_score、user_name常量可以使用全大写加下划线。遵循这些约定能让你的代码在团队协作中更易读也便于其他开发者快速理解你的意图。虽然 Python 解释器不会因为命名不符合规范而报错但工程项目的可维护性很大程度上取决于命名是否清晰。9.2init中显式初始化所有属性一个对象的数据结构应该尽量稳定。在__init__中将所有实例属性集中声明并赋初值好处有三点第一阅读类定义时就能看到这个对象的字段结构不需要翻遍整个类寻找属性的赋值位置第二避免某些属性在部分实例上不存在导致运行时才报AttributeError第三方便调试你可以在创建对象时立刻看出初始状态是否符合预期。如果你不确定某个属性初始值可以用None、空字符串或空列表占位但一定要写出来。9.3 入参校验与防御性编程虽然 Python 是动态类型语言参数在运行时才校验类型但我们仍然应该在方法入口对关键参数做合理性判断。比如成绩范围、年龄范围、字符串是否为空等。校验的好处是让错误尽早暴露调用方也能立刻知道问题所在而不是等到数据被污染后产生难以排查的脏数据。在实际项目中校验失败时更推荐抛出带明确信息的异常而不是简单地打印一句提示继续运行这样调用方可以主动处理错误。9.4 区分实例属性和类属性记住一个原则描述“这个类的所有对象共有的默认值”用类属性描述“每个对象自身特有的数据”用实例属性。如果把两者搞混最容易出现的问题是可变类属性被实例方法修改导致所有实例共享同一份数据。在团队协作中这种问题非常隐蔽因为代码在单实例测试时表现正常只有多个实例同时存在时才暴露。代码审查时遇到类属性建议多问一句这个值真的需要被所有实例共享吗9.5 不要滥用类面向对象不是解决所有问题的万能钥匙。如果你的类只有一两个方法而且没有明显的状态需要维护那么直接写普通函数可能更清晰。一个简单的判断标准是这个类是否有一组必须被绑定在一起的属性以及围绕这些属性的多个操作如果没有说明你并不需要一个类。工程上最怕的不是不会用 OOP而是到处强行抽象最后把简单问题搞复杂。10. 总结与下一步学习路线在本篇文章中我们完成了 Python 面向对象编程的第一站类和实例。我们理解了类和实例的区别掌握了class的定义方式搞清楚了__init__和self的作用能够创建多个互相独立的实例也踩了一遍初学者最常遇到的几个坑。最后通过学生信息管理案例把类的定义、属性初始化、实例方法调用完整走了一遍。接下来可以继续学习的方向包括实例方法、类方法、静态方法的区别与应用场景继承与多态理解子类如何复用父类逻辑私有属性与property装饰器学会控制属性的读写__str__、__repr__等常用魔术方法提升对象的可打印与调试体验以及dataclass它是 Python 3.7 引入的简化数据类定义的工具。建议你学完本篇文章后不要急着看下一篇先把案例代码自己敲一遍再尝试改动几个需求。比如给Student增加一个删除成绩的方法、增加按姓名查找学生的方法或者把学生信息保存到文件中。可以试着定义一个新的类比如BankAccount包含余额查询、存款、取款三个方法再想想取款超过余额时应该怎么处理。自己动手扩展过一遍对类和实例的理解会比单纯阅读深很多。如果本文对你有帮助欢迎收藏备用下一篇我们会继续深入面向对象的继承机制。
返回列表