ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python面向对象实验:从类设计到避坑的完整指南

Python面向对象实验:从类设计到避坑的完整指南

简介:这份Python实验报告面向正在学习面向对象程序设计的高校学生与自学者,聚焦类与对象、封装、继承、多态等核心概念,帮助读者通过动手实验真正理解抽象类、构造函数与析构函数、静态变量与类方法等知识点。资源包内含1个doc文档,约323KB,以实验报告形式完整呈现实验目的、实验内容、核心代码与运行结果,结构清晰便于对照学习。文档围绕交通工具Vehicle类展开,先演示成员变量、构造函数、析构函数、成员函数与类方法的定义与调用,再通过抽象类Vehicle及Car、Airplane子类展示继承与多态的实现,并借助run函数与map遍历体现不同对象对同一消息的差异化响应。目前已有3138人学习,适合需要完成课程实验、巩固面向对象编程基础或准备相关练习的读者参考借鉴。

1. 从一份实验报告说起:面向对象到底在练什么

很多人第一次拿到「Python实验一 面向对象程序设计实验.doc」这类任务时,第一反应是打开文档、照着题目把类写出来、跑通、截图、交差。但真正做过几轮之后你会发现,这个实验真正想让你练的不是「会写 class」,而是能不能用对象把现实问题拆干净。我带过几个刚入门的同学,他们能默写__init__和self,却在一个「图书借阅」或「学生选课」的小场景里把数据、行为、状态全揉进一个类,最后改一个需求就牵一发动全身。面向对象程序设计实验的价值,恰恰在于逼你把「谁拥有数据、谁负责行为、谁和谁交互」这三件事想清楚。这篇笔记就按一线做法,把这份实验从概念、设计、编码到排错完整走一遍,适合刚学完语法、准备动手写第一个像样类结构的人,也适合想回头把基础打牢的熟手。

2. 先想清楚对象再写代码:类、属性、方法的拆分逻辑

2.1 为什么不能上来就写 class

新手最容易翻车的地方,是拿到题目直接开编辑器,边想边写,最后得到一个几百行、什么都干的「上帝类」。面向对象的第一原则是职责单一:一个类只描述一类事物,只做这一类事物该做的事。实验里常见的场景无非是学生、课程、图书、订单、账户这几类,每一类对应一个类。判断标准很简单——如果一句话里出现「学生选课」,那「选」这个动作要么属于学生,要么属于课程,取决于谁持有选课列表。我一般会先在纸上画三列:类名、它持有的数据、它能做的事。三列填完,类结构基本就定了,再动手写代码,返工率能降一大半。

2.2 属性该放哪、方法该给谁

属性归属的判断依据是「谁的状态」。学生的姓名、学号属于学生;课程的容量、已选人数属于课程。方法归属则看「谁发起这个动作」。选课这个动作,如果由学生发起,就写成Student.select(course),内部再去调用course.add_student(self)。这样两个类各自维护自己的数据,谁都不越界。实验里经常要求统计、查询、修改,这些操作不要塞进实体类,可以单独抽一个管理类,比如CourseManager,专门负责增删查。实体类管状态,管理类管流程,这是最稳的分工。

2.3 用最小代码验证类结构

设计完先别写完整逻辑,用一段最小代码把类骨架和交互跑通,确认职责没串。下面这段是常见的学生选课骨架:

class Student: def __init__(self, sid, name): self.sid = sid # 学号,唯一标识 self.name = name # 姓名 self.courses = [] # 已选课程列表,属于学生自己的状态 def select(self, course): # 学生发起选课,把自身交给课程去登记 if course.add_student(self): self.courses.append(course) return True return False class Course: def __init__(self, cid, name, capacity): self.cid = cid # 课程号 self.name = name # 课程名 self.capacity = capacity # 容量上限 self.students = [] # 已选学生列表,属于课程自己的状态 def add_student(self, student): # 课程负责判断容量,容量满则拒绝 if len(self.students) >= self.capacity: return False self.students.append(student) return True

这段代码的关键在于:容量判断放在Course里,因为容量是课程的状态;选课记录同时写进两边,保证双向可查。参数capacity是硬约束,改大改小直接影响行为,实验里如果要求「满员提示」,就在这里返回False让上层处理。跑通骨架后再补查询、退课、统计,思路不会乱。

2.4 继承和多态在实验里怎么用才不别扭

实验通常要求体现继承。常见做法是抽一个基类,比如Person,让Student和Teacher继承它,共享name、uid这些字段。但要注意,继承是为了复用和统一接口,不是为了炫技。如果两个类只是名字像、行为完全不同,硬套继承反而添乱。多态则体现在「同一个方法名,不同类有不同实现」,比如基类定义describe(),子类各自返回自己的描述。实验里能自然用上就用,用不上别硬凑,评审看的是设计是否合理,不是继承层数。

3. 把实验跑起来:从建类到验证的完整操作路径

3.1 环境与文件组织

这份实验用纯 Python 就能完成,不需要额外依赖。建议按下面的结构组织文件,避免所有代码堆在一个.py里:

oop_lab/ ├── models.py # 实体类:Student、Course 等 ├── manager.py # 管理类:增删查逻辑 └── main.py # 入口,构造数据并验证

models.py只放实体,manager.py只放流程,main.py负责组装和测试。这样拆分的好处是,改实体不影响流程,改流程不动实体,实验报告里也容易画类图。运行方式就是python main.py,不需要虚拟环境也能跑,但养成建虚拟环境的习惯没坏处。

3.2 构造测试数据并验证核心行为

骨架有了,接下来用真实数据把每条路径都走一遍。下面这段构造了两个学生、一门容量为 1 的课程,专门验证满员逻辑:

from models import Student, Course if __name__ == "__main__": s1 = Student("S001", "A同学") s2 = Student("S002", "B同学") c1 = Course("C001", "Python程序设计", capacity=1) print(s1.select(c1)) # True,第一个选上 print(s2.select(c1)) # False,容量已满被拒 print(len(c1.students)) # 1,课程侧记录正确 print(len(s2.courses)) # 0,被拒的学生不应有记录

这段验证的重点是最后两行:被拒绝的选课不能留下脏数据。很多同学的 bug 就出在这里——课程侧拒绝了,学生侧却已经把课程加进列表,导致两边数据不一致。参数capacity=1是故意设小,方便触发边界。实验里凡是涉及数量、余额、库存的地方,都要用这种「刚好越界」的数据去测。

3.3 退课与数据一致性

选课写完了,退课是另一半。退课要同时清理两边的记录,任何一边漏了都会导致后续统计出错。常见写法是在Student上加drop,内部调用course.remove_student(self):

class Student: # 省略 __init__ 和 select def drop(self, course): if course in self.courses: self.courses.remove(course) course.remove_student(self) return True return False class Course: # 省略 __init__ 和 add_student def remove_student(self, student): if student in self.students: self.students.remove(student)

这里用in先判断再删,避免对不存在的记录操作抛异常。参数没有额外可调的,但顺序有讲究:先删自己再通知对方,还是反过来,取决于异常处理策略。我一般先删自己,再通知对方,因为自己是动作发起方,失败概率更低。退课之后一定要重新打印两边的列表确认一致,这是最省事的自检手段。

3.4 用断言把验证固化下来

手动 print 看结果容易漏,建议把关键验证写成断言,跑一次全过就放心:

assert s1.select(c1) is True assert s2.select(c1) is False assert len(c1.students) == 1 assert len(s2.courses) == 0 s1.drop(c1) assert len(c1.students) == 0 assert len(s1.courses) == 0

断言的好处是把「预期行为」写进代码,改逻辑时一跑就知道有没有破坏原有约定。实验报告里附上这段,比截图更有说服力。参数方面没有可调的,但断言要覆盖正常路径和边界路径两类,缺一不可。

4. 避坑与排查:面向对象实验里最容易翻车的五件事

4.1 可变默认参数导致数据串台

现象:两个不同学生对象的课程列表莫名其妙共享同一份数据,选了一个另一个也跟着变。原因:在__init__里写了def __init__(self, courses=[]),Python 的默认参数只在定义时求值一次,所有实例共用同一个列表。解决:默认值一律用None,在函数体内再初始化,写成self.courses = [] if courses is None else courses。这是最经典的坑,没有之一。

4.2 self 漏写导致属性变成类变量

现象:改了一个实例的属性,另一个实例也跟着变。原因:赋值时漏了self.,比如写成name = name而不是self.name = name,结果创建的是局部变量或类变量。解决:所有实例属性赋值前都带self.,写完用print(s1.name, s2.name)对比确认。这个坑隐蔽性强,因为代码不报错,只是行为不对。

4.3 双向关系只维护了一边

现象:从学生侧能查到课程,从课程侧查不到学生,统计人数对不上。原因:选课或退课时只更新了一方的列表。解决:把「同时更新两边」封装成一个方法,任何入口都走它,不要在两处分别写。上面select和drop的写法就是为此。排查时打印两边的长度对比,不一致就是这个问题。

4.4 继承里忘了调用父类初始化

现象:子类实例缺少父类定义的属性,访问时报AttributeError。原因:子类写了__init__却没调super().__init__(...)。解决:子类初始化第一行就调父类,把公共字段传上去。如果实验里用了继承,这条几乎必踩,写完先实例化一个子类,把父类属性挨个访问一遍。

4.5 把业务判断塞进实体类导致难测试

现象:实体类里出现大量 if-else 和外部依赖,单独测试一个类要构造一堆环境。原因:职责没分清,流程逻辑混进了实体。解决:实体类只维护自身状态和最基本的行为,复杂判断抽到管理类。排查方法是问自己「这个类能不能脱离其他类单独实例化并测试」,不能就说明耦合过重。

5. 进阶技巧:让这份实验从及格变成能拿得出手

把基本功能跑通只是及格线,真正让这份面向对象实验有价值的是后面这几步。第一步是用__repr__给每个实体类加一个清晰的字符串表示,调试和打印时一眼能看懂对象内容,比默认的<object at 0x...>强太多。写法很简单,返回一个包含关键字段的字符串即可,比如f"Student({self.sid}, {self.name})"。第二步是把管理类的查询接口设计成返回新列表而不是内部列表本身,避免外部代码直接改到内部数据,这是封装意识的体现。第三步是给关键方法补上类型注解,参数和返回值都标清楚,实验报告里显得专业,自己回头看也不容易忘。

再往上一层,可以引入简单的异常处理替代返回False。比如容量满时抛一个自定义的CourseFullError,调用方用 try 捕获。这样做的好处是错误不会被忽略,坏处是调用方必须处理。实验里如果要求「友好提示」,用异常更合适;如果只是内部判断,返回布尔值更轻。两种都行,关键是一致,不要一半返回布尔一半抛异常。

验证方面,除了断言,可以写一个极简的测试函数,把所有场景串起来跑一遍,输出通过项数。不需要引入测试框架,纯 Python 就够。最后我想说的是,我早期做这类实验也翻过车,把选课逻辑写在一个类里,后来加个「退课」需求改了整整一下午。从那以后我养成了一个习惯:动手前先花十分钟把类和职责写在纸上,宁可前期慢,也不后期返工。这个习惯比任何语法技巧都值钱。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进