这题“编程题:实现汽车租赁公司汽车出租方案”,我在不同的场合里见过太多版本了。大学课程设计里有它,初级编程考核里有它,甚至有些面试的笔试环节也会把题目换层皮拿出来用。它看着像一道普通的“算租金”题目,但真上手写一遍就会发现,里面牵扯到数据结构设计、面向对象建模、业务规则拆分、异常边界处理,甚至还暗藏着对“以后业务变了怎么办”的考量。说白了,这道题就是用一个大家都能理解的场景,逼你把“从需求到代码”的完整流程走一遍。
文章里我会用 Python 作为示范语言,从题目拆解、数据模型设计、计费规则制定,到一个可运行的最小系统,再到常见坑点和扩展方案,一次性讲透。无论你是在准备课程设计,还是想借这个题目练手,都能直接照着做,并且能明白每一步为什么要这么写,而不是背一段代码交差。
1. 题目到底在考什么:拆解需求与隐藏考点
1.1 表面需求很直白,但真正的需求藏在细节里
“汽车租赁公司汽车出租方案”,直接读过去无非就是:公司有一批车,顾客来租车,按某种规则收钱,最后输出一个出租方案。表面上的功能清单大概是这么几条:
- 能选择不同类型的车辆,比如轿车、SUV、商务车
- 能输入租车天数
- 能根据车型单价和租期计算出租金
- 能生成一条租赁记录或订单
但是,如果只按这个清单写代码,写出来的东西大概率是“一次性代码”:所有逻辑堆在 main 函数里,车型用 if-else 判断,租金规则写死,换个新车型就要改一遍甚至改崩原逻辑。题目真正的考点,从来不是“你会不会算乘法”,而是能不能把这个看似简单的业务抽象成清晰、可扩展、不容易出错的代码结构。
我做这道题的时候,习惯先把需求进一步拆成三块:
- 数据层:一辆车有哪些属性,一辆车和一个租赁订单怎么表示
- 规则层:租金怎么计算,优惠怎么叠加,特殊情况怎么处理
- 流程层:从用户选车到下单,再到结算费用,整个流程怎么串起来
这三块对应到程序中,就是数据结构设计、业务逻辑设计和流程控制设计。三道关卡都通过,才算真正完成了这个“出租方案”。
1.2 隐藏考点:建模能力、边界处理和可扩展性
有些同学会疑惑:这么简单的题,有必要想那么深吗?我给一个真实的观察:同样一道题,有的实现只有几十行,跑起来好像也能用;但只要面试官追加一个问题——“如果新增一种‘按小时计费’的新能源车,你怎么改?”——几十行版本马上暴露问题,要么到处加判断,要么复制粘贴逻辑。这就是可扩展性不足。
隐蔽考点大概有这几个:
- 面向对象建模:是否懂得用“类”来表示车辆和订单,而不是把所有字段塞进字典里手动维护
- 职责分离:车辆基本信息、租金计算、订单管理,是不是各自有独立的载体
- 参数校验:租期能不能是负数?一辆不存在的车能不能下单?这些边界条件必须处理
- 业务规则的灵活度:不同车型不同价、长期租赁有折扣、会员有优惠,这类规则能不能优雅地扩展
另外,还要看你是否懂得“把规则配置化”,而不是把价格直接写死在代码文件里。企业里的租金价格是市场部随时可能调整的,每调一次价格都要重新改代码、重新编译部署,那这套系统就不用要了。
所以这道题表面上是“出租方案”,本质上考的是“软件设计方案的合理性”。我从一开始就没有直接写计算函数,而是先花半小时把对象结构和规则表在脑子里勾勒清楚,这是很多新手容易跳过的步骤,恰恰也是最关键的。
2. 核心设计思路:数据模型与租金规则怎么定
2.1 车辆对象设计:从字段到类的选择
先说数据模型。车辆是租赁系统的核心实体,一辆车至少应该包含这些信息:
- 车牌号:唯一标识,用来区分每一辆车
- 车型名称:比如“经济型轿车”“豪华SUV”,方便展示
- 每日租金:最基本的计费单位
- 押金:租车通常需要收取押金,这也是业务规则的一部分
如果只是用字典表示一辆车:
car = { "plate_no": "京A12345", "model_name": "舒适型轿车", "daily_rent": 300, "deposit": 500 }这样写也能跑,而且非常直观。但问题是,如果后续要给车增加“保险费用”“排量”“座位数”等属性,字典的 key 会越来越多,代码里到处出现car["xxx"],一旦写错一个字符串,程序运行到那一步才会报错,排查成本很高。
更好的做法是用类把这个模型固定下来:
class Vehicle: def __init__(self, plate_no, model_name, daily_rent, deposit=0): self.plate_no = plate_no self.model_name = model_name self.daily_rent = daily_rent self.deposit = deposit这样做有几个直接的好处。第一,属性是明确的,IDE 能自动提示,写错字段名在写代码时就能发现。第二,可以给类添加行为方法,比如后面要写的calc_rent_fee,让数据和行为不分离。第三,如果以后有轿车、SUV、商务车的差别,可以用继承或组合来扩展,而不是在业务逻辑里写一长串if model_name == "SUV"。
有些人会问,为什么不直接用dataclass?当然可以,Python 3.7 之后用dataclass更简洁,也能自动生成构造方法:
from dataclasses import dataclass @dataclass class Vehicle: plate_no: str model_name: str daily_rent: float deposit: float = 0我用普通类来写,是因为很多教材和教学环境还停留在传统类定义方式上,方便不同基础的读者理解。实际开发中用dataclass完全没问题,效果是一样的。
2.2 租金规则怎么定:计费逻辑不能只做乘法
接下来是重头戏:租金规则。最朴素的计费方式就是“每日租金乘以天数”,但真实业务里一定会有如下变化:
- 不足一天怎么算?常见的约定是“不足一天按一天计”
- 租期较长有没有折扣?比如租满 7 天打 9 折,租满 30 天打 8 折
- 不同类型车辆是否能享受同样的折扣?有些公司会针对不同车型设不同折扣
- 要不要另收保险、手续费、押金?
我在这道题里会采用一套简洁但有代表性的规则:
- 车辆有基础日租金,作为唯一价格来源
- 租期不足 1 天按 1 天计算
- 租期为 1 到 6 天不打折
- 租期为 7 到 29 天,总租金打 9 折
- 租期达到 30 天及以上,总租金打 8 折
- 押金单独列出,不入租金
用代码表达,最直观的版本长这样:
def calc_rent_fee(vehicle, days): if days <= 0: raise ValueError("租期必须大于0天") # 不足一天按一天计算 if days < 1: days = 1 base_fee = vehicle.daily_rent * days if days >= 30: fee = base_fee * 0.8 elif days >= 7: fee = base_fee * 0.9 else: fee = base_fee return round(fee, 2)这个函数的核心逻辑很清楚,但我必须提醒一个新手常犯的错:days是整数还是浮点数?如果是浮点数,days=0.5表示半天,规则怎么算?我在代码里用days < 1把半天的场景兜住了,但这只是一个临时的约定。严谨的业务系统里,租期应该有明确的时间颗粒度,比如精确到小时,或者约定最小单位为 1 天。你要做的第一件事就是“把这个约定定下来”,而不是让程序去猜。
另一个细节是取整。费用计算里浮点数很容易出现360.00000000000006这种结果,所以最后加一个round(fee, 2),按“分”保留两位小数。这是金融类计算的基本素养,虽然这只是一道编程题,但好的习惯要一开始就养成。
2.3 为什么把规则写成配置,而不是硬编码
上面calc_rent_fee里的0.8、0.9就是“硬编码”。如果业务规则固定不变,硬编码没问题,代码还更好读。但汽车租赁行业的促销政策变化非常频繁,几乎每个月都可能调整折扣档位。这时候,频繁改业务函数就不合理了。
可配置化思路很简单:把折扣档位提取成一张规则表,用一个数据结构存放。
DISCOUNT_RULES = [ # (最少天数, 折扣率) (30, 0.8), (7, 0.9), (1, 1.0), ]然后计费函数不再写死 if 分支,而是遍历规则表:
def calc_rent_fee(vehicle, days): if days <= 0: raise ValueError("租期必须大于0天") days = max(days, 1) base_fee = vehicle.daily_rent * days for min_days, rate in DISCOUNT_RULES: if days >= min_days: return round(base_fee * rate, 2) return round(base_fee, 2)这里有个细节要注意:DISCOUNT_RULES必须按“最少天数”从大到小排序,否则会先命中低档折扣。我在规则表里手动写了(30, ...), (7, ...), (1, ...)的顺序,就是为了让逻辑按从高到低的顺序匹配。如果这张表是用户从配置文件读进来的,最好在加载后做一次排序,否则bug隐患很大。
可配置化的核心价值不是让代码变短,而是让“业务规则变化”和“程序代码修改”解耦。以后市场部改了折扣,只需要改规则表,甚至改外部配置文件,程序主体一行都不用动。这也是企业级开发里非常看重的一点,哪怕题目没有明确要求,你主动做出来,就是亮点。
3. 从零实现一个可运行的租车计价系统
3.1 定义车辆数据与基础操作
设计思路清楚了,现在直接写一个可以跑起来的完整方案。我会把代码分成几个模块,虽然都在同一个 Python 文件里演示,但结构上已经分层了。
首先是车辆类。为了演示扩展性,我定义一个基础类Vehicle,再派生出两个子类,体现不同车型的差异:
class Vehicle: def __init__(self, plate_no, model_name, daily_rent, deposit=0): self.plate_no = plate_no self.model_name = model_name self.daily_rent = daily_rent self.deposit = deposit def display(self): return f"[{self.plate_no}] {self.model_name},日租 {self.daily_rent} 元" class FamilyCar(Vehicle): # 家庭轿车:押金略低,日租经济 pass class SUV(Vehicle): # SUV:押金和日租都比轿车高 pass也许有人觉得FamilyCar和SUV的子类里什么都没写,定义出来有什么意义?意义在于:当不同车型需要不同的附加费用或库存策略时,可以在各自的子类里重写方法。比如 SUV 可能强制要求购买额外保险,那就在SUV.calc_extra_fee()里实现,而轿车类不需要。这个扩展不是臆想,是租车业务真会发生的需求。
接下来我初始化一个车辆库存列表,当作一个简单的“车场”:
vehicle_pool = [ FamilyCar("京B12345", "经济型家轿", 260, deposit=800), FamilyCar("京B67890", "舒适型家轿", 320, deposit=800), SUV("京C13579", "城市SUV", 460, deposit=1200), SUV("京C24680", "七座SUV", 560, deposit=1500), ]车辆列表用list存,已经足够满足题目场景。如果车辆数量很大,或者需要频繁按条件查询,就要考虑用dict以plate_no为主键,甚至上数据库,这里暂不展开。
3.2 租金计算核心逻辑与订单结构
计费函数沿用可配置化思路,稍微升级一下规则表,让规则的表达更丰富。我想让规则里带一个“说明文字”,这样给用户展示优惠信息时,可以直接从规则表里取:
DISCOUNT_RULES = [ {"min_days": 30, "rate": 0.8, "note": "租满30天享8折"}, {"min_days": 7, "rate": 0.9, "note": "租满7天享9折"}, {"min_days": 1, "rate": 1.0, "note": "标准日租价"}, ] def get_discount_rule(days): for rule in DISCOUNT_RULES: if days >= rule["min_days"]: return rule return {"min_days": 1, "rate": 1.0, "note": "标准日租价"} def calc_rent_fee(vehicle, days): if days <= 0: raise ValueError("租期必须大于0天") days = max(days, 1) # 不足一天按一天计 base_fee = vehicle.daily_rent * days rule = get_discount_rule(days) final_fee = round(base_fee * rule["rate"], 2) return final_fee, base_fee, rule["note"]这个函数返回了三个值:最终费用、原始费用、命中哪条折扣规则。为什么要把原始费用也返回出来?因为真实订单里要明确显示“原价多少”“优惠了多少”,不能只给一个最终数字。看似多写了一点,但这一下就让结果可解释、可验证,也方便后续打印发票或统计营收。
订单结构我用一个函数来创建,这样生成订单的逻辑是集中的,方便以后扩展:
def create_order(vehicle, days): final_fee, base_fee, note = calc_rent_fee(vehicle, days) order = { "plate_no": vehicle.plate_no, "model_name": vehicle.model_name, "days": days, "daily_rent": vehicle.daily_rent, "base_fee": base_fee, "discount_note": note, "final_fee": final_fee, "deposit": vehicle.deposit, "total_amount": round(final_fee + vehicle.deposit, 2) } return ordertotal_amount是顾客最终要支付的钱,包含押金和租金。押金虽然在结算时一起收,但退车时要退还,所以必须独立记录,不能和租金混在一起。这个细节在公司对账时尤其重要,我在实际做类似业务系统时被财务同事强调过很多次。
3.3 完整租车流程:选车、录入租期、结算出单
有了上面的基础模块,主流程就很简单了。我用一个打印菜单、接收用户输入、生成订单、展示结果的小程序把流程串起来:
def main(): print("欢迎来到模拟租车系统") print("当前可租车辆:") for idx, vehicle in enumerate(vehicle_pool, start=1): print(f"{idx}. {vehicle.display()}") choice = input("请输入车辆编号:") if not choice.isdigit(): print("输入无效,必须为数字") return idx = int(choice) - 1 if idx < 0 or idx >= len(vehicle_pool): print("车辆编号超出范围") return days_input = input("请输租车天数:") try: days = float(days_input) except ValueError: print("天数必须为数字") return selected_vehicle = vehicle_pool[idx] try: order = create_order(selected_vehicle, days) except ValueError as e: print(f"创建订单失败:{e}") return print("\n========== 租赁订单 ==========") print(f"车辆:{order['model_name']}({order['plate_no']})") print(f"日租金:{order['daily_rent']} 元") print(f"租期:{order['days']} 天") print(f"基础租金:{order['base_fee']} 元") print(f"优惠说明:{order['discount_note']}") print(f"租金合计:{order['final_fee']} 元") print(f"押金:{order['deposit']} 元") print(f"本次应收:{order['total_amount']} 元") print("==============================") if __name__ == "__main__": main()用户在控制台输入车辆编号和天数,程序就输出一张结构完整的订单。这部分代码里我重点处理了三个容易出错的地方:
第一,用户输入的可能不是数字,用isdigit()和try-except做双重防护。第二,用户输入的编号可能越界,所以要检查范围。第三,天数转换为浮点数后,如果用户输入0或负数,create_order里的校验会抛出ValueError,由主流程捕获。
这种防御式编程在真实项目里是标配,因为控制台程序面对的是不可预测的用户输入。别看这只是一道编程题,能把输入校验写好,代码的健壮性直接上一个档次。
4. 我的测试与调试经验:这些坑一定要避开
4.1 边界条件:零天租期、一天以内、跨月计费
我踩过的最深的坑,几乎都在边界条件上。举个例子,租满 30 天打 8 折的规则,如果用户输入恰好是 30 天,程序能正确打折;那如果输入 29.5 天呢?按照“不足一天按一天计”,29.5 天应该按 30 天算,程序却很容易把days=29.5直接拿去做判断,结果命中“7天9折”而不是“30天8折”。
这是我特别想强调的一个业务规则顺序问题:到底先向上取整再判断折扣,还是先判断折扣再取整?从用户角度,29.5 天和 30 天几乎没差别,但金额差异是实打实的。我在calc_rent_fee里先做了days = max(days, 1),但如果是 29.5,它没有向上取整到 30。后来的修改是把取值逻辑改成days = math.ceil(days),也就是说不足一天的部分直接往上进一位。
import math def normalize_days(days): if days <= 0: raise ValueError("租期必须大于0天") return math.ceil(days)这是“时间计费”里非常典型的取整问题。最小计费单位是天,那任何小数天数都要向上取整,不能让顾客占便宜,也不能让公司吃亏。你要把“取整规则”明确写在代码注释里,不然三个月后回头读代码,自己都会忘记当初为什么这么写。
跨月计费是另一个边界问题。如果按“天”计费,跨月并不会影响金额,因为费用只跟总天数相关。但如果题目改成“按自然日计算,不同月份价格不同”,那days一个数字就不够了,得用开始日期和结束日期,还要考虑月份的 28 天、30 天、31 天。这道题暂时不需要,但你要知道从“天数模式”切换到“日期模式”时,数据模型和计算逻辑都得跟着变,这是一次真正的架构升级。
4.2 输入校验和数据安全问题:流程入口最容易翻车
很多初学者把注意力都放在核心算法上,忽略了输入校验,这是大忌。我见过有人直接写days = int(input(...)),用户输了个“abc”,程序直接崩溃;也见过有人不检查车辆编号,用户输入“999”,代码直接索引越界,抛出一堆英文异常栈。这些在真实交付里都是低级事故。
我整理过一个简单的输入校验清单,照着做基本不会漏:
- 数值能不能解析成功,必须用
try-except捕获类型错误 - 解析成功后是否符合业务范围,比如天数大于 0、车辆编号在列表范围内
- 特殊业务数值是否要取整或做精确位数控制
- 安全方面,钱不可以用浮点数直接进行多次加减乘除而不做精度处理,避免累积误差
关于“钱用浮点数”这事,我还想多说一句。题目规模小,round(x, 2)够用;但如果以后接入真实支付系统,浮点误差会变成严重的财务问题。Python 里处理精确金额,标准做法是用Decimal:
from decimal import Decimal, ROUND_HALF_UP price = Decimal("0.1") total = price * 3 # 得到 Decimal("0.3"),而不是 0.30000000000000004这其实是一个加分项。如果你在代码里主动用Decimal管理金额,懂行的评审一眼就看得出你接触过真实项目,比写一堆炫技设计模式更有说服力。
4.3 折扣规则顺序和扩展性陷阱
折扣规则顺序的坑,我在前面提过一次,但值得单独拿出来说,因为它太隐蔽了。假设规则表这样写:
DISCOUNT_RULES = [ {"min_days": 1, "rate": 1.0, "note": "标准日租价"}, {"min_days": 7, "rate": 0.9, "note": "租满7天享9折"}, {"min_days": 30, "rate": 0.8, "note": "租满30天享8折"}, ]从业务角度看,这个表读起来更顺,因为从低到高排列,符合人的阅读习惯。但程序遍历的时候,输入days=30,它会先命中第一条规则min_days=1,直接返回原价!因为30 >= 1成立,后面的规则根本不会执行。
解决办法有两种:一种是像我前面那样,在遍历前对规则列表按min_days从大到小排序:
DISCOUNT_RULES = sorted(DISCOUNT_RULES, key=lambda x: x["min_days"], reverse=True)另一种是在遍历时记录“当前最优规则”,而不是一命中就返回:
def get_discount_rule(days): best_rule = None for rule in DISCOUNT_RULES: if days >= rule["min_days"]: if best_rule is None or rule["min_days"] > best_rule["min_days"]: best_rule = rule return best_rule or {"min_days": 1, "rate": 1.0, "note": "标准日租价"}第二种写法对规则的顺序不再敏感,更稳健,但代码稍长。我更喜欢这种“求最优”的思路,因为真实业务规则一旦多起来,“多条同时满足”是常态,你要选择最优的,而不是碰运气。
另一个扩展性陷阱是滥用继承。我前面给了FamilyCar和SUV两个子类,但如果你发现子类之间没有差异,那强行继承反而增加理解成本。更常见的合理做法是:把“车型差异”作为车辆的一个属性,而不是通过类的继承来表达,比如“座位数”“排量”就是属性,而“是否需要额外保险”才可能需要行为差异。这是一个需要在实际开发中不断体会的判断,没有绝对正确答案,但你要有意识去权衡。
5. 题目之外:让方案落地成更通用的框架
5.1 引入策略模式,把计费规则彻底解耦
前面用配置规则表的方式,已经让折扣档位不再写死在函数里,但严格来说,calc_rent_fee这个函数依然隐含了“租金只和天数相关”的业务逻辑。如果将来有一种计费模式是按“小时”的,另一种是按“公里数”的,最终费用函数就会变成一个不断膨胀的判断中枢。
这时候可以用“策略模式”进行重构。策略模式的核心思想,就是把一组可替换的算法封装成独立的对象,让调用方在选择策略时像插卡一样方便。
先定义一个计费接口:
class BillingStrategy: def calc(self, vehicle, days): raise NotImplementedError再实现按天计费策略:
class DailyBilling(BillingStrategy): def __init__(self, rules): self.rules = sorted(rules, key=lambda x: x["min_days"], reverse=True) def calc(self, vehicle, days): days = math.ceil(days) if days > 0 else 1 base_fee = vehicle.daily_rent * days for rule in self.rules: if days >= rule["min_days"]: return round(base_fee * rule["rate"], 2) return round(base_fee, 2)再实现一个简单的按小时计费策略作为演示:
class HourlyBilling(BillingStrategy): def __init__(self, hourly_rate): self.hourly_rate = hourly_rate def calc(self, vehicle, days): hours = max(days * 24, 1) return round(hours * self.hourly_rate, 2)最后,在车辆或订单层选择策略:
class RentCalculator: def __init__(self, strategy: BillingStrategy): self.strategy = strategy def calc(self, vehicle, days): return self.strategy.calc(vehicle, days)这样,主流程里只需要根据业务条件选择不同的策略对象,就能切换计费方式,而不用改动订单创建逻辑。很多设计模式教材喜欢用“鸭子”“披萨店”举例,但“租车计费”其实是非常贴合生活场景的例子,理解起来更直观。
5.2 存储层的设想:从内存列表到文件与数据库
当前的实现把车辆都放在内存列表里,程序一结束数据就没了,这也是题目场景的限制。如果想把方案扩展到实际可用的工具,至少要考虑持久化存储。
最简单的方案是存成 JSON 文件:
[ { "plate_no": "京B12345", "model_name": "经济型家轿", "daily_rent": 260, "deposit": 800, "type": "FamilyCar" } ]读取时用json.load()加载,再按type字段创建对应的类实例。这个方案对于中小规模的题目练习完全够用,也能很好地模拟“数据从配置来,而不是写死在代码里”的理念。
再往上走就是关系型数据库,设计一张vehicle表、一张rental_order表,车辆表和订单表之间通过plate_no关联。这也是真实租车系统的基本表结构。如果有人想把这个编程题扩展成课程设计,我会建议至少做一个vehicle表和order表,并通过一个简单的 SQLite 文件数据库实现,这样既免去安装数据库服务的麻烦,又能体验 SQL 操作。
5.3 关于测试:哪怕题目不要求,也要写一个测试用例
最后分享一个让我受益匪浅的习惯:无论题目有没有强制要求,都要为计算逻辑写几个自动化测试。代码很少,但作用很大:
import unittest class TestRentFee(unittest.TestCase): def test_standard_days(self): car = Vehicle("京B12345", "经济型家轿", 260) self.assertEqual(calc_rent_fee(car, 3)[0], 780.0) def test_discount_days(self): car = Vehicle("京B12345", "经济型家轿", 260) self.assertEqual(calc_rent_fee(car, 7)[0], 1638.0) def test_invalid_days(self): car = Vehicle("京B12345", "经济型家轿", 260) with self.assertRaises(ValueError): calc_rent_fee(car, -1) if __name__ == "__main__": unittest.main()为什么我特别强调测试?因为这类题目最大的学习价值,不是把功能跑通一遍,而是积累一套“以后可以复用的测试模板”。折扣计算、边界取整、非法参数校验,这几个测试用例是任何“按天计费”系统都必须覆盖的。你写一次,以后遇到类似的业务,直接套同样的测试思路,能省很多调试时间。
说实话,民间的编程题千千万万,但“汽车租赁公司汽车出租方案”能成为经典,就是因为它足够贴近真实业务。它不像写排序、写递归那样纯考算法,而是逼你考虑价格怎么算、规则怎么配、用户乱输入怎么办这些真问题。我自己的体会是,能把这种题目做干净,比刷一百道纯算法题更能体现工程能力。如果你正在准备这道题,别急着敲代码,先拿一张纸把车辆对象、计费规则、订单流程画清楚,动手之后你会发现思路顺得多。最后再啰嗦一句:规则表记得排序,输入记得校验,金额记得用精确计算。这三个小习惯,比任何炫技都能让你的方案真正“能交付”。