ARTICLE DETAIL

资讯详情

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

Python class完全指南:面向对象、继承与报错排查实战

Python class完全指南:面向对象、继承与报错排查实战 搞开发这么多年带过不少刚入行的新人发现大家对class这个概念的理解普遍卡在一个很奇怪的位置语法都认识代码也能抄但真要自己设计一个类的时候就抓瞎了。这一节内容我就把class的基本用法从头到尾捋一遍。不管你是刚学Python还是之前用过别的语言现在转过来这篇内容都能帮你把类、对象、方法、继承这些概念串成一条线。文末我还会把日常开发中遇到的那些奇奇怪怪的class相关报错整理一份排查清单保证你看完能直接上手写。1. class到底解决什么问题——先搞懂面向对象的底层逻辑1.1 用生活场景理解class和对象的关系很多教材喜欢上来就念定义类是对象的抽象对象是类的实例。这话没毛病但对初学者来说基本等于没说。我换个说法class就是一张设计图纸对象就是根据这张图纸造出来的具体房子。比如你设计了一张学生的图纸上面标了这个学生应该有哪些属性姓名、学号、成绩以及能做哪些动作选课、交作业、考试。这张图纸本身不能住人不能上课它就是一张纸。但你根据这张图纸可以造出无数个真实的学生出来——张三、李四、王五每个人都有自己的姓名和学号各自独立互不干扰。这里的学生就是类张三就是对象也叫实例。从这个例子里能总结出class的三个核心价值封装把数据属性和操作数据的方法函数打包在一起外界不需要关心内部怎么实现只需要调用暴露出来的接口。复用一张图纸可以用无数次不用每次从头写一遍同样的逻辑。扩展在已有图纸的基础上加新功能继承不会影响原来造出来的对象。1.2 class和普通函数的本质区别有人会问我写一堆函数也能实现同样的功能为什么要用class这个问题问得很好我直接说答案如果你只是临时处理一段数据、跑一次脚本纯函数完全够用不需要硬凑class。但一旦你的程序变复杂——比如需要同时管理多个具有相同结构的数据对象或者多个函数需要共享同一份状态——函数式写法就会变得非常痛苦。举个例子。不用class你要管理三个学生的成绩你得维护三个字典每个函数都得接收这些字典作为参数student1 {name: 张三, score: 85} student2 {name: 李四, score: 92} def get_score(student): return student[score] print(get_score(student1))这代码能跑但问题很多每个函数都要写清楚自己要操作哪个字典参数一多就容易传错字典的键写错了不会报错运行时才发现如果某天要给所有学生加一个年级字段你得把所有地方的字典初始化都找到并改一遍。class的核心优势就是把数据长什么样和能对这些数据做什么操作绑定到一起代码结构一目了然改一处就能全局生效。2. Python中class的基本语法与核心写法2.1 一个最小可运行的类长什么样先看最基本的定义格式class Student: name student_id def introduce(self): return f我叫{self.name}学号是{self.student_id}定义类用class关键字后面跟类名类名约定用大驼峰命名法每个单词首字母大写。类下面可以定义属性变量name、student_id和方法introduce。注意看方法定义的时候括号里写了一个self这个后面细说。光定义类还不够得实例化才能用s Student() s.name 张三 s.student_id 2024001 print(s.introduce())这里Student()就是实例化操作创建了一个具体的Student对象赋值给变量s。然后给这个对象的属性赋值再调用它的方法。运行结果就是我叫张三学号是2024001。2.2 __init__构造方法与self参数的来龙去脉上面那个例子每次都要手动给属性赋值太麻烦了。实际上绝大多数类都会定义一个__init__方法名字里外的双下划线表示这是Python内部的特殊方法。它的作用是在创建对象的时候就自动执行通常用来初始化对象的属性。class Student: def __init__(self, name, student_id): self.name name self.student_id student_id def introduce(self): return f我叫{self.name}学号是{self.student_id} s Student(张三, 2024001) print(s.introduce())创建s Student(张三, 2024001)的时候Python会在后台自动调用__init__方法并把张三传给参数name2024001传给参数student_id。注意这里有一个隐式传参实例本身作为第一个参数传给了self。所以你在__init__里写的self.name name意思就是在这个新创建的对象上存一个name属性值是传入的name。self这个名字只是个约定你可以换成别的名字但所有人都习惯用self别标新立异。到这里你应该理解了类方法第一个参数永远是这个对象自己。调用s.introduce()的时候Python会自动把s作为第一个参数传进去所以introduce定义里的self就指向了s这个对象。2.3 类变量与实例变量的区别——一个容易被忽略的坑回到之前最基本的定义方式类里面直接写的属性是类变量class Student: name # 类变量 school 第一中学 # 类变量而__init__里用self.xxx创建的是实例变量。两者的区别非常关键类变量是全类共享的所有实例访问到的是同一份数据实例变量是各管各的每个实例有自己的独立副本。class Student: school 第一中学 s1 Student() s2 Student() print(s1.school) # 第一中学 print(s2.school) # 第一中学 Student.school 第二中学 print(s1.school) # 第二中学类变量改动影响所有实例如果某个实例给类变量赋值事情就有意思了s1.school 实验中学 print(s1.school) # 实验中学 print(s2.school) # 第一中学这其实是Python动态语言的特性——给s1创建了一个新的实例属性school把原本引用的类变量给遮住了。写业务代码的时候我建议把所有跟具体对象相关的数据都放在__init__里初始化成实例变量类变量只用来放全类统一的常量。3. 继承、方法重写与魔法方法——让类真正好用起来3.1 继承的写法和super()的正确使用方式继承是class一个极其强大的特性它允许你在已有类的基础上定义新类新类自动拥有父类的属性和方法。Java、C#、Python等主流语言都支持写法大同小异。Python里继承的语法很简洁class Person: def __init__(self, name, age): self.name name self.age age def speak(self): return f{self.name}在说话 class Student(Person): def __init__(self, name, age, student_id): super().__init__(name, age) self.student_id student_id def speak(self): return f{self.name}学生在回答问题class Student(Person)表示Student继承自Person。Student类自动拥有了name、age属性和speak方法不需要重复写一遍。子类的__init__里调用了super().__init__(name, age)super()的意思是获取父类的代理对象这一行的作用就是执行父类已经写好的初始化逻辑避免你重写一遍name和age的赋值。这里我要特别强调一下子类定义了自己的__init__之后会完全覆盖父类的__init__除非你显式调用super().__init__()否则父类构造方法根本不执行。很多新手在这上面栽跟头——父类的属性一个都没初始化调用就报错。另外子类可以重写父类的方法。上面例子中Student覆盖了speak方法调用时会走子类版本。这种同一个调用不同对象有不同表现的特性就是面向对象里说的多态。3.2 魔法方法让类和Python原生语法无缝衔接Python中有大量双下划线开头结尾的方法统称魔法方法。除了__init__之外有几个很常用__str__定义print输出时的显示格式__repr__定义调试时显示的格式__len__定义len()函数的行为__eq__定义两个对象比较是否相等__lt__定义小于比较排序时会用到class Student: def __init__(self, name, score): self.name name self.score score def __str__(self): return f{self.name}: {self.score}分 def __lt__(self, other): return self.score other.score s Student(张三, 85) print(s) # 张三: 85分我经常提醒人__str__一定要尽早加上。不然你调试的时候打印一个对象看到的是__main__.Student object at 0x000001A2B3C4D5E6这种天书完全不知道里面存了什么数据。加上__str__之后哪里出了问题一眼就能看出来。4. 实战案例从零手写一个学生成绩管理系统4.1 需求分析与类设计思路说了这么多理论来一个完整案例。假设现在要写一个简单的学生成绩管理程序要求能添加学生、录入课程成绩、查询某位学生的平均分。拿到这个需求先别急着写代码。我习惯先列出来涉及的对象有哪些学生属性有姓名、课程成绩字典行为有添加成绩、计算平均分班级管理器属性有学生列表行为有添加学生、按学号查找学生为什么要单独设计一个班级类因为学生类的职责是管理单个学生自己的数据班级类是管理一堆学生的整体操作。职责划分清楚后续扩展谁都不影响谁。4.2 完整代码实现与运行效果class Student: def __init__(self, name, student_id): self.name name self.student_id student_id self.scores {} # 课程名 - 分数 def add_score(self, course, score): self.scores[course] score def average(self): if not self.scores: return 0 return sum(self.scores.values()) / len(self.scores) def __str__(self): return f{self.name}({self.student_id}) def __repr__(self): return self.__str__() class ClassManager: def __init__(self): self.students [] def add_student(self, student): self.students.append(student) def find(self, student_id): for s in self.students: if s.student_id student_id: return s return None def top_students(self, n3): sorted_students sorted(self.students, keylambda s: s.average(), reverseTrue) return sorted_students[:n] # 使用示例 manager ClassManager() s1 Student(张三, 2024001) s1.add_score(数学, 88) s1.add_score(英语, 75) manager.add_student(s1) s2 Student(李四, 2024002) s2.add_score(数学, 95) s2.add_score(英语, 91) manager.add_student(s2) print(s1.average()) # 81.5 print(manager.find(2024002)) # 李四(2024002) print(manager.top_students()) # [李四(2024002), 张三(2024001)]这段代码我故意把注释写的很精简因为逻辑本身不复杂。说一下几个细节average方法里先判断if not self.scores处理了还没有录入任何成绩的情况。否则sum一个空字典取平均值会报ZeroDivisionError这种小边界条件在真实开发里非常常见。sorted配合keylambda s: s.average()意味着按每个学生的平均分排序。这里用到了lambda匿名函数如果你还没学过也没关系大概意思就是告诉sorted函数用学生的平均分作为排序依据。这个案例麻雀虽小五脏俱全两个类、实例变量、实例方法、魔法方法、方法间的协作都有。你能把这30行代码吃透class的基本用法就掌握了七八成。5. 各种class相关报错与排查思路——一份实战避坑清单5.1 最常见的Python class报错及处理方法写class的时候报错是家常便饭。我按出现频率整理了一份清单报错信息典型原因解决办法TypeError: __init__() missing 2 required positional arguments创建实例时没有传入足够参数检查实例化代码的参数数量和__init__定义是否一致AttributeError: Student object has no attribute name没调用super().__init__()或者属性还没初始化就使用在__init__里初始化所有会用到的属性TypeError: Student() takes no arguments__init__方法写错了拼写或者缩进有问题导致没被识别为方法优先确认__init__是__init__不是int或__int__NameError: name Student is not defined类定义还没执行就尝试实例化确认类定义的代码块是否真的被执行到了TypeError: introduce() takes 0 positional arguments but 1 was given定义方法时漏写了self参数所有实例方法第一参数必须是self这里面最高频的就是漏写self。我自己教学的时候大家几乎都犯过这个错定义了一个def introduce():然后调用s.introduce()就报takes 0 positional arguments but 1 was given。理解了这个报错的含义——0个参数的方法被传了1个参数——就明白是self的问题了。5.2 Java与Kotlin等语言的class报错现象现在很多同学是Java、Kotlin、Python多语言混着写的class的报错在不同语言里表现差别很大。我看搜索引擎上有个高频报错是could not initialize class javax.imageio.ImageIO这是个典型的Java静态初始化失败问题。Java类里可以用static关键字定义静态代码块和静态变量在类首次加载时执行。如果静态初始化过程中抛出了异常JVM会把这个类标记为无法初始化状态之后任何代码尝试使用这个类都会抛Could not initialize class。解法通常是查看原始异常仔细看堆栈最底下的Caused by部分找到静态代码块里具体是哪个操作失败一般是文件找不到、依赖库缺失或环境变量不对。还有一个很经典的是failed to add class member。这常见于Java反射场景很多人以为能在运行时动态给类添加成员但Java的类结构在编译期就固定死了反射只能访问和调用已有的成员不能像Python那样随意给对象挂新属性。想实现类似效果的正路是先定义好接口再用动态代理或者用组合方式包装一层。Kotlin报错please ensure that class is marked as serializable则是因为Kotlin的序列化框架需要在编译期生成序列化器而类没有标注Serializable注解。解决方式很简单给类加上注解并导入kotlinx.serialization依赖。最后提一个看起来不太相关的vsdx installer retrieving the com class factory for component with clsid。这是Windows系统下COM组件注册出了问题应用在尝试创建某个COM对象时根据CLSID找不到对应的注册信息。通常需要重新注册DLL或修复安装程序。这种报错虽然不是编程语言层面的class但它同样是类工厂机制——Windows的COM体系里组件通过GUID注册调用方通过类工厂创建对象实例原理上和面向对象编程的class概念同根同源。5.3 排查报错的三条通用心法不管什么语言、什么报错我建议按以下顺序排查读完整堆栈而不是只看第一行。很多新手看到could not initialize class就把目光锁定在这一行却忽略了真正的根源是下面那个Caused by。Java的异常链、Python的Traceback都遵循同样的逻辑最底层的异常才是根本原因。检查环境而不是先怀疑代码。百度上一大堆报错最后发现是JDK版本不对、依赖没装全、DLL没注册。代码本身写的完全没问题。复现最小化。把出错的代码抽出来单独跑一点点加回去定位到是哪一步触发了问题。这个方法笨但极其有效。6. 写class的心得与建议——一些书本上不会写的经验6.1 什么时候用类什么时候不要用类我见过两类人一类完全不用class所有逻辑全堆函数代码写长了之后传参传到怀疑人生另一类刚学会class就什么都要写成类一个工具函数也要包一个类画蛇添足。我自己的判断标准是这样只有一组不相关的函数用模块就行别硬造类有状态需要维护、多个函数操作同一批数据用类数据和行为需要绑定、要创建多个类似对象用类只需要简单的键值对存储用字典或数据类就够了换句话说类是用来组织关联数据和操作的容器不是用来炫耀语法的。能用简单方式解决就别上复杂方案这个原则在工程上永远不会过时。6.2 几个应该尽早养成的class编码习惯第一属性尽量在__init__中集中定义。不要用着用着突然冒出一个self.xxx ...在某个方法里赋新属性这样一段时间后你根本不知道对象上有哪些属性。第二命名要动词化、语义明确。方法名用get_student_list、add_score这样的动作命名属性用名词。千万别写a1、b2这种抽象命名一个月后你自己都不认识。第三__str__方法一定写。调试时print一下对象就能看到内容能省大量时间。第四别滥用继承。继承层次太深以后会非常痛苦改一个父类方法可能牵动所有子类。实际开发中组合优先于继承是很多团队写进规范的原则。一个类需要另一个类的功能时优先考虑持有那个类的实例而不是继承它。6.3 学习方法上的建议学class最忌讳只背书不看代码。我的建议是看完这篇内容之后哪怕你觉得自己全懂了也要亲手把学生成绩管理案例敲一遍然后尝试给它加几个功能删除学生、按平均分降序输出、统计不及格人数。加功能的过程中踩几个坑你对class的理解就彻底扎实了。编程这东西看十遍不如自己动手写一遍动手跌过的跟头比什么教程都有用。下次遇到class相关的问题不管是语法疑惑还是报错排查可以先回来翻翻这篇清单。大多数坑前辈们都提前替你踩过了。
返回列表