ARTICLE DETAIL

资讯详情

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

Python类与对象全解析:从基础语法到实战避坑指南

Python类与对象全解析:从基础语法到实战避坑指南 前几天有个读者私信我说他把列表、字典、函数这些基础都过了一遍结果打开别人写的一个小项目迎面看到一大段带class的代码瞬间就懵了。这个场景我见得太多Python基础学习最尴尬的位置就在这语法本身不难难的是思维从从上到下一行行执行切换到对象之间互相调用。尤其到了系列的第十篇按学习节奏来说是时候正面硬碰类和对象了。这一篇我会把为什么要用类类底层的执行逻辑新手最容易踩的坑都拆开讲配合一个完整的成绩管理小案例争取看完就能开写。不管你是刚跟着教程装完环境还是已经能写几十行脚本这篇都值得停下来读一读。很多人会问Python里用函数加字典就能写出能跑的程序为什么非要引入类这个看似抽象的东西因为函数适合组织操作而类适合组织数据和操作在一起的状态。学到这里你的定位不再只是会写脚本的人而是要往能用代码表达业务逻辑的方向走。下面直接进入正题。1. 写了一段够用的代码之后为什么还需要类1.1 函数式写法在数据复杂后的真实痛点先看一段很常见的朴素写法。假设要管理几个学生的成绩最简单的办法是建一个字典列表students [ {name: 张三, scores: [88, 92, 79], class_name: Python基础班}, {name: 李四, scores: [91, 85, None], class_name: Python基础班}, ] def add_score(student, score): student[scores].append(score) def calc_avg(student): valid_scores [s for s in student[scores] if s is not None] return sum(valid_scores) / len(valid_scores) if valid_scores else 0 def print_student_report(student): print(f姓名{student[name]}平均分{calc_avg(student)}) for st in students: print_student_report(st)这段代码是能跑的但问题在于它默认了所有调用过 add_score 的人都知道传入的第一个参数必须是一个包含 name 和 scores 键的字典。一旦换个人接手把 scores 写成了 score或者多写了一个键程序不会马上报错只会在某个角落静默地给出错误结果。你会发现数据和操作之间没有强制绑定关系全靠写代码的人的自觉。1.2 类解决的正是约束和归属问题类做的事情可以理解成把数据和作用在这组数据上的动作装进同一个容器并设定明确的规则。你不再往函数里传一个裸字典而是传一个 Student 对象。对象内部有 scores有 name同时有 add_score 和 calc_avg 方法。方法天然就知道了自己该读哪份数据调用方也省去了传这个、传那个的啰嗦过程。举个例子用类的版本可以写得更像对一个真实学生下达指令class Student: def __init__(self, name, class_name): self.name name self.class_name class_name 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 calc_avg(self): valid [s for s in self.scores if s is not None] return sum(valid) / len(valid) if valid else 0 def report(self): print(f姓名{self.name}班级{self.class_name}平均分{self.calc_avg():.2f})你现在调用 new_student.add_score(90)实际上是在告诉这个学生对象你给自己加一条90分的成绩记录。数据修改的入口被收窄到类内部你能在做任何修改前后加校验逻辑这就是函数式写法和面向对象写法的本质区别——不是行数更少而是约束更强、边界更清楚。2. 一个类的解剖从 def 到 definit的思维切换2.1 类的基本骨架看起来简单但三个细节常错类的基本骨架并不复杂class Student: def __init__(self, name): self.name name def say_hello(self): print(f我是{self.name})但基础学习阶段我几乎每次带着看学生代码都能在这三行里找到下面三个细节问题的其中一个__init__这个方法是双下划线开头和结尾不是单下划线。单下划线的_init_不会被调用类能创建但不会执行任何初始化逻辑属性没定义后面调用self.name直接报 AttributeError。class Student:后面的类名强烈建议用大写字母开头的驼峰式命名如Student、ScoreManager。这和函数名的小写下划线风格区分开也是Python社区默认的PEP 8规范。新手如果类名全小写代码照样能跑但协作时别人一眼就知道这不是资深习惯。def say_hello(self)的第一个参数必须是self。这个self在调用的时候不需要手动传Python解释器会自动把当前实例传进去。因为这一点很多刚学的人会疑惑实例方法凭什么比普通函数多一个参数往下看马上解释。2.2 self 到底是谁把它想成每个对象都有一份自己的快递单理解self这一个点能顺带打通类的一大半。每次执行s1 Student(张三)Python做的事情是在内存里创建一个全新的Student实例对象自动调用__init__并在第一个位置把这个新对象传进去你在__init__里写的self.name name本质上是给这个新对象装上一个 name 属性。所以self不是关键字也不是什么魔法变量它就是一个普通的参数名只不过约定俗成叫这个。你可以把它改成this程序照样运行但没人会这么写。当你有两个实例s1和s2它们的内存地址不同各自的self在调用时分别指向自己所以s1.name和s2.name互不干扰。为了验证这一点你可以直接运行s1 Student(张三) s2 Student(李四) print(s1 is s2) # False两个对象地址不同 print(s1.name, s2.name) # 张三 李四这个每个实例拿一份自己的数据的机制就是面向对象里常说的状态。2.3 写了init却不调用它会发生什么我见过不只一个初学者定义了__init__之后反而奇怪地绕过了它s Student.__new__(Student) s.name 手工塞进去的名字这种写法能凑合出对象但完全没有意义还把自己绕晕。正确做法是直接用类名加括号的方式创建实例也就是Student(张三)。Python从创建对象到初始化是会自动按顺序执行的不需要你手动调__init__。反过来说如果你在类里定义了__init__却从不在任何地方显式调用它那这个类就没法正常完成初始化——因为通过Student()创建时Python会自动去找__init__来执行。s Student(张三) # 这一步会自动执行 __init__记住一条规律类名加括号 新建实例 自动初始化这两个动作是绑在一起的不用拆开。3. 封装、继承、多态用Python的逻辑重新理解三大特性3.1 封装不是加个下划线是明确边界教材里讲到封装动不动就说用_name表示私有属性。但Python里没有真正的私有属性下划线更多是君子协定。真正务实的封装思路是把不希望外部随便改的内部状态用方法收口而不是直接暴露一个原始属性。还是用成绩管理的例子。如果外部代码直接写s.scores.append(-5)负数成绩就进列表了。如果你在scores这个列表前面加双下划线写成self.__scores外部就不能直接访问s.__scores了其实还是可以通过_Student__scores访问但正常人不会这么干。再加上类里只提供add_score(score)方法而这个方法内部做了数值合法校验那外部想乱改也没办法。所以封装真正的价值是给数据加一道门禁。这道门禁的价值看起来平时没什么用但一旦数据量变大或多人协作好处立刻显现——所有对数据的修改都必须经过同一道校验逻辑。class Student: def __init__(self, name): self.name name self.__scores [] # 外部别直接碰这个列表 def add_score(self, score): if isinstance(score, (int, float)) and 0 score 100: self.__scores.append(score) else: raise ValueError(非法成绩)从代码风格的角度说实际项目里前缀下划线用得其实比双下划线多因为它不阻碍调试又明确传达了这是内部细节别从外面动的信号。3.2 继承的写法与该不该多层继承继承比较容易理解子类把自己当作父类的一个更具体的版本。学生分为普通学生和奖学金学生那奖学金学生就可以继承普通学生再加一个scholarship_amount属性。class ScholarshipStudent(Student): def __init__(self, name, scholarship_amount0): super().__init__(name) self.scholarship_amount scholarship_amount def report(self): base super().report() return f{base}奖学金金额{self.scholarship_amount}这里有个细节子类不一定非得定义自己的__init__。如果没定义创建子类实例时Python会自动调用父类的__init__。但如果定义了就一定要在子类__init__第一行显式调用super().__init__(...)否则父类里初始化好的name或scores就没了。我个人的建议是学习阶段继承层级最多不要超过三层。很多人为了用继承而继承搞出 A-B-C-D 这样的链条改一个方法要沿着链条找半天调试成本急剧上升。Python本身允许多重继承但新手强烈不建议碰一碰就容易踩到 MRO方法解析顺序的坑。3.3 Python的多态比别的语言更随意学过Java的人对多态的印象往往是必须有继承关系、必须有抽象方法、必须有接口实现。但Python的多态是鸭子类型——只要你有某个方法调用时就不用关心你到底是什么类。class Teacher: def report(self): return 老师报告 class Student: def report(self): return 学生报告 def show_report(obj): print(obj.report()) show_report(Teacher()) # 老师报告 show_report(Student()) # 学生报告show_report不在乎传入的是 Teacher 还是 Student只要对象有report()方法就能用。这在写通用的打印函数、导出函数时特别方便。你不需要为每种类型单独写一个分支。4. 类在真实小项目里怎么组织以成绩管理系统为例4.1 先画数据关系再想该写哪些类很多人学完类语法后不知道从哪下手拿到需求就开始敲 class结果越写越乱。这里分享一个方法上手前先问自己三个问题这个项目里有哪几类核心的东西学生、课程、成绩单本身都是候选类。每个核心东西自带哪些属性学生有姓名、班级课程有课程名、学分成绩单有学生、课程、分数。谁主动对谁发出动作比如成绩单负责记录系统负责管理学生和课程列表。按照这个思路一个简单但结构完整的成绩管理系统可以拆成这几个类Student、Course、GradeManager。GradeManager负责统一管理所有学生和成绩的录入、统计、输出。4.2 一个可以照着敲的完整案例先说明这个案例为了演示类之间的协作故意保留在单文件的规模。如果你要拆成多个 .py 文件可以看下一节的模块组织。class Student: def __init__(self, student_id, name): self.student_id student_id self.name name self.grades {} # 课程名 - 分数 def set_grade(self, course_name, score): if not isinstance(score, (int, float)) or not (0 score 100): raise ValueError(成绩必须在0-100之间) self.grades[course_name] score def avg_grade(self): if not self.grades: return 0 return sum(self.grades.values()) / len(self.grades) def info(self): return f{self.student_id} | {self.name} | 平均分: {self.avg_grade():.2f} class Course: def __init__(self, course_name, credit): self.course_name course_name self.credit credit class GradeManager: def __init__(self): self.students [] def add_student(self, student): self.students.append(student) def add_grade(self, student_id, course_name, score): for st in self.students: if st.student_id student_id: st.set_grade(course_name, score) return raise KeyError(f不存在学号为{student_id}的学生) def generate_report(self): report_lines [ 成绩单 ] for st in sorted(self.students, keylambda s: s.avg_grade(), reverseTrue): report_lines.append(st.info()) return \n.join(report_lines) manager GradeManager() s1 Student(P001, 张三) s2 Student(P002, 李四) manager.add_student(s1) manager.add_student(s2) manager.add_grade(P001, Python基础, 92) manager.add_grade(P002, Python基础, 88) print(manager.generate_report())这段代码的关注点不是功能多而是职责划分。Student只关心自己GradeManager只关心学生列表和成绩录入Course暂未深度参与但为扩展留了位。以后要加课程平均分就往GradeManager里加方法要加学生个人信息扩展就往Student里加属性互不污染。4.3 类与模块的关系一个文件放多少类合适很多新手会纠结是不是一个类就要单独放一个文件为什么教程里全是写在一个文件里的。我的建议是分情况学习阶段写在同一个文件里方便快速理解和运行到了真实项目阶段按功能模块组织比如models.py放数据类Student、Coursemanager.py放管理逻辑类main.py放入口调用。不要机械地按一个类一个文件来因为那样反而会出现大量只有十几行的碎片文件对维护并不友好。5. 新手写 class 必踩的坑报错信息与排查链路5.1 报错1TypeError:init() takes 1 positional argument but 2 were given这个报错几乎100%出现在新手第一次写类的时候class Student: def __init__(self): self.name 张三 s Student(李四)报错说__init__()只接受1个位置参数但给了2个。为什么因为self已经自动占了一个位置你定义__init__(self)就相当于对外只能接收0个参数调用时写Student(李四)就多传了一个。排查思路很简单数一下定义参数的数量然后记得self不算用户传入的参数。要在创建时传入name就得写成def __init__(self, name)。5.2 报错2AttributeError: Student object has no attribute name这个报错常见于两种情况。第一种是__init__里没给self.name赋值后面却调用了s.name第二种是__init__方法名拼错写成了def init或者def _init_导致类创建时初始化逻辑根本没执行。排查建议是一行行检查先确认__init__拼写正确再确认属性赋值语句缩进正确级联关系往往是缩进错误引起的。我还见过一种隐蔽情况在__init__里给self.student_name赋值在另一个方法里写self.name结果一调用就报属性不存在。这种属于属性命名不一致排查时用编辑器的全局搜索功能搜一下self.name和self.student_name基本一眼就能看到。5.3 案例3类变量和实例变量改一个全都变这是个比报错更坑的问题因为它不报错只是结果不对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(90) s2.add_score(80) print(s1.scores) # [90, 80]问题来了理论上s1.scores应该只有90但结果出现了80。原因在于scores []定义在__init__外面它是类的属性所有实例共享同一份列表。给self.scores.append(score)操作的是共享列表不是每个实例自己的。要把这个列表变成每个实例自己一份必须放到__init__里面class Student: def __init__(self, name): self.name name self.scores []这个坑特别阴险的地方在于只有当你创建了多个实例并且都调用append时才会暴露。如果只有一个实例结果完全正确你会误以为代码没问题。排查建议凡是每个对象各自拥有、不该共享的数据一律放进__init__用self.xxx定义不要图省事写在类体里。6. 学完类的语法之后下一步练习怎么安排6.1 先复制再改写最后自己设计我的真实建议是不要一上来就要求自己独立设计类。打开别人写好的类逐行读弄明白每个方法的意图然后在本地运行并故意破坏它看看会抛出什么错误。比如把self.scores改成scores亲眼看看所谓的 NameError 和之前说的共享变量问题到底是怎么触发的。这种破坏性练习对形成肌肉记忆很有效。然后进入改写阶段给定一个函数式写好的小程序你手动把它改写成类版本。不用太多两三百行的程序就够了。改完之后对比两版代码你会发现类版本虽然在结构上多了一些样板但在主流程里调用起来反而更清晰——因为隐藏了内部数据细节。最后再尝试从零设计。可以从你熟悉的场景入手图书馆借书管理、记账本、待办事项清单都可以。6.2 一些基于个人教学经验的小提醒学到这里建议你开始养成读报错信息的习惯。很多初学者看到 Traceback 就慌直接跳过最后一行去检查代码这是一种低效的排错方式。正确做法是先读最后一行从最后一个错误出发往上追溯。这类习惯比多记几个API更重要。还有一个小细节日常写练习时建议把.py文件放在独立的项目目录里文件名不要用class.py这种会和关键字混淆的名字更不要所有练习堆在一个test.py里。每练习一个知识点就新建一个对应目录这样的学习过程更贴近真实项目结构后面翻笔记也方便。在Python基础学习这条路上类是一个分水岭。它不在字法上为难你而是逼你想清楚数据和操作的关系。写文章的时候我特意保留了大量真实的报错场景因为我始终觉得能踩出问题、看得懂报错、自己动手解决才是把知识变成技能的唯一路径。按这个节奏练下去继续往后学习文件读写和异常处理时你就能真正体会到面向对象设计带来的好处了。
返回列表