
简介这份Python实验报告面向高校计算机相关专业学生及Python初学者聚焦面向对象程序设计这一核心编程范式帮助读者通过动手实践掌握类与对象、封装、继承、多态等关键概念。资源包内含1个doc文档大小约323KB内容涵盖实验目的、实验内容、核心代码与运行结果结构完整可直接作为课程实验参考或自学练习材料。文档以交通工具Vehicle类为主线先讲解成员变量、构造函数、析构函数、静态变量与类方法的定义与使用再通过抽象类及Car、Airplane子类演示继承与多态的实现并借助run函数与map遍历展示不同对象对同一消息的差异化响应。已有3138人学习该资源读者可从中获得完整的实验代码示例、清晰的类设计思路以及面向对象编程的实践方法适合用于课后巩固、实验报告撰写参考或课堂练习对照。1. 从一份实验文档说起面向对象到底在练什么很多人第一次拿到「Python实验一 面向对象程序设计实验.doc」这类实验文档时第一反应是打开 Word 扫一眼题目然后直接开始敲class。结果往往是类写出来了继承也写了但运行报错、属性找不到、方法调用混乱最后对着报错信息发呆。问题不在语法而在于没有想清楚「这个实验到底想让我练什么」。面向对象程序设计实验的核心不是让你背class的语法而是训练一种建模思维把现实问题拆成对象把数据和行为绑在一起用继承和多态来消除重复代码。这份实验文档通常会覆盖类定义、属性与方法、封装、继承、多态、特殊方法这几个模块。它适合刚学完 Python 基础语法、准备进入工程化编程的开发者也适合工作几年但一直用面向过程写脚本、想补齐 OOP 短板的人。接下来我会按实验文档的典型结构把每一步拆到能直接复现的程度。2. 类与对象的最小闭环从定义到实例化2.1 为什么先写__init__而不是先写方法很多新手写类的顺序是先想有哪些方法再补__init__。这个顺序是反的。__init__决定了对象的初始状态也就是这个对象「出生时带着什么数据」。如果先写方法你会发现方法里用到的属性根本没定义运行时报AttributeError。正确的做法是先问自己「这个对象需要记住哪些信息」把这些信息写成__init__的参数赋值给self.xxx。然后再问「这个对象能做什么」把这些行为写成方法。class Student: 学生类封装基本信息与成绩计算 def __init__(self, name, student_id, scoresNone): # name 和 student_id 是必须传入的标识信息 self.name name self.student_id student_id # scores 用 None 作为默认值避免可变默认参数的坑 self.scores scores if scores is not None else [] def add_score(self, score): 添加一科成绩带范围校验 if not 0 score 100: raise ValueError(f成绩必须在 0-100 之间收到 {score}) self.scores.append(score) def average(self): 计算平均分空列表返回 0 if not self.scores: return 0 return sum(self.scores) / len(self.scores) def __repr__(self): # 方便调试时直接 print 对象看到关键信息 return fStudent(name{self.name!r}, id{self.student_id!r}, avg{self.average():.1f})这段代码里有两个关键决策。第一scoresNone而不是scores[]。Python 的可变默认参数只在函数定义时求值一次如果写scores[]所有不传scores的实例会共享同一个列表一个学生加成绩另一个学生的成绩也跟着变。这是血泪经验几乎每个人都踩过。第二__repr__不是必须的但它能让你在调试时少写很多print语句。参数说明name和student_id是位置参数调用时必须传scores是可选参数不传就是空列表。add_score里做了范围校验这是封装的第一层含义——对象自己保证数据的合法性而不是靠调用方自觉。2.2 实例化与调用的完整验证流程写完类之后不要急着写下一个类。先实例化几个对象把每个方法都跑一遍确认行为符合预期。# 创建两个学生对象 s1 Student(张三, 2024001) s2 Student(李四, 2024002, [85, 92, 78]) # 验证添加成绩 s1.add_score(90) s1.add_score(88) print(s1.average()) # 89.0 # 验证默认参数不共享 s3 Student(王五, 2024003) print(s3.scores) # []不是 s1 的成绩 # 验证校验逻辑 try: s1.add_score(150) except ValueError as e: print(f捕获异常{e}) # 验证 repr print(s2) # Student(name李四, id2024002, avg85.0)验证的重点有三个默认参数是否独立、校验是否生效、repr输出是否可读。如果s3.scores打印出来不是空列表说明默认参数写错了。如果add_score(150)没有抛异常说明校验逻辑有问题。这三步跑通类和对象的最小闭环就算完成了。3. 封装、继承与多态把重复代码压下去3.1 封装的边界什么该设为私有封装不是把所有属性都加双下划线。Python 没有真正的私有__xxx只是触发了名称改写name mangling变成_ClassName__xxx。封装的真正目的是对外暴露稳定的接口对内隐藏实现细节。一个实用的判断标准如果这个属性被外部直接修改会导致对象状态不一致就应该设为受保护的单下划线_xxx或私有的双下划线__xxx并提供方法或property来访问。class BankAccount: def __init__(self, owner, balance0): self.owner owner self.__balance balance # 余额不允许外部直接改 property def balance(self): 只读属性外部可以查但不能直接赋值 return self.__balance def deposit(self, amount): if amount 0: raise ValueError(存款金额必须为正数) self.__balance amount def withdraw(self, amount): if amount 0: raise ValueError(取款金额必须为正数) if amount self.__balance: raise ValueError(余额不足) self.__balance - amount这里__balance被名称改写为_BankAccount__balance外部直接account.__balance会报AttributeError。但property让account.balance可以读。注意property只给了 getter没有给 setter所以account.balance 100会报错。这就是「只读属性」的实现方式。3.2 继承什么时候该用什么时候不该用继承的常见误用是「为了复用代码而继承」。比如Student继承了Person只是因为Person有name和age。但如果Student和Person的关系不是「is-a」继承就是错的。正确的继承场景子类确实是父类的一种特化。比如SavingsAccount是一种BankAccountGraduateStudent是一种Student。class SavingsAccount(BankAccount): 储蓄账户在基础账户上增加利息计算 def __init__(self, owner, balance0, interest_rate0.02): # 调用父类初始化复用余额管理逻辑 super().__init__(owner, balance) self.interest_rate interest_rate def add_interest(self): 按当前余额计算利息并存入 interest self.balance * self.interest_rate self.deposit(interest) return interestsuper().__init__(owner, balance)是关键。它调用父类的初始化方法把owner和balance的处理逻辑复用过来。如果不写这一行self.__balance就不会被设置后续deposit和withdraw全部报错。参数说明interest_rate默认 0.02表示 2% 的年利率简化模型。add_interest里通过self.balance读取余额通过self.deposit存入利息完全复用了父类的校验逻辑。3.3 多态同一个接口不同的行为多态的价值在于调用方不需要知道具体是哪个子类只需要知道它支持某个方法。class CheckingAccount(BankAccount): 活期账户取款时收取手续费 def __init__(self, owner, balance0, fee1.0): super().__init__(owner, balance) self.fee fee def withdraw(self, amount): # 重写父类方法加入手续费逻辑 total amount self.fee if total self.balance: raise ValueError(余额不足以支付取款金额和手续费) super().withdraw(total) return amount def process_withdrawal(account, amount): 统一处理取款不关心具体账户类型 try: received account.withdraw(amount) print(f{account.owner} 取款 {received}余额 {account.balance}) except ValueError as e: print(f{account.owner} 取款失败{e}) # 多态调用 savings SavingsAccount(张三, 1000) checking CheckingAccount(李四, 1000, fee2.0) process_withdrawal(savings, 200) # 走 BankAccount.withdraw process_withdrawal(checking, 200) # 走 CheckingAccount.withdrawprocess_withdrawal不关心传入的是SavingsAccount还是CheckingAccount只要它有withdraw方法就行。这就是多态同一个函数调用根据对象实际类型执行不同的代码。CheckingAccount.withdraw里用super().withdraw(total)调用父类方法完成实际的余额扣减自己只负责手续费的计算和校验。4. 特殊方法与运算符重载让对象用起来像内置类型4.1__str__与__repr__的分工__repr__面向开发者用于调试和日志理想情况下eval(repr(obj))能重建对象。__str__面向用户用于print和字符串拼接。如果只定义了__repr__print会回退到它。如果两个都定义print用__str__交互式环境直接输入对象名用__repr__。class Vector: def __init__(self, x, y): self.x x self.y y def __repr__(self): return fVector({self.x}, {self.y}) def __str__(self): return f({self.x}, {self.y}) def __add__(self, other): if not isinstance(other, Vector): return NotImplemented return Vector(self.x other.x, self.y other.y) def __eq__(self, other): if not isinstance(other, Vector): return NotImplemented return self.x other.x and self.y other.y def __abs__(self): return (self.x ** 2 self.y ** 2) ** 0.5__add__里返回NotImplemented而不是抛异常这是 Python 的约定。当两个操作数类型不匹配时Python 会尝试调用另一边的__radd__如果都没有才抛TypeError。__eq__同理。4.2 运算符重载的验证与边界v1 Vector(1, 2) v2 Vector(3, 4) print(v1 v2) # (4, 6)走 __str__ print(repr(v1 v2)) # Vector(4, 6)走 __repr__ print(v1 Vector(1, 2)) # True print(abs(Vector(3, 4))) # 5.0 # 边界与不支持的类型相加 try: v1 5 except TypeError as e: print(f类型错误{e})验证要点v1 v2返回新对象而不是修改原对象这是运算符重载的基本原则。v1 5应该抛TypeError因为Vector.__add__返回了NotImplemented而int.__radd__也不支持Vector。如果这里没有报错而是返回了奇怪的结果说明__add__里缺少类型检查。5. 避坑与排查面向对象实验里最容易翻车的五个点5.1 可变默认参数导致实例间数据串扰现象两个实例的列表属性互相影响一个改了另一个也变。原因def __init__(self, items[])里的[]在函数定义时创建所有实例共享同一个列表对象。解决默认值用None在函数体内判断并创建新列表。self.items items if items is not None else []。5.2 继承时忘记调用super().__init__()现象子类实例化后父类定义的属性不存在调用父类方法时报AttributeError。原因子类重写了__init__但没有调用父类的初始化方法父类的属性赋值逻辑没有执行。解决在子类__init__的第一行调用super().__init__(需要的参数)。如果父类有多个按 MRO 顺序逐个确认。5.3 在__init__里做耗时操作导致测试困难现象实例化一个对象要等好几秒单元测试跑得极慢。原因__init__里做了网络请求、文件读写、复杂计算等操作。解决__init__只做属性赋值耗时操作放到单独的方法里由调用方决定何时执行。这样测试时可以跳过耗时步骤只验证属性是否正确。5.4 重写__eq__却没有重写__hash__现象对象放进set或作为dict的 key 时行为异常或者报TypeError: unhashable type。原因Python 中定义了__eq__的类__hash__会被隐式设为None导致实例不可哈希。解决如果对象需要放进集合或作为字典键同时定义__hash__通常基于用于比较的属性来算哈希。如果不需要哈希就接受不可哈希的事实不要强行加。5.5 多态调用时子类方法签名与父类不一致现象多态调用时报TypeError: withdraw() got an unexpected keyword argument。原因子类重写方法时增加了父类没有的参数或者改了参数顺序导致调用方按父类接口传参时失败。解决子类重写方法时参数列表应该与父类兼容。如果需要额外参数用关键字参数并给默认值。里氏替换原则的核心就是子类对象应该能替换父类对象而不破坏调用方逻辑。6. 从实验到工程把 OOP 实验代码改造成可测试模块实验文档里的代码通常写在一个.py文件里类定义和测试代码混在一起。这种写法交作业没问题但放到工程里就是灾难。我一般会做三件事拆文件、加类型注解、写单元测试。拆文件的原则是按职责分。models.py放类定义services.py放业务逻辑main.py放入口。类型注解不是装饰它能让 IDE 提前发现参数类型错误。# models.py from dataclasses import dataclass, field dataclass class Student: name: str student_id: str scores: list[float] field(default_factorylist) def add_score(self, score: float) - None: if not 0 score 100: raise ValueError(f成绩必须在 0-100 之间收到 {score}) self.scores.append(score) def average(self) - float: return sum(self.scores) / len(self.scores) if self.scores else 0.0dataclass自动生成__init__、__repr__和__eq__field(default_factorylist)解决了可变默认参数问题。代码量比手写少一半而且不容易出错。# test_models.py import pytest from models import Student def test_add_score_valid(): s Student(张三, 001) s.add_score(90) assert s.scores [90] def test_add_score_invalid(): s Student(张三, 001) with pytest.raises(ValueError): s.add_score(150) def test_average_empty(): s Student(张三, 001) assert s.average() 0.0 def test_default_scores_independent(): s1 Student(张三, 001) s2 Student(李四, 002) s1.add_score(90) assert s2.scores [] # 验证默认参数不共享跑pytest test_models.py -v四个测试全绿说明类的行为符合预期。最后那个测试专门验证默认参数独立性这是最容易翻车的地方值得单独写一个用例。一个习惯每次改完类定义先跑测试再提交。测试不过说明改动破坏了已有行为要么修代码要么修测试预期。这个习惯能帮你省下大量调试时间。希望帮到你。本文还有配套的精品资源点击获取