☰
Python购物小票打印案例:从基础语法到真实打印
2026/9/27 1:32:18 网站建设 项目流程

写简单案例其实比写复杂项目更难,因为一个“小”案例要把核心知识点串起来,还不能显得枯燥。打印购物小票这个案例我用了很久,教过不少朋友,可以说它麻雀虽小五脏俱全:变量、数据类型、循环、格式化输出、函数封装全都能带出来,甚至还能延伸到文件操作和真实打印机对接。这个案例适合刚学完Python基础语法的人,也适合想找一个完整小项目练手的学生,以及对“写出来的程序到底能干啥”有好奇心的初学者。

我之前在几个技术社区分享过这个案例的代码,收到最多的反馈是:原来Python可以做这么贴近生活的东西,而且代码量真的不大,理解了之后还能自己改着玩。这篇文章就把完整的拆解思路、核心代码、实操过程、问题排查全都写出来,你照着敲一遍,再自己改几个功能,基本上就把Python的基础用法吃透了一大半。

1. 案例拆解与整体设计思路

1.1 购物小票打印的“需求本质”是什么

购物小票表面上就是几行商品名称、单价、数量、金额,再加一个合计,但实际上它包含了一个完整程序必须具备的三个环节:数据的组织、业务的计算、结果的呈现。

数据组织是指怎么把商品信息存下来。最简单的方式是列表里装元组,每个元组表示一条商品记录,比如(“苹果”, 8.5, 2),分别是名称、单价、数量。稍微复杂一点可以用字典,把名称、单价、数量、折扣都放进去。这一步对应真实项目里的“数据结构设计”,决定后续代码好不好写。

业务计算是指金额怎么算。单件商品的小计是单价乘数量,多个商品要累加总价,如果设置了折扣或满减,还要在适当位置扣减。真实收银系统还会涉及抹零、四舍五入到分、找零计算,这一步对应的是“业务逻辑”,也是面试和考试爱考的部分。

结果呈现是指怎么把数据变成人能看懂的内容。对这个小案例来说,就是控制台里输出一张对齐的小票。更进阶一点是写入文本文件、生成PDF,甚至直接发送给热敏打印机。在真实项目中,这一步关系到用户最终看到的东西是什么样,是体验的最后一公里,也最容易出细节问题。

1.2 为什么选择“购物小票”作为练手项目

选这个题目有几个很现实的原因。首先它足够小,核心代码去掉注释和空行,不到一百行就能写出干净整洁的版本,不会让人觉得“学了一堆语法却无处施展”。其次它足够完整,从输入数据到计算再到输出,是一条完整的链路,学完以后能清楚知道一个程序是怎么从零到一跑起来的。

还有一个重要原因是这个场景大家都见过。超市小票、奶茶店小票、外卖单,几乎每天都会接触,所以程序输出的内容跟生活中真实的东西对上号时,理解成本直接降为零。学编程最怕的就是“代码和现实世界没有联系”,这个案例恰好把联系拉得很近。

1.3 方案选型:基础版到进阶版的阶梯式写法

这个案例我建议分三个层次来做,不同基础的人可以停在适合自己的位置。

第一层:控制台纯文本输出。只用Python内置的print和字符串格式化,做一张包含店名、商品明细、合计金额的小票。这一层覆盖的知识点包括变量、for循环、f-string、数字格式化,没有任何第三方库,是标准的基础练手方式。

第二层:函数封装 + 文件保存。把打印小票的逻辑封装成函数,传入商品列表就能输出。同时在控制台输出的基础上,把文本内容写入一个txt文件,这样程序运行的“结果”就留下来了。这一层开始有一点真实项目的味道,因为真实系统不太可能只用控制台展示,一定有持久化。一般写到这一层,基础语法已经算掌握得比较扎实了。

第三层:对齐真实打印场景。用文本文件配合操作系统的打印命令,或者用第三方库生成PDF,再交给打印机。这一层涉及编码、宽度对齐、打印驱动这些真实世界里才有的问题,属于把“玩具案例”变成“工具”的关键一步。

在后面的实操章节里,我会把第一层和第三层的关键代码都写出来,第二层的函数封装就穿插在其中一起说。

2. 核心细节解析与关键代码实现

2.1 数据结构设计:列表、元组还是字典

购物小票的核心数据是商品列表,这个列表怎么设计,直接影响后续代码好不好写。

最基础的方式:列表套元组。

goods = [ ("苹果", 8.5, 2), ("牛奶", 5.0, 1), ("面包", 12.0, 3), ]

元组里的三个元素分别是商品名称、单价、数量。这种方式写起来最短,位置固定,不会搞混,适合新手。缺点是如果以后要加“折扣”“会员价”“商品编码”这些字段,元组就得跟着改长度,代码里所有取数据的地方也得一起改,扩展性差。

推荐的进阶方式:列表套字典。

goods = [ {"name": "苹果", "price": 8.5, "count": 2}, {"name": "牛奶", "price": 5.0, "count": 1}, {"name": "面包", "price": 12.0, "count": 3}, ]

字典的好处是每个字段有名字,读代码的人一眼就明白price是单价、count是数量,不需要靠记忆。以后加字段也不需要动已有代码的结构,只在字典里多写一个键就行。真实项目里,数据大概率会以类似JSON的格式存在,字典的思维方式也更贴近实际。

我个人建议你在自己练习时,两种方式都写一遍。先写元组版,体会“简洁”;再改成字典版,体会“可扩展”。这两个版本写完之后,你对数据结构设计的理解会比只看别人的代码深刻很多。

2.2 金额计算的边界处理

金额计算是整个案例里最容易“差不多就行”的地方,但恰恰是真实收银系统里最重要的环节。Python的浮点数有一个经典问题——0.1 + 0.2并不精确等于0.3,在控制台里试一下就知道了。直接拿浮点数计算金额,攒多了就会差几分钱。

处理这个问题有三种常见做法:

方案一:用Decimal定点数。Python自带的decimal模块可以精确表示小数,适合金额计算。

from decimal import Decimal price = Decimal("8.5") count = Decimal("2") subtotal = price * count # Decimal精确结果是17.0

注意这里传的是字符串,不是浮点数。如果传Decimal(8.5),还是会有浮点数的精度问题残留。

方案二:统一以“分”为单位,用整数计算。这就是真实收银系统的常用做法。把钱都换算成分,8.5元就存850,计算全部用整数,最后展示时再除以100。整数计算不会有任何精度问题,性能还好。

方案三:最后用round统一舍入。这种方法适合练习,不适合正式场景。round带有银行家舍入的规则,而且舍入时机不同结果可能不同,非常容易踩坑。

对于这个案例,我建议你用整数分这个方案,体会一下真实系统的做法。它会让你的代码在计算这一环就比大多数人写的严谨。

2.3 对齐输出的核心算法:中文宽度处理

购物小票输出的最大难点不是计算,而是对齐。英文和数字在等宽字体下占1个字符宽度,中文占2个,Python的字符串长度函数len只会按字符数统计,不会区分中英文,所以直接用format冒号宽度对齐,中文一多就歪了。

举个例子,你写f"{name:<10}"想把商品名放到宽度为10的区域里,如果name是“苹果”,len("苹果")等于2,但屏幕上实际占了4个格子,后面内容就会错位。

解决方式是写一个能计算“显示宽度”的工具函数:

def display_width(text): """计算字符串在终端中的显示宽度,中文按2个宽度算""" width = 0 for char in text: if '\u4e00' <= char <= '\u9fff': # 常用汉字范围 width += 2 else: width += 1 return width

然后做填充的时候,不能直接用ljust,因为ljust也是按字符数量计算,必须自己写一个按显示宽度左对齐的函数。

这个函数是整个案例最值钱的代码,因为它是“中文场景独有的问题”。你在网上看到很多Python格式化教程,几乎都不会讲这一层,但我可以说,几乎所有做中文小票输出的人都被这个问题折腾过。做进销存系统的朋友应该会深有体会:商品名称有中文有英文有数字,长度参差不齐,不做宽度处理,打印出来就是一团糟。

2.4 小票模板设计:分块区域规划

一张购物小票从视觉上可以分成四块区域,我们写代码时也按这四块来组织逻辑。

区块一:店名与抬头。一般居中显示,上面还可以加一行日期时间。真实的超市小票会在这部分打出“欢迎光临”之类的文案,门店编号、收银员编号也在这里。我们案例里放店名、日期、单号就够了。

区块二:表头与商品明细。表头是“商品 单价 数量 小计”这样的列标题,下面是逐行商品数据。这一块是重点,每一列的对齐都要精确控制。实际超市小票的商品列表还会在单价后面标注是否参与促销,我们基础版先不做。

区块三:金额信息。包括商品总数、合计、实收、找零。真实场景还会打印“含税金额”“优惠金额”等字段,我们案例里先放基础字段。这里有一个生活化的小技巧:真实小票里这三行经常用分隔线和左右对齐的方式排布,右边是数字,左边是标签,中间用空格填充。

区块四:尾部信息。可以是“谢谢惠顾,欢迎再次光临”的温馨提示,也可以是会员积分、退货政策说明。这部分在真实场景里承担营销和告知功能,我们做案例时写一行感谢语就够了。

把输出内容拆成四块之后,代码结构就非常清晰,每个区块写一个函数,最后在主流程里依次调用来组装整张小票。这也是真实项目“模板化”“组件化”思路的雏形。

3. 实操过程与核心代码实现全流程

3.1 环境准备:Python安装与编辑器选择

运行这个案例需要一个Python环境。如果你还没装Python,直接去官网下载安装包,安装时记得勾选“Add Python to PATH”,这样在命令行里输入python才能识别。装好之后打开命令行,输入python --version,能显示版本号就说明环境没问题。

编辑器方面,新手我推荐直接用VS Code,装一个Python扩展就够了。VS Code的好处是集成终端,写代码和跑程序都在一个窗口里,省去来回切换的麻烦。你不需要一开始就折腾复杂的IDE配置,等代码量变大、需要调试功能时再换更专业的工具也完全来得及。

在这个案例中用到的全部是Python标准库,没有第三方依赖。也就是说,只要你装好了Python本身,就能跑通下面所有代码,不需要额外装任何包。

3.2 完整代码:最基础的购物小票

先给你一个最直观的版本。这个版本不处理中文宽度对齐,用最简单的f-string,让你先看到程序的整体骨架。

import datetime goods = [ {"name": "苹果", "price": 8.5, "count": 2}, {"name": "牛奶", "price": 5.0, "count": 1}, {"name": "面包", "price": 12.0, "count": 3}, ] total = 0 print("=" * 30) print(" ××超市购物小票") print("=" * 30) print(f"时间: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}") print("-" * 30) for item in goods: subtotal = item["price"] * item["count"] total += subtotal print(f"{item['name']} {item['price']} x{item['count']} {subtotal:.2f}") print("-" * 30) print(f"合计: {total:.2f} 元") print("=" * 30) print(" 谢谢惠顾,欢迎再次光临!")

这段代码跑起来就能看到一张基本的小票。它用的知识点有字典取值、for循环遍历列表、累加求和、格式化保留两位小数、datetime获取当前时间。这里面{total:.2f}的意思是格式化浮点数并保留两位小数,是金额展示的常用写法。

你可以先跑通这段代码,理解整体逻辑,然后再进入下面的升级版。

3.3 升级版:中文宽度对齐 + 函数封装

这一版是很多人真正需要的一版。它引入了显示宽度计算函数,输出内容能严丝合缝地对齐,再把各部分封装成函数,方便复用。

import datetime def display_width(text): """计算字符串在终端中的显示宽度,中文按2个宽度计算""" width = 0 for char in text: if '\u4e00' <= char <= '\u9fff': width += 2 else: width += 1 return width def pad(text, total_width): """ 按显示宽度左对齐填充空格 total_width是期望占用的显示宽度 """ return text + " " * max(0, total_width - display_width(text)) def print_receipt(goods, store_name="××超市"): """根据商品列表打印购物小票""" print("=" * 36) print(pad(store_name, 36)) now = datetime.datetime.now() print(f"时间: {now:%Y-%m-%d %H:%M:%S}") print("-" * 36) # 表头 print(pad("商品", 16) + pad("单价", 6) + pad("数量", 6) + pad("小计", 8)) print("-" * 36) total = 0 for item in goods: name = item["name"] price = item["price"] count = item["count"] subtotal = price * count total += subtotal print(pad(name, 16) + pad(f"{price:.2f}", 6) + pad(str(count), 6) + pad(f"{subtotal:.2f}", 8)) print("-" * 36) print(pad("合计", 28) + pad(f"{total:.2f}", 8)) print("=" * 36) print(pad("谢谢惠顾,欢迎再次光临!", 36)) if __name__ == "__main__": goods = [ {"name": "苹果", "price": 8.5, "count": 2}, {"name": "牛奶", "price": 5.0, "count": 1}, {"name": "面包", "price": 12.0, "count": 3}, {"name": "进口提拉米苏蛋糕", "price": 39.9, "count": 1}, ] print_receipt(goods)

这段代码里最关键的就是display_width和pad这两个函数。你注意第6行pad(name, 16),它的意思是这个区域总共要占16个显示宽度,如果name是“苹果”,实际显示宽度是4,函数会在后面补12个空格。而“进口提拉米苏蛋糕”显示宽度是18,已经超过16,此时pad函数会返回原字符串,不会截断,也不会补空格,后面内容会挤在一起,这其实是另一个需要注意的问题:列宽和超长商品名之间的取舍,后面会在问题排查讨论。

3.4 金额优化:用整数“分”避免浮点误差

上面这个版本直接用浮点计算金额,看结果可能没问题,但我建议你养成用整数分的习惯。把单价改成以分为单位的整数,计算时全部用整数,只在最终展示时才转成带两位小数的字符串。

def format_money(cents): """把以分为单位的整数转成保留两位小数字符串,如850 -> '8.50'""" yuan = cents // 100 # 整数部分 fen = cents % 100 # 小数部分 return f"{yuan}.{fen:02d}"

对应的商品数据就要改造:

goods = [ {"name": "苹果", "price_cents": 850, "count": 2}, {"name": "牛奶", "price_cents": 500, "count": 1}, ]

计算小计用subtotal_cents = item["price_cents"] * item["count"],合计也全部以分累加。展示时调用format_money(total_cents)。

这个写法可能一开始让你觉得麻烦,但它的价值在于计算结果是完全精确的。比如三件单价0.1元的商品,浮点算出来可能是0.30000000000000004,整数分算出来就是30分,“0.30”,干净利落。实际开发中,金额系统绝大多数都是这么做的。

3.5 保存小票文本 + 利用系统命令实现真实打印

控制台输出只是第一步,真实场景需要把小票内容保存成文件,这样既可以存档,也能交给打印机。实现起来很简单,把print的内容换成写入文件即可,更通用的做法是重构一下,让函数既能打印到控制台,也能写入文件。

一个比较实际的方案是把小票文本先写入txt文件,然后调用系统打印功能。Windows系统下可以用os.startfile(path, "print")把文件交给默认打印程序,或者用subprocess.run(["notepad", "/p", path])调用记事本的打印命令。这些思路比直接控制打印机简单得多,适合个人项目和轻量场景。

更进一步的方案是用reportlab或者其他PDF库,先组合生成PDF小票。PDF的好处是排版可靠,字体不乱,无论哪台机器打开效果都一样。真实商业场景中,大多数小票打印是走热敏打印机厂商的SDK或指令集,这个案例先不展开那么深,但你可以从“控制台 → 文件 → 打印”这条路径体会到开发流程的升级。

3.6 完整实操流程:从代码到打印

我按真实操作顺序给你梳理一遍完整流程。假设你用的是VS Code,文件名叫receipt.py。

第一步,创建项目文件夹,把上面的代码完整粘贴到receipt.py里,保存。第二步,在终端运行python receipt.py,如果没有报错,控制台会显示出对齐整齐的小票内容,这就是程序的“控制台模式”输出。第三步,验证对齐效果,把“进口提拉米苏蛋糕”这种长名称加进去,观察列是否错位,顺便理解为什么需要宽度处理函数。第四步,把print部分的输出同时写入文件,再在资源管理器里双击打开生成的txt,查看视觉效果。第五步,连接一台打印机,测试系统打印命令,确认打印输出与屏幕显示一致。

我自己第一次做完这个流程时最大的感受是:控制台里看着没问题,实际拿打印机打出来以后才发现中文等宽字体和理想效果差距不小。所以如果你手边有打印机,一定要走一遍真实打印流程,很多问题只有到这一步才暴露。

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

4.1 中文对齐错乱、列不对齐

这是出现频率最高的问题,原因就是前面说的Python字符串宽度和显示宽度不一致。如果你遇到小票内容歪歪扭扭,第一件事不是怀疑代码逻辑,而是检查有没有用到按显示宽度填充的函数。系统自带的format、ljust、rjust都不区分中英文宽度,中文一多就会错位。

解决方案是写一个显示宽度计算函数,然后自己实现填充逻辑,替换掉原生的ljust。再补充一点:数字里英文标点和汉字等不同字符宽度也要按实际显示效果处理,不能想当然。如果你发现某个字符比如“·”或者“¥”显示宽度跟预期不符,可以逐步把特殊字符加进宽度函数里兜底。

4.2 中文乱码问题

在Windows控制台里运行Python,打印中文偶尔会出现乱码。这通常跟控制台代码页有关。Python 3在Windows上默认输出编码是UTF-8,但老版本的Windows控制台默认可能是GBK,两边不匹配就乱码。解决办法是写文件时显式指定编码:open("receipt.txt", "w", encoding="utf-8"),这样文件本身是UTF-8,用支持UTF-8的编辑器打开就不会乱。如果控制台乱码,可以在代码开头加上# -*- coding: utf-8 -*-,或者通过设置环境变量PYTHONIOENCODING为utf-8来约束输出编码。

写文件时还有个细节,Windows自带记事本对UTF-8文件会读得很好,但如果你用系统打印命令打印txt,编码也可能造成乱码。最稳妥的方式是生成PDF而不是txt,PDF内部有规定的字体编码,天然规避乱码问题。

注意:不是所有环境都默认UTF-8,如果项目中涉及其他人读代码或跨平台运行,最好在文件头部统一声明编码策略。

4.3 金额显示多位小数或精度错误

如果直接用浮点数累加金额,常见问题是输出出现1.2000000000000002这种长尾巴。这是因为浮点数二进制存储无法精确表示某些十进制小数。解决办法是前文说的用整数分或者Decimal。如果你只是做练习,round也能凑合,但你要知道round不是万能的,它可能因为舍入规则和时机不同产生偏差。展开来说,round(2.675, 2)在Python里结果是2.67而不是2.68,很多人栽在这里。所以涉及金额,我建议从书写代码的第一刻起就用分做整数,后面就不会返工。

4.4 打印出来的小票与屏幕不一致

这种情况多半不是代码问题,而是打印机、驱动、纸宽等多方面因素。热敏打印机小票纸一般是58mm或80mm宽,每行能容纳的字符数是有限的,超出部分会被裁掉或折行。你打印前需要根据纸宽调整每行总宽度,比如58mm纸常见一行能打印32个英文字符,也就是16个汉字。案例里的36宽度更适合80mm纸或普通A4打印,如果你用58mm小票机,要重新度量。

其次,字体不同也会导致实际宽度不同。等宽字体和比例字体下,同样字符数排出来的宽度可能差很多。解决方式是在打印之前,先用一款目标环境的字体做测试,以真实输出的行宽为准。

4.5 文件打印命令没有反应

调用系统打印命令时没有反应,最常见原因是系统里没有设置默认打印机,或者打印服务被关闭。排查顺序是:先确认能打印测试页,再确认默认打印机选对,再检查打印队列是否堵塞。如果你是在办公环境,有时候网络打印机不在线也会导致命令无反应。个人练习环境建议先用PDF输出验证排版,再考虑真实打印,这样可以隔离软件和硬件的问题。

4.6 遇到问题时的通用排查思路

小票程序出问题,不要盯着代码一行行咬文嚼字,按这个顺序来找:

第一,看报错位置。Python报错会给出文件名、行号和错误类型,先看是不是语法错误、缩进错误,这类占了初学者的七成问题。第二,打印中间变量。在关键计算处打印出来,比如每种商品的subtotal、每次累加后的total,看看数据流在哪里断掉。第三,简化数据。把你的商品列表砍到只剩一条,跑通之后再加第二条,定位是通用逻辑问题还是特定数据问题。第四,重读需求。你这一步是想输出什么、对齐成什么样、计算什么金额,先想清楚再改代码,很多时候改着改着忘了本来要干什么,越改越乱。

5. 进阶扩展:让小票真正“落地”到现实工具

5.1 增加会员折扣和满减规则

真实购物小票必然有优惠逻辑。你可以给商品字典加一个“discount”字段,按折扣计算小计;或者在计算合计之后,根据满减规则调整总额。比如满100减10,满200减30,用if判断就能实现。这个扩展非常适合练习条件分支,学起来也不复杂。做完这个扩展之后,你就能理解为什么真实收银系统里优惠计算和商品列表是分开存储、分开展示的。

具体实现思路:商品明细打印原价小计,合计行之前增加一个“优惠金额”行,最后再打印“应收金额”。这比直接把折扣写进每行商品里更接近真实场景。

5.2 对接真实打印机的两种路径

如果你的目标是把小票真正打出来,目前有两类做法。一类是走通用打印方案,就是前面提的生成txt或PDF,再交给系统打印,优点是代码量小,缺点是对纸张宽度、打印指令的控制能力弱。另一类是直接用厂商SDK开发套件,比如常见的58mm热敏打印机厂家都会提供Windows下的动态库或指令文档,用Python的ctype直接调用动态库,或者通过串口/USB发送打印指令。这个路子能做得很底层,控制精细,但代码量和工作量都比较大。

我建议个人学习阶段走第一类,先把业务逻辑和排版做扎实,等到确实需要生产级别的打印时再研究指令协议也不迟。

5.3 让程序接收真实输入

现在的代码是写死商品数据的,接下来把它变成一个“能收数据”的工具就更有意思了。最简单的方式是用内置的input函数交互式录入,复杂一点可以读取csv文件批量生成小票,再复杂一点可以做个简单的Web页面,前端填写商品,后端生成小票。这些方向都能锻炼不同层面的能力。我个人觉得读取csv这个方向性价比最高,因为熟练之后你手上有一份商品清单,随时运行脚本生成小票,很实用。

import csv def load_goods_from_csv(file_path): goods = [] with open(file_path, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: goods.append({ "name": row["name"], "price_cents": int(float(row["price"]) * 100), "count": int(row["count"]), }) return goods

这个函数会读一个包含name、price、count三列的csv文件,把每行转换成字典,供打印函数使用。csv表格可以用Excel编辑,对非程序员也友好,整个工具链瞬间就完整了。

5.4 从案例到系统的思路迁移

把这个小票案例做熟之后,你可以往两个方向迁移。一个方向是往“进销存”靠,小票只是销售环节的末端,往前还有一个商品库存、订单管理、客户信息等一整套东西,你可以逐步加:加一个库存字典,卖出就减库存;加一个订单类,把小票数据对象化。另一个方向是往“数据可视化”靠,把小票里的销售记录汇总成日报表,用matplotlib画一个每天销售额的折线图。不管哪个方向,这个案例都会是你的起点。

我个人在实际操作中的体会是:小票案例最妙的地方在于它像一块积木,单独看很小,但拼到任何系统里都能用。做完这个案例后,再去看开源的后台管理系统,你会发现很多模块本质上就是“取数据、算数据、呈现数据”三个环节,小票只是呈现环节的一个特例,而你已经把这个特例完整掌握了一遍。

最后再分享一个小技巧:如果你打算长期写Python做数据处理,尽量把“数据存储”和“展示排版”分开。这个小票案例里,商品数据是数据层,print和pad函数是展示层。数据层保持纯数据,展示层只管排版,以后你想换Web展示、PDF展示、还是打印机展示,数据层都不用动。很多人在小项目里养成混写的习惯,一到大型项目就吃亏。这个案例麻雀虽小,如果能一开始就养成分层的习惯,后面会受益很久。

做完了这些,你可以把代码保存下来,放一个备注,下次帮朋友写个简单的销售记录工具,直接改改就能用。这就是从“练手案例”到“趁手工具”的转变。

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

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

立即咨询