☰
Python期末作业智能停车管理系统:模拟出入场与计费逻辑实现
2026/10/26 7:15:22 网站建设 项目流程

简介:这是一份面向高校学生与Python初学者的期末项目实战资源,围绕智能停车管理系统的模拟出入场业务展开,涉及GUI搭建、车牌识别、数据库存储与计费结算等核心环节,适合作为课程设计参考或练手项目。压缩包共12个文件,以8个py源码为主,辅以keep占位文件与1个说明txt,整体约17KB,体积轻量便于快速阅读与二次修改。源码按模块拆分,涵盖窗体界面、车牌识别、数据库操作、计费逻辑与测试脚本,结构清晰,能帮助读者理解从需求分析、概要设计到详细设计与测试的完整开发流程。项目采用OpenCV结合YOLO与OCR完成车牌识别,使用MySQL存储入场、出场及费用信息,并借助pygame制作操作窗体,技术栈贴近实际应用。目前已有762人学习,适合希望将Python、图像识别与数据库知识串联起来的读者参考借鉴。

1. 从一份期末作业说起:智能停车管理系统到底在模拟什么

很多同学拿到「Python期末作业-智能停车管理系统(模拟出入场)」这个题目,第一反应是去搜免费python源码大全,想直接抄一份交差。但真正动手做过的人都知道,这类题目的评分点从来不在界面多花哨,而在你有没有把「车辆进出场」这件事的状态流转讲清楚。所谓模拟出入场,本质是用程序模拟一个停车场在时间轴上的车位占用与释放过程:车来了要分配车位、记录入场时间,车走了要计算停车时长和费用、释放车位。它不需要真实道闸硬件,也不需要摄像头识别车牌,核心是一套能自洽运行的状态机加计费逻辑。适合刚学完python入门、想用一个完整项目把变量类型、字典、类、文件操作串起来的人,也适合想拿它当课程设计底稿继续扩展的熟手。

2. 需求拆解与数据结构选型:别急着写界面

2.1 先想清楚停车场里有哪些实体

动手写代码之前,我一般会先在纸上列实体。一个最简的智能停车管理系统,至少有三个核心对象:车位(ParkingSpot)、车辆(Vehicle)、停车场(ParkingLot)。车位关心的是编号和是否被占用;车辆关心的是车牌号、入场时间、出场时间;停车场则是管理者,负责把车停进空位、把车放出去并结算。

很多人一上来就写个全局列表存车牌,写到后面发现要查某辆车停了多久,只能遍历列表,代码越写越乱。这就是没做数据建模的后果。用字典按车牌号索引,用类封装车位状态,后面加功能才不会翻车。

选型上,我建议用「字典 + 类」的组合,而不是纯列表或纯字典。原因很直接:字典的键值查找是常数级,车牌号天然适合当键;类则把车位和车辆各自的属性、行为绑在一起,避免一堆散落的变量。下面这张表是我在几个版本迭代后固定下来的字段设计,可以直接抄。

实体字段类型说明
ParkingSpotspot_idint车位编号,从1开始
ParkingSpotis_occupiedbool是否被占用
ParkingSpotplatestr占用该车位的车牌,空位为None
Vehicleplatestr车牌号,唯一标识
Vehicleentry_timefloat入场时间戳,用time.time()
ParkingLotspotsdictkey为spot_id,value为ParkingSpot
ParkingLotvehiclesdictkey为plate,value为Vehicle

2.2 用类搭出最小可运行骨架

数据结构定好,就可以写骨架了。下面这段代码是能直接跑的最小版本,只包含入场和出场两个动作,没有界面,但状态流转是完整的。

import time class ParkingSpot: def __init__(self, spot_id): self.spot_id = spot_id self.is_occupied = False self.plate = None class Vehicle: def __init__(self, plate): self.plate = plate self.entry_time = None self.exit_time = None class ParkingLot: def __init__(self, total_spots): # 用字典存车位,key是编号,方便按编号直接取 self.spots = {i: ParkingSpot(i) for i in range(1, total_spots + 1)} # 用字典存在场车辆,key是车牌,避免遍历查找 self.vehicles = {} def find_empty_spot(self): # 返回第一个空车位,没有则返回None for spot in self.spots.values(): if not spot.is_occupied: return spot return None def enter(self, plate): if plate in self.vehicles: return f"车牌 {plate} 已在场内" spot = self.find_empty_spot() if spot is None: return "停车场已满" vehicle = Vehicle(plate) vehicle.entry_time = time.time() spot.is_occupied = True spot.plate = plate self.vehicles[plate] = vehicle return f"车牌 {plate} 入场,分配车位 {spot.spot_id}" def exit(self, plate): if plate not in self.vehicles: return f"车牌 {plate} 不在场内" vehicle = self.vehicles.pop(plate) vehicle.exit_time = time.time() # 找到该车牌占用的车位并释放 for spot in self.spots.values(): if spot.plate == plate: spot.is_occupied = False spot.plate = None break duration = vehicle.exit_time - vehicle.entry_time return f"车牌 {plate} 出场,停车 {duration:.2f} 秒"

这段代码里,find_empty_spot用遍历找空位,车位少的时候没问题,车位上千就要换成维护一个空闲编号队列,这是后话。enter里先判断车牌是否已在场内,避免同一辆车重复入场,这个校验很多人会漏,导致后面出场时状态错乱。exit里用pop把车辆从字典移除,同时遍历车位释放占用,注意这里必须用break,否则会把所有匹配的车位都释放掉,虽然正常情况只有一个匹配,但逻辑上要严谨。

参数方面,total_spots建议设成 10 到 50 之间做测试,太大打印结果会刷屏。时间戳用time.time()返回浮点秒数,计费时再换算成小时。如果你想让模拟更快,可以在测试时手动改entry_time,不用真的等。

2.3 计费逻辑单独抽出来

把计费写死在exit里,后面改费率就要动核心逻辑,这是常见的后悔药场景。我一般会把计费抽成一个函数或独立类,传入入场时间、出场时间和费率表,返回费用。这样期末答辩时老师问「如果改成前半小时免费呢」,你改一个函数就行。

def calculate_fee(entry_time, exit_time, rate_per_hour=5.0, free_minutes=0): duration_seconds = exit_time - entry_time duration_hours = duration_seconds / 3600 # 扣除免费时长 billable_hours = max(0, duration_hours - free_minutes / 60) # 不足一小时按一小时算,这是停车场常见规则 import math billable_hours = math.ceil(billable_hours) if billable_hours > 0 else 0 return billable_hours * rate_per_hour

rate_per_hour默认 5 元,free_minutes默认 0,这两个参数就是给你调规则用的。math.ceil实现不足一小时按一小时,如果你想让计费更精细,改成按分钟计费即可,把billable_hours换成duration_seconds / 60 * rate_per_minute。注意free_minutes扣除后可能为负,用max(0, ...)兜底,否则会算出负数费用,这种边界在测试时一定要覆盖。

3. 模拟出入场的时间推进与批量测试

3.1 用随机事件流模拟真实出入场

单个入场出场跑通只是第一步,期末作业要体现「模拟」二字,就得让程序自己产生一串进出事件。常见做法是用random模块随机生成车牌和动作,按时间顺序推进。下面这段脚本模拟 20 辆车在 100 个时间单位内的随机进出。

import random import time def simulate(lot, num_events=20): plates = [f"京A{random.randint(10000, 99999)}" for _ in range(10)] log = [] for _ in range(num_events): plate = random.choice(plates) action = random.choice(["enter", "exit"]) if action == "enter": result = lot.enter(plate) else: result = lot.exit(plate) log.append(result) # 模拟时间流逝,实际测试可以设成0 time.sleep(0.01) return log lot = ParkingLot(5) for line in simulate(lot, 20): print(line)

plates只生成 10 个车牌,但事件有 20 次,这样必然出现重复入场和未入场就出场的情况,正好用来验证你的校验逻辑是否生效。time.sleep(0.01)是为了让时间戳有差异,如果你嫌慢,直接注释掉,手动给entry_time加偏移也行。跑完观察输出,如果出现「已在场内」或「不在场内」的提示,说明边界处理起作用了。

3.2 用断言做自动化验证

光看打印结果不够,期末作业如果能附上一段自动化测试,分数会好看很多。用assert写几个关键场景,跑一遍就知道逻辑有没有崩。

def test_parking_lot(): lot = ParkingLot(2) # 场景1:正常入场出场 assert "入场" in lot.enter("京A11111") assert "出场" in lot.exit("京A11111") # 场景2:重复入场被拦截 lot.enter("京A22222") assert "已在场内" in lot.enter("京A22222") # 场景3:车位满了 lot.enter("京A33333") assert "已满" in lot.enter("京A44444") # 场景4:未入场就出场 assert "不在场内" in lot.exit("京A55555") print("全部测试通过") test_parking_lot()

这四个场景覆盖了正常流程和三类异常,是老师最常问的点。ParkingLot(2)只开两个车位,第三个入场必然触发「已满」,不用等真实车位耗尽。断言里的字符串要和enter、exit返回的提示完全一致,否则会误报失败,改提示语时记得同步改测试。

3.3 把状态持久化到文件

程序一关,数据全丢,这在答辩时会被追问。用json把车位和车辆状态存下来,下次启动能恢复,工作量不大但很加分。

import json def save_state(lot, filename="parking_state.json"): data = { "spots": {sid: {"is_occupied": s.is_occupied, "plate": s.plate} for sid, s in lot.spots.items()}, "vehicles": {p: {"entry_time": v.entry_time} for p, v in lot.vehicles.items()} } with open(filename, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def load_state(lot, filename="parking_state.json"): with open(filename, "r", encoding="utf-8") as f: data = json.load(f) for sid, info in data["spots"].items(): spot = lot.spots[int(sid)] spot.is_occupied = info["is_occupied"] spot.plate = info["plate"] for plate, info in data["vehicles"].items(): v = Vehicle(plate) v.entry_time = info["entry_time"] lot.vehicles[plate] = v

ensure_ascii=False让中文车牌正常显示,不加会变成转义字符。indent=2方便人眼检查文件内容。注意json的键必须是字符串,所以存的时候sid会变成字符串,读的时候要int(sid)转回来,这个坑我踩过,读出来车位编号对不上,排查了半天。

4. 避坑与常见问题排查

4.1 车牌重复入场导致车位泄漏

现象:同一辆车连续调用两次enter,第二次返回「已在场内」,但车位占用数却多了一个。原因:enter里先判断了车牌是否在场,但判断通过后没有再次确认车位分配的唯一性,如果代码顺序写反,先分配车位再判断车牌,就会泄漏。解决:把车牌校验放在最前面,分配车位放在校验之后,确保任何提前返回都不会改动状态。

4.2 出场时释放了错误的车位

现象:A 车出场后,B 车的车位变成空闲,B 车还在场内却显示无车位。原因:释放车位时用spot.plate == plate匹配,但如果之前有脏数据导致两个车位存了同一车牌,break只释放第一个,另一个永远占着。解决:入场时保证一个车牌只对应一个车位,出场时用车辆对象里记录的spot_id直接定位,而不是遍历匹配。我在Vehicle里加一个spot_id字段,出场时self.spots[vehicle.spot_id]直接取,既快又不会错。

4.3 时间戳精度导致计费为零

现象:入场后立刻出场,费用显示 0 元,但按规则不足一小时应该收一小时。原因:time.time()两次调用间隔可能小于 1 秒,duration_hours接近 0,math.ceil后还是 0。解决:在计费函数里加一个最小计费单位,比如if duration_seconds > 0: billable_hours = max(1, billable_hours),或者测试时手动把entry_time减 3600。这个坑在演示时特别容易翻车,老师一看停车 0 秒收 0 元,就会追问规则。

4.4 文件读写路径不对导致状态丢失

现象:save_state执行没报错,但load_state读不到数据。原因:保存和加载时的工作目录不同,或者文件名大小写不一致。解决:用os.path.join(os.path.dirname(__file__), filename)拼绝对路径,保证读写同一个文件。另外load_state前先判断文件是否存在,不存在就跳过,避免第一次运行直接崩。

4.5 随机模拟出现负数车位

现象:批量模拟后打印剩余车位,结果是负数。原因:find_empty_spot返回None时,enter里如果没正确处理,继续执行了spot.is_occupied = True,就会报AttributeError,但如果你用try吞掉了异常,状态就乱了。解决:find_empty_spot返回None后立即return "停车场已满",不要有任何后续状态修改。测试时把车位设成 1 个,连续入场两次,看第二次是否干净地返回已满。

5. 进阶技巧:用命令行参数控制模拟规模

5.1 用 argparse 让作业更像一个工具

期末作业如果只能改代码里的数字来调参数,显得不够专业。用argparse加几个命令行参数,运行时指定车位数量和模拟事件数,演示时一条命令就能切换场景。

import argparse def main(): parser = argparse.ArgumentParser(description="智能停车管理系统模拟") parser.add_argument("--spots", type=int, default=10, help="车位总数") parser.add_argument("--events", type=int, default=20, help="模拟事件数") parser.add_argument("--rate", type=float, default=5.0, help="每小时费率") args = parser.parse_args() lot = ParkingLot(args.spots) log = simulate(lot, args.events) for line in log: print(line) print(f"剩余车位:{sum(1 for s in lot.spots.values() if not s.is_occupied)}") if __name__ == "__main__": main()

--spots控制车位规模,--events控制模拟强度,--rate留给计费扩展。运行时用python parking.py --spots 5 --events 30,不用改代码。if __name__ == "__main__"保证模块被导入时不会自动执行,方便你单独测试某个函数。

5.2 用日志代替 print 方便排查

print在模拟事件多的时候会刷屏,而且没法按级别过滤。换成logging,把入场出场记成 INFO,异常记成 WARNING,输出到文件,答辩时直接翻日志。

import logging logging.basicConfig( filename="parking.log", level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", encoding="utf-8" ) def enter_with_log(self, plate): result = self.enter(plate) if "入场" in result: logging.info(result) else: logging.warning(result) return result

filename指定日志文件,level=INFO表示 INFO 及以上都记录,format里的asctime会自动带时间戳。把enter和exit的返回值按成功失败分到 INFO 和 WARNING,排查时先看 WARNING,一眼就能找到异常事件。这个习惯我从课程设计一直用到工作项目,日志比断点调试省事得多。

5.3 一个容易被忽略的验证方法

写完别急着交,把ParkingLot的车位数设成 1,然后手动按这个顺序调用:入场 A、入场 B、出场 A、入场 B、出场 B。预期结果是:A 入场成功,B 提示已满,A 出场成功,B 入场成功,B 出场成功。如果中间任何一步提示不对,说明状态流转有漏洞。这个单线程顺序测试能覆盖 90% 的逻辑错误,比随机模拟更容易定位问题。我每次改完计费或车位分配逻辑,都会先跑一遍这个序列,确认无误再跑批量模拟。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询