那天晚上快十一点,我在客厅沙发上瘫着刷手机,想订一张第二天早上第一场的电影票,结果某个App卡了半天一直转圈。人都困得不行了,订单还在“处理中”。我当时就想:要不自己用 Python 写个小电影订票小系统玩玩算了,起码逻辑和问题都在我掌控里,对吧。
就当是给自己练下基本功,你也可以跟着一起写一个,很简单的那种,命令行就能跑起来,不整花里胡哨的页面。
先想一想,电影订票到底要干嘛嘛,别一上来就敲代码。正常用户的操作流程,大概就是:
所以最小闭环其实就是:场次 + 座位 + 订单。你把这三块搞清楚,一个小系统就成型了。
我一般习惯用几个类把东西抽一下,不然全写在一个脚本里,变量到处飞,很快就乱了。
我们这次会有这么几个概念:
先从最核心的“场次 + 座位”开始写起。
那天在公司楼下抽烟的时候,我就在纸上随手画了个座位图,5 排 8 列那种迷你厅就够演示了。代码里就是一个二维数组就搞定了。
我们用 dataclasses 来省点事儿,定义好 Seat 和 Show 两个类:
from dataclasses import dataclass, field
from typing import List
@dataclass
classSeat:
"""一个座位"""
row: int # 第几排,从 0 开始存
col: int # 第几列,从 0 开始存
is_booked: bool = False# 是否已售
@dataclass
classShow:
"""一场电影场次"""
id: int
movie_title: str
time: str # 简单用字符串,不搞 datetime 了
price: float
rows: int = 5# 座位行数
cols: int = 8# 座位列数
seats: List[List[Seat]] = field(init=False)
def__post_init__(self):
# 初始化座位二维数组
self.seats = [
[Seat(r, c) for c in range(self.cols)]
for r in range(self.rows)
]
defavailable_seat_count(self) -> int:
"""计算还剩多少空座"""
return sum(
1
for row in self.seats
for seat in row
ifnot seat.is_booked
)
defshow_seat_map(self) -> None:
"""在命令行打印座位图,O=空位 X=已售"""
print(f"\n场次 {self.id} - 《{self.movie_title}》 {self.time}")
print("座位图:O=可选,X=已售")
# 打印列号(从 1 开始给用户看)
header = " " + " ".join(f"{c+1:2d}"for c in range(self.cols))
print(header)
for r, row in enumerate(self.seats):
line = f"第{r+1:2d}排 "
for seat in row:
line += " X"if seat.is_booked else" O"
print(line)
print()
defbook_seat(self, user_row: int, user_col: int) -> Seat:
"""
订座位,用户输入是从 1 开始的行列,
内部存储是从 0 开始,所以要减 1
"""
r = user_row - 1
c = user_col - 1
ifnot (0 <= r < self.rows and0 <= c < self.cols):
raise ValueError("座位不存在,检查下排号和列号是不是超范围了")
seat = self.seats[r][c]
if seat.is_booked:
raise ValueError("这个座位刚刚被别人抢先买了,换一个吧")
seat.is_booked = True
return seat
这里几个点说一下,别光看过去就完了:
__post_init__ 是 dataclass 在初始化后自动调用的,我们在这里把 seats 二维数组建出来show_seat_map 这个打印函数,很粗糙,但够用,看着也清楚有了场次,还差一个东西:订单。
你总得知道“谁买了哪个场次的哪几个座位”,对不对。为了简单,我们不搞用户登录,就用一个随便输入的“昵称”当作用户标识。
订单结构大概就:订单号 + 用户名 + 场次 + 座位列表 + 总价。
from dataclasses import dataclass
from typing import List, Tuple
@dataclass
classOrder:
"""简单的订单结构"""
order_id: int
user_name: str
show: Show
seats: List[Seat]
@property
deftotal_price(self) -> float:
return round(len(self.seats) * self.show.price, 2)
defsummary(self) -> str:
seat_str = ", ".join(
f"{s.row + 1}排{s.col + 1}座"for s in self.seats
)
return (
f"订单号:{self.order_id}\n"
f"用户:{self.user_name}\n"
f"电影:{self.show.movie_title}\n"
f"场次时间:{self.show.time}\n"
f"座位:{seat_str}\n"
f"总价:{self.total_price} 元"
)
这里我懒得搞单独的价格字段了,反正总价就是:数量 × 单价。
上面这些都只是“模型”,真正跟用户打交道的是那个“主程序循环”。就是那种:
我当时写的时候心里就一个念头:千万别把逻辑写得太复杂,不然你半夜调 bug 真的会骂自己。
来,我们写一个 TicketSystem 把东西包起来:
classTicketSystem:
def__init__(self):
# 模拟几场电影
self.shows = {
1: Show(1, "星际穿越", "2024-01-07 10:00", 39.9),
2: Show(2, "盗梦空间", "2024-01-07 14:30", 35.0),
3: Show(3, "三体:黑暗森林", "2024-01-07 20:00", 49.9),
}
self.orders: List[Order] = []
self._order_seq = 1# 自增订单号
defrun(self):
print("欢迎来到命令行电影订票系统 🎬")
whileTrue:
print("\n========== 主菜单 ==========")
print("1. 查看所有场次")
print("2. 订票(选场次 + 选座)")
print("3. 查看我的订单")
print("4. 退出系统")
choice = input("请输入操作序号:").strip()
if choice == "1":
self.list_shows()
elif choice == "2":
self.handle_booking()
elif choice == "3":
self.list_orders()
elif choice == "4":
print("好的,祝你观影愉快~")
break
else:
print("输入不太对,再来一次吧")
deflist_shows(self):
print("\n当前可选场次:")
for show in self.shows.values():
print(
f"场次ID: {show.id} | 《{show.movie_title}》 "
f"| 时间: {show.time} | 单价: {show.price} 元 "
f"| 剩余座位: {show.available_seat_count()}"
)
defhandle_booking(self):
ifnot self.shows:
print("现在还没有上架的电影场次。")
return
self.list_shows()
try:
show_id = int(input("\n请输入想要订票的场次ID:").strip())
except ValueError:
print("场次ID必须是数字。")
return
if show_id notin self.shows:
print("没有这个场次ID,检查一下是不是输错了。")
return
show = self.shows[show_id]
show.show_seat_map()
try:
num = int(input("你想一次买几个座位?:").strip())
if num <= 0:
print("买的数量得大于 0 吧。")
return
except ValueError:
print("数量必须是数字。")
return
seats: List[Seat] = []
for i in range(num):
print(f"\n正在选择第 {i+1} 个座位:")
try:
row = int(input("请输入排号(从1开始):").strip())
col = int(input("请输入列号(从1开始):").strip())
seat = show.book_seat(row, col)
seats.append(seat)
print(f"已选:第{row}排{col}座")
except ValueError as e:
print(f"选座失败:{e}")
# 简单处理:如果失败,这个座位就不算
# 也可以选择让用户重选,这里先不复杂化
continue
ifnot seats:
print("一个座位都没选上,这次订票就算了。")
return
user_name = input("\n请输入你的昵称(用于记录订单):").strip() or"匿名用户"
order = Order(
order_id=self._order_seq,
user_name=user_name,
show=show,
seats=seats
)
self.orders.append(order)
self._order_seq += 1
print("\n订票成功!订单信息如下:")
print("--------------------------------")
print(order.summary())
print("--------------------------------")
deflist_orders(self):
ifnot self.orders:
print("\n你还没有任何订单。")
return
print("\n你的全部订单:\n")
for order in self.orders:
print(order.summary())
print("--------------------------------")
最后在文件末尾加上一个入口:
if __name__ == "__main__":
system = TicketSystem()
system.run()
这么一整套下来,一个可以跑的“命令行电影订票系统”就算完工了。你在终端里 python movie_ticket.py 跑起来,实际体验一下流程,会比只看代码印象深很多。
这里顺手说几个这种小玩具项目里,比较容易踩的点,免得你自己写的时候又重新掉坑里:
一个是输入校验。比如行列号越界、场次 ID 不存在、数量不是数字,这些东西在自己测试的时候不一定会去故意输错,但一旦别人来用,就一定有人各种乱输。上面我都是用 try/except ValueError 做了一层兜底,虽然简陋,但基本不会直接把程序整崩了。
还有一个是状态更新要统一地方维护。我们现在是:
Show 负责改 Seat.is_bookedTicketSystem 负责把 Order 存起来如果你哪天想加“退票”功能,只要记住两件事: 把订单里的座位 is_booked 改回 False,然后把订单从 self.orders 里删掉就完事。逻辑都在这两块,不会散得到处都是。
再往后要扩展,其实有很多方向: 比如你想加:
Show 里多加一个 hall_name)FastAPI 或 Flask 把这套逻辑包成 HTTP 接口,再随便搞个前端页面这些都可以在现在这份代码的基础上慢慢加,你会发现:当最早的数据模型设计得还算清晰的时候,后面要长肉就舒服多了。
我写这种小系统,主要是有两个用处:
一个是练手,刷语法、刷类的设计、刷异常处理这些东西; 另一个就是,你之后面试的时候,如果有人问你“用面向对象设计一个订票系统”这种题,你脑子里是有画面的,不会愣在那里现编。
你可以先把上面的代码直接搞成一个 movie_ticket.py 跑一遍,能跑通之后,再根据自己想法魔改一版: 比如加个“同一行必须连坐”的限制、加个“学生票半价”的逻辑之类的。改着改着,这个就变成你自己的电影订票系统了。
行,我这边就先说到这,我去给自己真买张电影票了,不然写了半天系统结果一个电影都没看,太亏了。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html