Python进阶与并发编程01:报价为什么不能随手改——对象、引用与精确金额
Python进阶与并发编程01报价为什么不能随手改——对象、引用与精确金额你已经会写列表、字典和函数。现在有一个真实后端需求用户购买KB-001系统向几家供应商询价选出能供货且含运费最便宜的一家。功能第一天似乎只需要min(prices)第二天就有人反馈“昨天查两件还是这个价今天查一件怎么把两件的结果也改了”这类错误并不需要线程才会发生只要两个变量指向同一个可变对象普通同步程序也会踩中。本系列的项目叫 QuoteFlow。本课先把不会因并发而改变的业务规则讲清楚金额怎么算库存怎样筛选数据归谁所有同价时怎样排序。后面无论换成 HTTP、线程池还是协程都要保留这些规则。环境只需要 Python 3.11本课没有网络请求或第三方依赖。先手算一次建立可检查的目标供应商报的是单价、运费和可用库存。金额统一使用整数分运费按本次订单收一次。供应商单价/分运费/分库存买1件总价买2件总价atlas129006001001350026400boreal125009001001340025900cedar131000113100不可售买一件时 cedar 胜出买两件时 cedar 不够货boreal 胜出。公式是unit_cents * quantity shipping_cents不能写成(unit_cents shipping_cents) * quantity。请求数量必须是 1–100 的整数。库存不足可以是正常业务状态不要为了凑齐三家而把它放进最低价排名。图里最重要的箭头是“生成新结果”。原始报价代表供应商告诉我们的事实按两件计算出的总价是当前查询的派生结果。这两种数据不应该混装在同一个可变字典中。名字不是装值的独立盒子先在交互解释器运行这个反例original{supplier:atlas,shipping_cents:600}for_promotionoriginal for_promotion[shipping_cents]0print(original[shipping_cents])# 0赋值把for_promotion绑定到同一个字典并没有克隆字典。original is for_promotion判断的是身份此时为真判断的是值是否相等。调试共享状态问题时把“同一个对象”和“内容相同的两个对象”混在一起会让你找错方向。你也许会改成for_promotion original.copy()。当前字典只有整数和字符串这样通常够用但浅复制只复制最外层容器。如果字典新增tags[standard]两个字典里的tags仍可能指向同一个列表。deepcopy可以递归复制更多对象却不能替你决定哪些事实可以修改、哪些资源可以复制。数据库连接、锁和网络客户端尤其不是“深复制一份”就能获得新所有权。我们的报价模型只放简单值并让修改以新对象的形式出现。列表也有相同问题。sorted(quotes)创建新列表quotes.sort()修改原列表两者都不会自动复制列表里的报价对象。选择sorted能避免影响调用方的列表顺序但若元素仍是可变字典元素字段依然能被改动。这正是本课同时处理容器和元素的原因。用 dataclass 表达一份报价事实Quote有五个字段分别对应供应商、SKU、单价、运费和库存。dataclass会生成初始化方法、方便调试的字符串表示和按字段比较的相等判断它减少的是重复样板不会自动理解业务。dataclass(frozenTrue,slotsTrue)classQuote:supplier:strsku:strunit_cents:intshipping_cents:intstock:intfrozenTrue使普通属性赋值被拒绝。如果促销要免运费我们写replace(quote, shipping_cents0)得到一个新Quote。原来的报价继续保持 600 分运费任何已经持有它的调用方都不会被促销逻辑悄悄影响。这里的“不可变”是常规使用下的字段不可重新绑定不是 Python 级别的绝对安全隔离而且 frozen 不会递归冻结嵌套列表。所以本模型特意只保存字符串和整数。slotsTrue限制实例通常可拥有的属性也通常减少每个实例的额外存储。它不是正确性的核心不能把它当作性能优化的万能开关。它还有一个教学好处写错字段名不会静悄悄创建新字段。例如把shipping_cents写成shiping_cents应该尽早暴露。类型提示unit_cents: int方便人和类型检查器理解接口但 Python 运行时不会因为这行注解而禁止传入字符串。因此完整代码在__post_init__中做校验。这个方法在 dataclass 生成的初始化完成后执行非常适合检查“一个对象一旦创建就必须满足”的约束。本例允许零单价、零运费和零库存但拒绝负数金额精度不是靠“先接受任意值以后再补救”。一个容易忽略的边界是boolPython 中isinstance(True, int)为真。如果接口要求严格的数量整数True不能意外代表买一件。所以这里使用type(value) is int而不是宽泛的isinstance(value, int)。这是针对业务输入的有意选择需要接收 NumPy 整数等类型的库可能会采用不同规则不能机械照搬。金额为什么不存 float二进制浮点数无法精确表示所有十进制小数常见实验是0.1 0.2 0.3为假。报价累计、比较和审计需要一致规则在只有“分”这一精度的合同中整数既直接又足够。12900表示 129.00 元不是 12900 元变量名里的_cents能把单位带到调用处。也不要把int(float_text * 100)当成可靠的人民币解析器。若上游返回十进制金额字符串应在接入边界使用Decimal并定义保留几位、怎样舍入再转换为分。本系列的上游合同已经提供整数分所以核心模型无需承受两套单位。展示给人看时才格式化单位排序时仍比较整数。使用整数并不意味着金额的全部问题都解决了。多币种汇率、税费、优惠顺序和单项舍入属于新的业务规则本课没有实现这些规则。保持范围清楚比提前塞进一套货币框架更有助于学习对象设计。原始报价和当前查询结果分开PricedQuote包含quote和total_cents。这个小对象使调用方能同时看到原始单价和当前数量对应的总价而不需要修改Quote。rank_quotes接受报价可迭代对象、数量和 SKU先校验数量再过滤 SKU 和库存最后计算并排序。为什么先校验数量再进入列表推导式如果校验藏在循环里空输入不会执行循环rank_quotes([], 0)就可能悄悄返回空列表。非法请求不应该因为供应商恰好为空就突然合法。本课的测试特意使用空输入验证这条边界避免测试只覆盖“有报价”的顺利路径。排序键是lambda item: (item.total_cents, item.quote.supplier)。Python 按元组从左向右比较先比较总价只有相同时再比较供应商名称。这是明确的业务平局规则。Python 的排序本身稳定只表示键相等时保留原先顺序如果原先顺序将来由网络完成顺序决定仅依赖“稳定排序”仍会让相同价格的结果时前时后。把名称放进键才能消除到达顺序的影响。本系列默认供应商名使用小写 ASCII 标识排序不是给中文展示名称做语言学排序。本模型允许把 Iterable 传进来后续生成器也能使用但完整排序需要收集可售结果内存开销随可售报价数量增长。三家供应商没有问题千万行离线数据就要考虑只求最小值或外部排序。一个函数能接生成器不等于整个函数自动拥有常量内存。运行并解释结果以下命令均从课程根目录python-advanced-concurrency-course执行。Windows PowerShellpy-3.11 lessons/01/quote_model.py py-3.11-m unittest discover-s lessons/01-ptest_*.py-vLinux/macOS 使用python3.11替换py -3.11。完整脚本输出如下quantity1 [(cedar, 13100), (boreal, 13400), (atlas, 13500)] quantity2 [(boreal, 25900), (atlas, 26400)] original_shipping600 new_shipping0前两行验证手算目标后两行验证replace创建新值。测试还覆盖布尔数量、负金额、其他 SKU、库存为零以及同价结果是否独立于输入顺序。测试的期望值是手算出来的字面量而不是调用同一套公式重新计算期望否则公式写错时测试与实现可能一起错。本课没有测试 dataclass 能否工作而是在测试我们交给它的业务约束。比如把stock quantity错写成stock quantity库存恰好一件的 cedar 就会被错误排除“买一件”的断言会立即失败。这样的测试才有保护规则的作用。练习 E01数量变化与相同总价使用本课Quote与rank_quotes单独写一个可运行脚本完成以下验收对标准三家报价分别查询 quantity1 和 quantity2。输出必须依次为[(cedar,13100),(boreal,13400),(atlas,13500)]与[(boreal,25900),(atlas,26400)]。构造输入顺序为 boreal、atlas 的两条报价boreal 单价1100分、运费100分、库存10atlas 单价1000分、运费300分、库存10SKU都是KB-001。quantity2 时两家均为2300分必须先 atlas 后 boreal反转输入后结果仍相同。使用断言证明查询没有修改标准报价的供应商顺序。不要通过原地排序来实现。下一篇提供完整答案lessons/02/answer_previous.py并解释为什么“排序稳定”和“结果可重复”不是同一个要求。完整可运行代码将下面每个代码块保存到标注路径目录结构与课程下载包一致。测试是可执行验收材料日常运行只需quote_model.py。后续课程保留本模型的独立快照避免要求读者通过数字目录做跨课导入。lessons/01/quote_model.pyImmutable quote values and deterministic, integer-money ranking.fromdataclassesimportdataclass,replacefromtypingimportIterabledefvalidate_quantity(quantity:int)-None:iftype(quantity)isnotintornot1quantity100:raiseValueError(quantity must be an integer from 1 to 100)dataclass(frozenTrue,slotsTrue)classQuote:supplier:strsku:strunit_cents:intshipping_cents:intstock:intdef__post_init__(self)-None:fornamein(supplier,sku):valuegetattr(self,name)ifnotisinstance(value,str)ornotvalue.strip():raiseValueError(f{name}must be a nonempty string)fornamein(unit_cents,shipping_cents,stock):valuegetattr(self,name)iftype(value)isnotintorvalue0:raiseValueError(f{name}must be a nonnegative integer)dataclass(frozenTrue,slotsTrue)classPricedQuote:quote:Quote total_cents:intdefrank_quotes(quotes:Iterable[Quote],quantity:int,sku:strKB-001)-list[PricedQuote]:validate_quantity(quantity)priced[PricedQuote(quote,quote.unit_cents*quantityquote.shipping_cents)forquoteinquotesifquote.skuskuandquote.stockquantity]returnsorted(priced,keylambdaitem:(item.total_cents,item.quote.supplier))defsample_quotes()-tuple[Quote,...]:return(Quote(atlas,KB-001,12900,600,100),Quote(boreal,KB-001,12500,900,100),Quote(cedar,KB-001,13100,0,1),)defmain()-None:quotessample_quotes()forquantityin(1,2):rankedrank_quotes(quotes,quantity)print(fquantity{quantity},[(p.quote.supplier,p.total_cents)forpinranked])free_shippingreplace(quotes[0],shipping_cents0)print(original_shipping,quotes[0].shipping_cents,sep)print(new_shipping,free_shipping.shipping_cents,sep)if__name____main__:main()lessons/01/test_quote_model.pyimportunittestfromdataclassesimportFrozenInstanceError,replacefromquote_modelimportQuote,rank_quotesclassQuoteRulesTests(unittest.TestCase):defsetUp(self):self.quotes[Quote(atlas,KB-001,12900,600,100),Quote(boreal,KB-001,12500,900,100),Quote(cedar,KB-001,13100,0,1),]deftest_quantity_changes_eligibility_and_total(self):onerank_quotes(self.quotes,1)tworank_quotes(self.quotes,2)self.assertEqual([(p.quote.supplier,p.total_cents)forpinone],[(cedar,13100),(boreal,13400),(atlas,13500)])self.assertEqual([(p.quote.supplier,p.total_cents)forpintwo],[(boreal,25900),(atlas,26400)])self.assertEqual([q.supplierforqinself.quotes],[atlas,boreal,cedar])deftest_equal_totals_use_supplier_not_arrival_order(self):quotes[Quote(boreal,KB-001,1100,100,10),Quote(atlas,KB-001,1000,300,10)]self.assertEqual([p.quote.supplierforpinrank_quotes(quotes,2)],[atlas,boreal])deftest_reject_invalid_quantity_even_when_no_quotes(self):forquantityin[0,101,True,1.5,2]:withself.subTest(quantityquantity),self.assertRaises(ValueError):rank_quotes([],quantity)deftest_reject_invalid_money_and_stock(self):forchangesin[{unit_cents:-1},{unit_cents:True},{shipping_cents:1.5},{stock:-1}]:withself.subTest(changeschanges),self.assertRaises(ValueError):replace(self.quotes[0],**changes)deftest_replacement_does_not_change_original(self):originalself.quotes[0]adjustedreplace(original,shipping_cents0)self.assertEqual(original.shipping_cents,600)self.assertEqual(adjusted.shipping_cents,0)withself.assertRaises(FrozenInstanceError):original.stock999deftest_other_sku_and_no_stock_have_no_best_offer(self):quotes[Quote(atlas,OTHER,1,0,100),Quote(boreal,KB-001,1,0,0)]self.assertEqual(rank_quotes(quotes,1),[])if__name____main__:unittest.main()

相关新闻

Agent工程化落地的5个关键问题与实战方案

Agent工程化落地的5个关键问题与实战方案

1. 这不是考算法,而是考你能不能把Agent真正跑起来“请手写一个ReAct框架”——这种问题在三年前可能还出现在某些技术面试里,但今天再问,基本等于在面试一个还没摸过生产环境的在校生。我参与过二十多场面向中高级工程师的Agent方向技术面试…

2026/10/16 7:53:14 阅读更多 →
AI日报系统设计:从信源治理到时间戳工程的全链路实践

AI日报系统设计:从信源治理到时间戳工程的全链路实践

1. 项目概述:这不是一份“新闻简报”,而是一套可复用的AI内容日更系统“AI 日报 2026-10-07”这个标题乍看像某平台每日推送的资讯合集,但真正有经验的人一眼就能看出——它背后必然藏着一套稳定、低干预、可持续输出的自动化内容生产流程。…

2026/10/16 7:53:14 阅读更多 →
LTC681X官方驱动源码全解析:BMS电池采样与均衡控制实践

LTC681X官方驱动源码全解析:BMS电池采样与均衡控制实践

简介:LTC681X系列是ADI面向高精度电池管理系统(BMS)设计的多通道AFE芯片,这套官方驱动源码主要面向电池管理开发者与嵌入式软件工程师,解决多节串联电池电压采样、隔离SPI通信及数据解析等核心难题。资源包采用rar压缩…

2026/10/16 7:53:14 阅读更多 →

最新新闻

C#面向对象:Person类、Student类继承与构造函数全解析

C#面向对象:Person类、Student类继承与构造函数全解析

1. 面向对象的第一步:从Person类的设计说起很多人学C#敲的第一段面向对象代码,就是从定义人类Person开始的。这个题目看着简单,但背后其实藏着一堆值得掰扯清楚的细节——构造函数怎么写、字段和属性有什么区别、继承之后构造函数怎么联动。我…

2026/10/16 10:41:46 阅读更多 →
Tailwind CSS组件库v6.23:纯CSS驱动的62个可复用UI模块

Tailwind CSS组件库v6.23:纯CSS驱动的62个可复用UI模块

简介:本资源是 Tailwind CSS 6.23 版本的完整组件实践包,面向前端开发者、UI 工程师及响应式项目快速搭建者,解决传统 CSS 开发效率低、定制难、响应式适配繁琐等痛点。压缩包含 2007 个文件,主体为 1304 个 HTML 示例页&#xff…

2026/10/16 10:41:46 阅读更多 →
线性加权滑移平均精准捕捉局部小峰值的Matlab实现与调参

线性加权滑移平均精准捕捉局部小峰值的Matlab实现与调参

前阵子处理一段传感器信号时,我遇到一个特别典型的困境:数据里明明有很关键的局部小峰值,结果一用常规的滑动平均去噪,峰值幅度被削掉一大半,位置还往后偏移了好几个采样点。后来换成线性加权滑移平均算法,…

2026/10/16 10:41:46 阅读更多 →
用Niagara制作逼真火焰:噪声、颜色曲线与透明淡出全解析

用Niagara制作逼真火焰:噪声、颜色曲线与透明淡出全解析

从小我就觉得粒子特效是门“骗术”:喷泉喷的是水,火焰烧的是粒子,但只要观感对了,没人会在意那是不是真的流体。喷泉那一篇发出来后,不少人问我能不能把发射器关掉重力、换个橙色贴图,直接当火焰用。我的回…

2026/10/16 10:41:46 阅读更多 →
Utilman.exe丢失或找不到怎么办?系统自带安全修复方法全解析

Utilman.exe丢失或找不到怎么办?系统自带安全修复方法全解析

系统突然出现“Utilman.exe文件丢失或找不到”的报错,登录界面右下角的“轻松使用”按钮点了没反应,甚至开机就卡在欢迎界面。这个问题在Windows 7到Windows 11上都可能出现,不少朋友第一反应是去第三方下载站找个exe文件补上——我劝你千万打…

2026/10/16 10:41:46 阅读更多 →
AI逆向工程实战:大语言模型如何将二进制翻译为伪代码

AI逆向工程实战:大语言模型如何将二进制翻译为伪代码

这个标题是我在刷开源社区动态的时候看到的,第一反应是“又来了个套壳项目”,但点进去翻了源码和论文之后,我承认我小看它了。这个项目目前在 GitHub 上拿下了 31.2K Star,热度确实不是刷出来的。我花了两天时间把它部署到本地&am…

2026/10/16 10:40:41 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →