简介:这份Python实验报告面向正在学习面向对象程序设计的高校学生与自学者,聚焦类与对象、封装、继承、多态等核心概念,帮助读者通过完整实验案例理解并实践Python面向对象编程的基本原理与技巧。资源包内含1个doc文档,大小约323KB,内容涵盖实验目的、实验内容、核心代码与运行结果,结构完整,便于对照学习。文档以交通工具Vehicle类为主线,讲解成员变量、构造函数、析构函数、静态变量、类方法及成员函数的定义与调用,并进一步通过抽象类与Car、Airplane子类演示继承与多态的实现方式,配合map函数与列表推导式展示多态调用过程。目前已有3138人学习下载,适合需要完成课程实验、巩固面向对象基础或准备相关练习的读者参考,可帮助读者快速掌握类的创建、对象实例化及继承多态的实际运用。
1. 从一份 Python 实验报告说起:面向对象到底该怎么落地
很多人第一次接触 Python 面向对象程序设计,都是从一份实验报告开始的。你手里这份Python实验一 面向对象程序设计实验.doc就是典型场景:它把类、对象、封装、继承、多态、构造函数、析构函数、静态变量、类方法这些概念全塞进两个实验任务里,逼着你从零把Vehicle类写出来,再拆成Car和Airplane两个子类跑多态。听起来像教科书作业,但真正动手的人会发现,这份文档里藏着不少能直接复用到实际项目里的设计思路——比如用静态变量做全局配置、用抽象基类约束接口、用map批量驱动对象。它适合正在学 Python 面向对象的新手照着敲一遍,也适合工作几年但没系统整理过 OOP 基础的熟手拿来查漏补缺。下面我就按这份实验的脉络,把每一步拆开讲清楚,包括代码怎么写、参数怎么传、哪里容易翻车。
2. 拆解 Vehicle 类:构造函数、析构函数与静态变量的真实用法
2.1 为什么先定义一个带静态变量的 Vehicle 类
实验第一部分要求定义一个Vehicle类,包含品牌brand和型号type两个成员变量,并且明确要求“通过静态变量方式设置品牌”。这里有个容易混淆的点:Python 里没有像 Java 那样的static关键字,所谓静态变量就是定义在类体里、不在__init__里的类属性。它的特点是所有实例共享同一份,修改类属性会影响所有对象。实验里让构造函数通过静态变量设置品牌,其实是在模拟“全局默认配置”的场景——比如你写一个车辆管理系统,所有车辆默认品牌可以先设成某个值,后续再单独覆盖。
先看核心代码 1 的原始写法:
class Vehicle(): def __init__(self, brand, type): self._brand = brand self._type = type def __del__(self): print(self._brand, "is deleted") self._brand = '' def set_brand(this, brand): this._brand = brand def out(self): return self._brand, self._type def main(): v = Vehicle("audi", "A6") print(v.out()) v.__del__() v.set_brand('Volkswagen') print(v.out()) v.set_brand('audi') print(v.out()) if __name__ == '__main__': main()这段代码能跑,但有几个细节值得说。__init__里self._brand和self._type用了单下划线前缀,这是 Python 的约定:告诉调用者“这是内部属性,别直接碰”。__del__是析构函数,对象被垃圾回收时自动调用,这里手动调了一次v.__del__(),其实不太推荐——手动调析构容易导致重复释放,后面避坑章节会细说。set_brand的第一个参数写成了this,虽然能跑,但 Python 社区约定俗成用self,换成this会让读代码的人愣一下。
参数说明:brand和type都是字符串,传入时注意不要传None,否则out()返回的元组里会出现None,打印出来不好看。set_brand接收一个字符串参数,直接覆盖_brand。
2.2 静态变量和类方法:怎么改才不影响已有对象
实验里还要求“定义一个类方法,在其中更新交通工具类的静态变量品牌和型号”。类方法用@classmethod装饰,第一个参数是cls,代表类本身。下面是我一般会写的版本:
class Vehicle: # 类属性,所有实例共享 default_brand = "unknown" default_type = "unknown" def __init__(self, brand=None, type=None): # 如果实例没传品牌,就用类属性兜底 self._brand = brand if brand is not None else Vehicle.default_brand self._type = type if type is not None else Vehicle.default_type @classmethod def update_default(cls, brand, type): # 通过类方法修改类属性,影响后续创建的对象 cls.default_brand = brand cls.default_type = type def out(self): return self._brand, self._type # 使用 v1 = Vehicle("audi", "A6") print(v1.out()) # ('audi', 'A6') Vehicle.update_default("大众", "通用") v2 = Vehicle() print(v2.out()) # ('大众', '通用')逻辑说明:default_brand和default_type定义在类体里,是类属性。__init__里用brand if brand is not None else Vehicle.default_brand做兜底,这样创建对象时可以不传参数。update_default是类方法,通过cls.default_brand修改类属性,之后新建的对象都会用新默认值。注意:已经创建的对象v1的_brand还是'audi',因为实例属性一旦赋值就独立于类属性了。这一点实验文档里没展开,但实际写代码时经常有人以为改了类属性所有对象都会变,结果发现老对象纹丝不动,这就是踩坑点。
参数说明:update_default接收两个字符串,分别赋给cls.default_brand和cls.default_type。如果只想改品牌,可以把型号参数设成None然后加判断,但实验要求两个都更新,就保持两个参数。
提示:类属性适合放全局默认值、计数器、配置项,但不要放可变对象(比如列表),否则所有实例共享同一个列表,改一个全变。
3. 继承与多态:抽象基类、子类实现和 run 函数的配合
3.1 抽象类 Vehicle 和 abstractmethod 的约束力
实验第二部分要求定义一个抽象类Vehicle,包含抽象方法drive,然后让Car和Airplane继承并实现。Python 里抽象基类靠abc模块实现,核心代码 2 的写法是:
from abc import abstractmethod, ABCMeta class Vehicle(metaclass=ABCMeta): @abstractmethod def drive(self): pass class Car(Vehicle): def drive(self): print("驾驶员:%s\n 汽车型号:%s\n 行驶年数:%d" % ('LZG', 'AE86', 3)) class Airplane(Vehicle): def drive(self): print("机长:%s\n 乘务员数量:%d\n 乘客数量:%d" % ('lzg', 15, 66)) def run(vehicle): print(vehicle.__class__) vehicle.drive() def main(): v = [Car(), Airplane()] m = map(run, v) list(m) if __name__ == '__main__': main()这里Vehicle用metaclass=ABCMeta声明为抽象基类,drive用@abstractmethod装饰。关键点:抽象类不能直接实例化,如果你写Vehicle()会报TypeError: Can't instantiate abstract class Vehicle with abstract method drive。子类Car和Airplane必须实现drive,否则它们也是抽象类,同样不能实例化。这个约束在团队协作里很有用——你定义好接口,别人写子类时忘了实现,运行时报错比默默跑出错误结果要好。
参数说明:Car.drive里硬编码了驾驶员'LZG'、型号'AE86'、年数3;Airplane.drive里硬编码了机长'lzg'、乘务员15、乘客66。实际项目里这些应该作为实例属性在__init__里传入,但实验为了聚焦多态,简化了。run函数接收任意Vehicle子类对象,先打印类名再调drive,这就是多态:同一个run函数,传入Car输出汽车信息,传入Airplane输出飞机信息。
3.2 map 驱动对象列表:为什么用 list(m) 而不是直接打印
main里v = [Car(), Airplane()]创建了一个列表,里面是两个不同子类的实例。map(run, v)把run函数依次应用到每个元素上,返回一个迭代器。Python 3 里map是惰性的,不调用list(m)的话run根本不会执行。很多人第一次写会直接print(m),结果输出<map object at 0x...>,以为代码坏了。加上list(m)强制求值,才会看到两段输出。
如果你想让代码更直观,可以用 for 循环替代:
for vehicle in v: run(vehicle)效果一样,但可读性更好。map适合函数式风格,for适合命令式风格,选哪个看团队习惯。实验文档里用了map,你照着写没问题,但要知道list(m)这步不能省。
注意:
run函数里print(vehicle.__class__)输出的是<class '__main__.Car'>这种形式,包含模块名。如果你把类定义在别的模块里,输出会带模块路径,调试时能快速定位对象来源。
4. 避坑与排查:这份实验代码里最容易翻车的五个地方
4.1 手动调用__del__导致重复释放
现象:代码里写了v.__del__(),运行后打印一次"audi is deleted",但程序结束时又打印一次,因为对象真正被回收时 Python 还会自动调一次析构。
原因:__del__是析构函数,由垃圾回收器触发,手动调用只是普通方法调用,不会阻止后续自动调用。
解决:不要手动调__del__。如果需要在对象销毁前做清理,用close()方法或上下文管理器(with语句),把清理逻辑放进去,让调用方显式关闭。
4.2 类属性被实例属性覆盖后改不回来
现象:先创建v1 = Vehicle("audi", "A6"),然后Vehicle.update_default("大众", "通用"),再v1.out()发现还是('audi', 'A6'),以为类方法没生效。
原因:v1在__init__里已经给self._brand赋了值,实例属性优先于类属性。类方法改的是类属性,不影响已有实例的实例属性。
解决:如果希望所有对象都跟着变,就不要在__init__里给实例属性赋值,而是让out()直接读self.__class__.default_brand。或者接受这个行为——已有对象保持原值,新对象用新默认值,这本来就是类属性的语义。
4.3 抽象类忘了实现抽象方法,实例化时报错
现象:写了一个新子类Boat(Vehicle),但没写drive方法,然后Boat()报TypeError。
原因:抽象基类里@abstractmethod装饰的方法,子类必须全部实现,否则子类也是抽象类。
解决:检查子类是否漏了方法,或者给抽象方法一个默认实现(去掉@abstractmethod),但那样就失去约束意义了。常见做法是保留抽象方法,在子类里补上。
4.4map返回迭代器被消费后无法复用
现象:m = map(run, v)之后先list(m)打印了结果,再list(m)发现是空列表。
原因:map返回的迭代器只能遍历一次,第一次list(m)已经把它耗尽了。
解决:如果需要多次遍历,把结果存成列表:results = list(map(run, v)),之后对results操作。或者直接用 for 循环,每次重新遍历原列表。
4.5 字符串格式化里%d传了字符串导致报错
现象:print("行驶年数:%d" % ('3'))报TypeError: %d format: a number is required, not str。
原因:%d要求整数,传字符串会报错。实验代码里写的是3没加引号,所以没事,但如果你从输入读进来的是字符串,就会翻车。
解决:用int()转换,或者改用 f-string:print(f"行驶年数:{year}"),f-string 会自动调str(),不挑类型。
5. 进阶技巧:用 dataclass 和类型注解重写这份实验
实验代码能跑,但放在 2024 年写 Python,我一般会用dataclass和类型注解让代码更干净、更不容易出错。下面是我重写的版本,功能完全对齐实验要求,但可读性和可维护性高不少:
from abc import ABC, abstractmethod from dataclasses import dataclass, field @dataclass class Vehicle(ABC): brand: str = "unknown" type: str = "unknown" @abstractmethod def drive(self) -> None: ... @dataclass class Car(Vehicle): driver: str = "LZG" model: str = "AE86" years: int = 3 def drive(self) -> None: print(f"驾驶员:{self.driver}\n 汽车型号:{self.model}\n 行驶年数:{self.years}") @dataclass class Airplane(Vehicle): captain: str = "lzg" crew: int = 15 passengers: int = 66 def drive(self) -> None: print(f"机长:{self.captain}\n 乘务员数量:{self.crew}\n 乘客数量:{self.passengers}") def run(vehicle: Vehicle) -> None: print(type(vehicle).__name__) vehicle.drive() if __name__ == '__main__': vehicles: list[Vehicle] = [Car(), Airplane()] for v in vehicles: run(v)逻辑说明:@dataclass自动生成__init__、__repr__、__eq__,省掉手写构造函数的重复代码。Vehicle继承ABC并声明drive为抽象方法,子类必须实现。Car和Airplane各自加了业务字段,用类型注解标明类型。run函数参数标注Vehicle,IDE 能给出更好的补全提示。vehicles: list[Vehicle]是 Python 3.9+ 的泛型写法,老版本用List[Vehicle]需要从typing导入。
参数说明:brand和type有默认值"unknown",创建对象时可以不传。Car的driver、model、years都有默认值,Airplane的captain、crew、passengers同理。run接收Vehicle类型,传Car或Airplane都行,多态照常工作。
对比原实验代码,这个版本少了手动__del__、少了map的迭代器陷阱、类型清晰、字段可配置。如果你在写实验报告,可以把两个版本都放上去,对比说明为什么现代 Python 更推荐 dataclass。从那以后我每次写面向对象代码,都会先问自己:这个类能不能用 dataclass 简化?抽象方法是不是真的需要?类属性会不会被实例属性覆盖?把这三个问题过一遍,基本能避开大部分 OOP 翻车场景。希望帮到你。
本文还有配套的精品资源,点击获取