简介:这份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, scores=None): # 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 f"Student(name={self.name!r}, id={self.student_id!r}, avg={self.average():.1f})"这段代码里有两个关键决策。第一,scores=None而不是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='李四', id='2024002', avg=85.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, balance=0): 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是一种BankAccount,GraduateStudent是一种Student。
class SavingsAccount(BankAccount): """储蓄账户:在基础账户上增加利息计算""" def __init__(self, owner, balance=0, interest_rate=0.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, balance=0, fee=1.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, fee=2.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 f"Vector({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_factory=list) 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.0@dataclass自动生成__init__、__repr__和__eq__,field(default_factory=list)解决了可变默认参数问题。代码量比手写少一半,而且不容易出错。
# 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,四个测试全绿,说明类的行为符合预期。最后那个测试专门验证默认参数独立性,这是最容易翻车的地方,值得单独写一个用例。
一个习惯:每次改完类定义,先跑测试再提交。测试不过,说明改动破坏了已有行为,要么修代码,要么修测试预期。这个习惯能帮你省下大量调试时间。希望帮到你。
本文还有配套的精品资源,点击获取