☰
用Python字典搭建智能零食货架:从嵌套结构到库存实战
2026/10/15 6:06:35 网站建设 项目流程

学完Python基础语法之后,很多人都会遇到同一个问题:字典这个数据结构,教程里翻来覆去讲增删改查,但真到自己写东西的时候,总觉得不知道拿它干什么。列表好歹能存一堆名字,字典好像除了当通讯录就没别的用场了。我当初也有这种感觉,直到某天盯着办公室的零食货架发呆,突然想明白了一件事——货架不就是一本活字典吗?货架编号是键,商品是值,每个商品又是一组属性的集合。嵌套字典刚好对应现实中的“货架——商品——属性”三层结构。于是就有了这个用纯Python字典搭建的智能零食货架系统,也叫“代码便利店”。它可以做商品上下架、库存扣减、补货预警、临期检查、优惠结算,还能把数据落盘保存,下次启动接着用。

这个项目特别适合刚学完字典、函数、文件读写,想找一个“练手不练废”的综合性案例的人。它不需要装第三方库,纯标准库就能跑通,但覆盖的知识点很密:嵌套字典设计、深浅拷贝的坑、日期时间比较、JSON序列化、异常处理,外加一点“用代码模拟真实业务流程”的系统思维。这篇文章我会从设计思路讲到完整功能实现,再到我调试过程里踩过的坑,最后附上排查速查表。代码不长,但每一步都值得琢磨。

1. 项目全貌与核心设计思路

1.1 需求拆解:零食货架到底要管什么

先别急着写代码,想一想现实中的零食货架需要处理哪些事。我当时的清单大概是这样的:

  • 货架上有多个商品,每个商品有名称、价格、库存、保质期。
  • 顾客“拿”走一个商品,库存要减一。
  • 库存低于某个阈值,要提醒补货。
  • 快到保质期的商品,要提前预警,方便打折处理。
  • 结算时可能有多件商品,要算总价,也可能有优惠。
  • 关店之后,货架数据要能保存,第二天开店还能读回来。

拆成技术语言就是:需要一种数据结构保存“SKU编码到商品详情”的映射,需要一批函数操作这些数据,需要文件来持久化。而“SKU编码到商品详情的映射”用字典来做,简直是在舒适区里游泳。

我给每个货位编了三到四位编码,比如A01、B02。为什么不用商品名直接当键?因为货架物理位置是固定的,补货时照着编码找位置才是真实流程;另外商品名容易重复,一包薯片有原味、番茄味、烧烤味,名字又长又容易打错。编码短、稳定、唯一,作为键再合适不过。

1.2 数据模型选型:为什么不用列表和类

这是初学者最爱问的问题。

用列表行不行?能存,但很勉强。列表里放商品,每个商品用字典表示,找某个商品得遍历一遍,时间复杂度O(n)。如果货架只有二十个位置还好,一旦要扩展到几百个SKU,每次查询都全表扫描,代码又慢又丑。

用类行不行?行,而且不少正规项目就是用类来建模商品。但对一个练手项目来说,类的scheme有时候反而是负担——你定义了一个Snack类,添加属性要改类定义,序列化要额外处理。而字典天生就是“属性包”,加一个字段,直接往字典里塞键值对就行,写起来最顺手。我经常跟人说:起步阶段,先学会用基本数据结构搭一个能跑的系统,你才会真正理解为什么后来需要类、需要数据库。直接上重型方案,很容易迷失在语法细节里。

我的选型结论很明确:外层字典存货架,每个键是货位编码,每个值又是一个嵌套字典,存放name、price、stock、expiry这些字段。这就是三层嵌套,也是整个系统的数据地基。

1.3 扩展性预留:从一开始就想到下一步

虽然是练手项目,但代码结构我还是按半正式工程来组织的。为的是之后想加功能时不用推倒重来。

我预留了几个扩展口:

  • 每个商品除了基本信息,可以随时加discount字段,不影响现有代码。
  • 结算时支持“会员积分”字段,挂在顾客侧而不是商品侧。
  • 数据保存用JSON,天然兼容未来迁移到数据库或者写成Web API返回格式。

这等于把“今天能跑”和“明天能改”同时考虑进来。当然,我不是说一开始就要搞得很复杂,而是说别把代码全塞在一个脚本里,哪怕只是粗略分个main.py和store.py,后面维护都舒服很多。

2. 核心代码拆解:从零搭建智能货架

2.1 货架数据结构的定义与初始化

我直接上代码,这是整个系统最核心的部分:

import json import datetime # 初始货架数据 shelf = { "A01": { "name": "苏打饼干", "price": 6.5, "stock": 20, "expiry": "2025-12-31" }, "A02": { "name": "海盐薯片", "price": 8.9, "stock": 5, "expiry": "2025-10-15" }, "B01": { "name": "黑巧克力", "price": 12.0, "stock": 0, "expiry": "2026-03-01" } }

这里每个值都是一个字典,字段含义一眼能看懂。expiry为什么用字符串而不是日期对象?因为JSON序列化时日期对象不能直接保存,字符串才是跨场景最稳的形式。等到需要比较日期时,再用datetime.strptime解析成时间对象,这是我在多次踩坑后确定的方案,后面会专门讲。

初始化的时候,我故意把一个商品的库存设为0、一个设为5,是为了后续测试补货和预警功能时有现成数据。这种刻意制造的边界数据,调试时非常有用。

2.2 存数据与加载数据:让字典“活”到硬盘上

纯内存的字典,程序一关就没了,所以必须有持久化。JSON是首选格式,原因很简单:Python字典跟JSON在结构上是天然对应的,嵌套字典存成JSON后人类也能直接阅读修改。

def save_data(filename="store_data.json"): with open(filename, "w", encoding="utf-8") as f: json.dump(shelf, f, ensure_ascii=False, indent=2)
def load_data(filename="store_data.json"): global shelf try: with open(filename, "r", encoding="utf-8") as f: shelf = json.load(f) except FileNotFoundError: print("没找到存档文件,用初始货架启动。")

两个函数加起来不到十行。ensure_ascii=False是为了让导出的文件里能直接看到中文商品名,indent=2是让JSON有缩进,方便调试时肉眼检查。

这里有一个非常典型的场景:程序每次启动时先load_data(),从磁盘恢复货架状态;每次操作结束后save_data(),把最新状态写回文件。有人会问为什么不每次都自动保存?实际操作中,过于频繁的写盘在频繁修改时会拖慢速度,所以我选择“手动在关键操作后调用”。这也是真实项目里常见的取舍。

2.3 售出与补货:字典的经典操作现场

售出逻辑是整个系统中操作最频繁的部分,也是磨炼基本功的地方:

def sell_item(sku, quantity=1): if sku not in shelf: print(f"货架没有 {sku} 这个位置") return False item = shelf[sku] if item["stock"] < quantity: print(f"{item['name']} 库存不足,当前只剩 {item['stock']} 件") return False item["stock"] -= quantity print(f"售出 {item['name']} x {quantity},剩余 {item['stock']} 件") return True

这短短一段,覆盖了三个重要考点:成员判断sku not in shelf、嵌套字典的链式访问item["stock"] -= quantity、返回值设计。关于最后一点,很多人写函数只靠打印输出,不看返回值。但一个真正的系统,调用方往往需要根据成功与否做后续动作(比如更新账单、记录日志),所以sell_item必须返回布尔值。这是教科书里不细讲、但实际项目一定会用到的习惯。

补货逻辑是售出的镜像操作:

def restock_item(sku, quantity): if sku not in shelf: print(f"货架没有 {sku} 这个位置") return False shelf[sku]["stock"] += quantity print(f"{shelf[sku]['name']} 补货成功,当前库存 {shelf[sku]['stock']} 件") return True

注意这里我直接写shelf[sku]["stock"] += quantity,省掉了先取中间变量。在Python里,链式键访问是允许直接赋值的,这也是嵌套字典方便之处。但写的时候要保证sku一定存在于shelf中,否则会抛KeyError。所以我每次都先做成员检查,宁可多两行,也不赌用户输入合法。

2.4 定时巡检:临期预警和补货提醒

智能货架的“智能”体现在它能主动告诉你“该补货了”“有东西快过期了”,而不是只会被动等你查。

def check_shelf(): print("=== 货架巡检 ===") today = datetime.date.today() for sku, item in shelf.items(): name = item["name"] stock = item["stock"] # 补货提醒 if stock == 0: print(f"[缺货] {sku} {name} 卖完了,尽快补货") elif stock < 5: print(f"[低库存] {sku} {name} 只剩 {stock} 件,建议补货") # 临期提醒 expiry_date = datetime.datetime.strptime(item["expiry"], "%Y-%m-%d").date() days_left = (expiry_date - today).days if days_left < 0: print(f"[过期] {sku} {name} 已过期 {abs(days_left)} 天,必须下架") elif days_left <= 7: print(f"[临期] {sku} {name} 还有 {days_left} 天到期,建议打折处理")

这段代码把字典遍历、时间计算、条件分支全串起来了。

我来说说时间处理的思路。item["expiry"]是字符串,比如"2025-12-31"。datetime.strptime把字符串解析成日期对象,datetime.date.today()获取今天日期,两者相减得到一个timedelta对象,取days就是相隔天数。负数说明已过期,0说明今天到期,7以内的进入临期预警。这套逻辑是业务系统里非常经典的“剩余天数”计算模式。

补货阈值这个5,我是直接写死在函数里的。严谨一点应该做成参数,比如def check_shelf(low_stock_threshold=5)。但练手项目里先写死,让逻辑跑通,再考虑参数化,这符合“先能跑、再优化”的原则。在文章末尾的扩展部分,我会再说怎么改。

3. 场景化功能实现:让系统真正可交互

3.1 购物结算与优惠计算逻辑

有了商品,有了库存,接下来就是结算功能。这里的难点在于:购物车可能是多个不同的SKU,每种可能不止一件,计算总价时要遍历购物车,去货架里取价格,同时考虑有没有折扣。

我的购物车也用字典保存:键是SKU,值是数量。这和货架的结构形成了天然的“嵌套字典与传统字典”的对照。

cart = {} def add_to_cart(sku, quantity=1): if sku not in shelf: print(f"货架没有 {sku} 这个位置") return if shelf[sku]["stock"] < quantity: print(f"{shelf[sku]['name']} 库存不足") return if sku in cart: cart[sku] += quantity else: cart[sku] = quantity print(f"已加入购物车:{shelf[sku]['name']} x {quantity}")

这里有个很大的坑:当用户把商品加入购物车时,我并没有直接扣减库存。库存应该等到真正结算时才扣,这是模拟真实超市流程——逛的时候东西还在货架上,结账时才从货架上拿下来。如果你在加入购物车时就扣库存,用户中途放弃购买,库存就“留在购物车里”了,还得再写一个归还逻辑。这是初学者很容易设计反的地方。

结算函数:

def checkout(member_id=None): if not cart: print("购物车是空的") return total = 0.0 print("=== 结算清单 ===") for sku, qty in cart.items(): item = shelf[sku] price = item["price"] subtotal = price * qty total += subtotal print(f"{item['name']} x{qty} = {subtotal:.2f} 元") # 满减优惠 if total >= 50: discount = total * 0.1 total -= discount print(f"满50减10%优惠:-{discount:.2f} 元") # 会员额外折扣 if member_id: total *= 0.95 print("会员额外95折") print(f"应付总额:{total:.2f} 元") # 确认后扣库存 confirm = input("确认结算?(y/n): ") if confirm.lower() == "y": for sku, qty in cart.items(): shelf[sku]["stock"] -= qty cart.clear() print("结算完成,库存已更新。") save_data() else: print("已取消结算,购物车保留。")

这个函数很长,但逻辑非常清晰:先算原价总和,再应用满减优惠,再判断会员折扣,最后确认结算、扣库存、保存。注意f"{subtotal:.2f}"这种格式化写法——显示金额用两位小数是基本常识,不加就是99.99999这种尴尬数字。这些都是细节,但细节决定这个系统靠不靠谱。

3.2 终端交互菜单:把功能串起来

有了底层函数,还需要一个让用户愿意用的交互入口。我写了一个简单的手写菜单循环,用while True加用户输入选择。

def main(): load_data() while True: print(""" 1. 查看货架 2. 上架新商品 3. 售出商品 4. 补货 5. 加入购物车 6. 结算 7. 货架巡检 8. 保存并退出 """) choice = input("请选择操作:") if choice == "1": for sku, item in shelf.items(): print(f"{sku} | {item['name']} | ¥{item['price']} | 库存{item['stock']} | 到期{item['expiry']}") elif choice == "2": sku = input("新货位编码:").upper() if sku in shelf: print(f"{sku} 已存在,不能重复上架") continue name = input("商品名称:") price = float(input("单价:")) stock = int(input("初始库存:")) expiry = input("保质期(YYYY-MM-DD):") shelf[sku] = { "name": name, "price": price, "stock": stock, "expiry": expiry } print(f"{sku} 上架成功") elif choice == "8": save_data() print("数据已保存,再见!") break # 其余分支类似,不逐一展开

这里我故意用.upper()把输入的SKU都转成大写。因为SKU编码设计成大写字母加数字,如果用户输入小写a01,直接去字典里找会找不到。这个不起眼的细节,在真实使用中会频繁出现——用户输入不规范是常态,你要在入口处做防御式处理,而不是指望用户乖乖听话。

3.3 数据完整性与备份策略

程序跑起来之后,最怕的是数据丢失。字典在内存里改得再漂亮,断电就什么都没了。我的做法是双保险:每完成一次关键操作(售出、补货、结算)就调用一次save_data();退出时再存一次。这就相当于“事务提交”,虽然我们没用真正的数据库,但思路是一样的。

另外我额外写了一个备份逻辑:每次保存前,把已有文件改名为store_data_backup.json,再写新文件。这样万一新数据写入失败,还能用备份恢复到上一个版本。代码很简单:

import os if os.path.exists("store_data.json"): os.rename("store_data.json", "store_data_backup.json")

这招在真实业务里非常常用,叫“滚动备份”。对于一个练手项目,这点设计能让它带上一丝工程化味道,也让你提前理解数据安全不是一句空话。

4. 常见问题与排查技巧实录

4.1 嵌套字典的引用陷阱:改一个全变了

这是我在调试过程中遇到最隐蔽的问题,也非常有典型性。

Python里,字典是可变对象。当你把一个字典赋值给另一个变量、或者把它作为参数传给函数时,你并没有复制它——你只是让它多了一个名字,底层还是同一个对象。也就是说:

cart_item = shelf["A01"] cart_item["stock"] = 99

执行完这段代码,你会发现shell["A01"]["stock"]也变成了99。因为cart_item和shelf["A01"]指向同一个字典。这不是bug,这是Python对象模型的工作原理,但初学者不知道这件事,就会写出“我只改了一个变量,为什么原数据也变了”的情况。

解决方法是:如果你需要临时复制一份数据再修改,用copy.deepcopy():

import copy temp_shelf = copy.deepcopy(shelf)

deepcopy会递归复制,嵌套字典也能得到完整副本。修改temp_shelf不会影响shelf。

这个问题的排查经验就是:遇到“莫名其妙牵连修改”的情况,第一反应检查是不是有人把可变对象赋值来赋值去。我建议用打印内存地址的方式来辅助排查,id(shelf["A01"])和id(cart_item)一致,就说明是同一个对象。

4.2 日期比较失败:字符串和日期对象混在一起

我在最初写巡检功能时,犯过一个低级错误:直接把item["expiry"]和datetime.date.today()比较,结果报出TypeError: can't compare datetime.datetime to str。原因很简单,一个是字符串,一个是日期对象,两者类型不同不能比较。

正确流程只能是这样:

expiry_date = datetime.datetime.strptime(item["expiry"], "%Y-%m-%d").date() days_left = (expiry_date - today).days

如果是“YYYY-MM-DD”这种标准格式,strptime的格式化串写"%Y-%m-%d"即可。注意%m表示月,%M表示分钟,不能混。

后来我还专门写了个工具函数来封装日期解析,这样就不用每次到处重复strptime:

def parse_date(date_str): return datetime.datetime.strptime(date_str, "%Y-%m-%d").date()

4.3 用户输入的脏数据:float解析和空输入

菜单程序里最让人头疼的是用户乱输入。我总结出这几类高频问题:

第一个是空输入。用户直接回车,sku = "",然后去字典里查,找不到,程序可能抛异常。我的做法是在解析前先去掉空白,并检查是否为空:

sku = input("货位编码:").strip().upper() if not sku: print("编码不能为空") continue

第二个是数字输入错误。float(input("单价:"))这行,如果用户输入abc,程序会抛ValueError直接崩溃。正式一点的做法是用try-except包裹,提示用户重新输入:

try: price = float(input("单价:")) except ValueError: print("价格必须是数字") continue

有人觉得练手项目不需要这么较真。但恰恰是这种小地方,区分了“能自己玩”的脚本和“能给别人用”的小工具。我在调试这个项目时,至少有三分之一的时间花在处理各种非法输入上。

第三个是SKU大小写问题。前面提到了,统一用.upper()转换即可,这里不再重复。

4.4 排查速查表

为了方便复盘,我把编辑过程中遇到的所有问题整理成了一张表,如果读者跟着写代码,这张表可以直接当参考。

现象可能原因排查与解决
提示KeyError: 'xx'SKU不存在,或者键名拼写错误打印shelf.keys()确认;输入统一.upper()
一个字典被多处修改可变对象直接赋值,未用deepcopy用copy.deepcopy()复制后再修改
日期比较报TypeError字符串和日期对象混用用strptime解析后再比较
输错数字程序崩溃没有捕获ValueErrortry-except包裹转换逻辑
中文商品名在JSON里显示乱码写入时没加ensure_ascii=Falsejson.dump(..., ensure_ascii=False)
重启后数据没恢复没有调用load_data()主循环启动前先加载
结算完库存没变扣减逻辑写在结算之外没执行检查结算函数里是否执行了stock -= qty
文件写入后数据丢了旧版直接覆盖写先改名为备份文件再写新文件

4.5 进阶扩展:从字典玩具到半真实系统

如果你已经把这个基础版跑通了,我强烈建议你沿着下面这几个方向自己改一改,每一个方向都能学到新东西。

第一是购物车结算时加入按品类折扣。比如所有饮料9折。实现方法是在商品字典里加一个category字段,结算时判断品类。这只需要在结算循环里加一个if分支,难度不大。

第二是把库存流水记录下来。每次售出或补货,往一个logs列表里追加一条记录,比如{"time": "2025-01-01 10:00", "sku": "A01", "change": -1}。然后把日志和货架数据一起存进JSON。这样你做库存复盘时能看到完整轨迹。这一招放在真实项目里就叫“审计日志”。

第三是把低库存阈值从写死改成可配置项。在shelf中加一个特殊的配置键,比如shelf["_config"] = {"low_stock_threshold": 5}。巡检时从这里读取。这样不同商品可以有不同的补货阈值。唯一要注意的是遍历时跳过_config,避免被当成普通商品处理。

第四是商品字典加barcode字段,用来模拟扫码枪录入。输入条形码,快速找到对应商品。实际拆解下来就是bar_to_sku = {"6901234567890": "A01"}这样一个映射字典,结合货架字典一起用。

我经常和一起学习的人说,千万别只满足于“跑通了”。一个项目真正的价值,是你往旁边再走几步,碰到新问题,解决掉,然后觉得自己比昨天强了一点。这个零食货架系统的扩展方向非常多,每走一步,你对Python的理解就更深一层。

5. 写在最后的一点经验

整个项目写下来,我最大的感悟其实不是“字典真有用”,而是“数据结构选型直接影响代码的舒适度”。同样的功能,用列表套字典也能实现,但你的查询逻辑会很别扭;用类定义也能实现,但你的序列化和初始化会多出不少模板代码。字典之所以适合这个场景,是因为货架的天然属性就是键值映射,你的数据形态和业务逻辑长得越像,你的代码就越容易写,bug也越容易找。

另外一点实在体会是,调试嵌套字典的时候,不要凭脑子猜数据长什么样,多打印print(json.dumps(shelf, ensure_ascii=False, indent=2)),一眼就能看清当前所有字段。太多次,我以为某个字段被改了,其实没有;我以为某个库存还够,其实早就变成负了。可视化地看数据,比任何推理都高效。

如果你照着这个项目写了一遍,我建议你试着把菜单里的“上架新商品”“修改价格”等功能补全,然后改一版自己设计的货架布局。代码是死的,思路是活的。用同一个架子去装糖水铺、装书桌、装工具间,逻辑完全一样,但你会越来越理解“数据建模”这件事的乐趣。

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

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

立即咨询