freeCodeCamp 每日编程挑战解析:Python 实现 Pounds to Kilograms 单位换算与格式化输出
freeCodeCamp 每日编程挑战解析:Python 实现 Pounds to Kilograms 单位换算与格式化输出
本篇技术指南以 freeCodeCamp 开源仓库中「每日编程挑战(Daily Coding Challenges)」Python 模块的第 120 题 Challenge 120: Pounds to Kilograms(磅转千克)为讲解主体,完整还原题目描述、5 个自动化测试用例、种子代码与官方参考实现,并深入剖析 f-string 保留两位小数、名词单复数切换等核心技巧。读完本文,你将掌握如何在 freeCodeCamp 的 Python 挑战环境中写出通过全部 unittest 断言的健壮解法,并理解这类挑战在仓库中的运行机制与验证链路。
挑战概览:它在整个项目中处于什么位置
该挑战文档位于仓库的 daily-coding-challenges-python 块中,是该 Python 系列 200 多道每日一题中的第 120 题。从块配置可以看出该模块的关键属性:
challengeType: 29:Python 编码挑战类型(对应前端ShowClassic组件中的challengeType: 29,见 show-daily-coding-challenge.tsx);usesMultifileEditor: true:启用多文件编辑器,Python 侧使用main.py作为种子文件;helpCategory: "Python":归入 Python 帮助分类。
仓库同时维护了同编号的 JavaScript 版本(daily-coding-challenges-javascript 块),并由 daily-challenges.test.js 通过 Vitest 自动断言两个语言版本在 id、标题、描述、测试数量上保持一一对应。也就是说,这道题不仅是一道独立练习,更是 freeCodeCamp「同题双语言」课程体系的一部分。
题目要求逐条拆解
原文档(691b559495c5cb5a37b9b480.md)给出了明确规格:实现函数 convert_to_kgs(lbs),给定一个以磅为单位的数值,返回字符串 "(lbs) pounds equals (kgs) kilograms.",并满足以下规则:
"(lbs)"处替换为输入的磅数(原样输出,不做格式化);"(kgs)"处替换为换算后的千克数,四舍五入保留两位小数,且始终显示两位小数(例如0.45、1.00、45.36);- 换算系数固定为 1 磅 = 0.453592 千克;
- 当输入为
1时,单位词用单数pound,否则用复数pounds; - 当换算结果为
1时,单位词用单数kilogram,否则用复数kilograms。
表面上是单位换算,实质考验的是三点:浮点数乘法的精度处理、字符串格式化的「补零」能力,以及基于最终显示文本而非数值本身做单复数判断的思维。
五个自动化测试用例:理解断言边界
原文档在 # --hints-- 段给出了 5 个测试,全部通过 runPython 注入 Python 的 unittest.TestCase().assertEqual 执行。逐一对照如下:
输入 lbs |
期望返回值 | 考察点 |
|---|---|---|
1 |
"1 pound equals 0.45 kilograms." |
单数 pound;0.453592 四舍五入为 0.45 |
0 |
"0 pounds equals 0.00 kilograms." |
结果 0.0 必须显示为 0.00(两位小数补齐) |
100 |
"100 pounds equals 45.36 kilograms." |
常规换算:100 × 0.453592 = 45.3592 四舍五入为 45.36 |
2.5 |
"2.5 pounds equals 1.13 kilograms." |
浮点输入与进位:2.5 × 0.453592 = 1.13398 → 1.13 |
2.20462 |
"2.20462 pounds equals 1.00 kilogram." |
千克侧恰好为 1,使用单数 kilogram |
注意最后一个用例:2.20462 × 0.453592 ≈ 1.00000,换算结果显示为 1.00 且单位取单数 kilogram——这是全题最容易遗漏的边界条件。输入侧 2.20462 与输出侧 1.00 都不是整数 1,但判定「是否为 1」必须基于格式化后的显示值,这正是官方参考实现的精妙之处。
种子代码与解题思路
挑战在编辑器中提供如下种子函数(main.py):
def convert_to_kgs(lbs):
return lbs
种子只完成了「原样返回输入」,显然无法通过任何断言。解题的最小步骤是:
- 用换算系数
0.453592计算千克值; - 用格式化语法保留两位小数并强制补齐;
- 分别根据输入值和换算结果显示单复数单位;
- 按模板拼接完整句子。
官方参考实现逐步拆解
def convert_to_kgs(lbs):
KG_PER_POUND = 0.453592
kgs = f"{lbs * KG_PER_POUND:.2f}"
pound_word = "pound" if lbs == 1 else "pounds"
kilogram_word = "kilogram" if kgs == "1.00" else "kilograms"
return f"{lbs} {pound_word} equals {kgs} {kilogram_word}."
逐行解读其中的三个关键设计:
① 常量命名与可读性
KG_PER_POUND = 0.453592 把魔法数字提取为语义化常量,对应题目「1 pound equals 0.453592 kilograms」的换算基准。虽然本题只使用一次,但显式常量让审阅者一眼看懂换算依据,也方便后续按比例调整。
② f-string 的 .2f 格式说明符
f"{lbs * KG_PER_POUND:.2f}" 做了三件事:执行浮点乘法、四舍五入到小数点后两位、不足两位时用 0 补齐。因此 0 * 0.453592 = 0.0 会显示为 "0.00",1 * 0.453592 ≈ 0.453592 会显示为 "0.45"(四舍五入),完美命中测试用例 1、2 的预期。这也是为什么结果被显式转成字符串 kgs 而不是保留 float——为下一步的字符串比较做准备。
③ 基于显示值而非数值的单复数判断
pound_word = "pound" if lbs == 1 else "pounds":直接比较输入数值,覆盖convert_to_kgs(1)返回"1 pound ..."的用例;kilogram_word = "kilogram" if kgs == "1.00" else "kilograms":比较的是格式化后的字符串"1.00"而非浮点数1.0。原因在于,当换算结果接近 1 时(如输入2.20462),浮点计算值可能是0.9999999...或1.0000001...,直接做数值比较会因浮点误差导致判断不稳定;而.2f格式化后统一收敛为"1.00",字符串比较既精确又可读。
④ f-string 模板拼接
f"{lbs} {pound_word} equals {kgs} {kilogram_word}." 直接复刻题目要求的输出模板,输入值 lbs 保持原始形式(整数 1、100,浮点 2.5、2.20462 原样输出),与全部 5 个断言逐字符吻合。
本地复现与验证:超越编辑器直接跑通全部断言
原文档的断言依赖平台内置的 runPython,但你完全可以脱离平台在本地验证解法。将参考实现保存为 main.py,再手动复刻测试逻辑:
from unittest import TestCase
from main import convert_to_kgs
tc = TestCase()
tc.assertEqual(convert_to_kgs(1), "1 pound equals 0.45 kilograms.")
tc.assertEqual(convert_to_kgs(0), "0 pounds equals 0.00 kilograms.")
tc.assertEqual(convert_to_kgs(100), "100 pounds equals 45.36 kilograms.")
tc.assertEqual(convert_to_kgs(2.5), "2.5 pounds equals 1.13 kilograms.")
tc.assertEqual(convert_to_kgs(2.20462), "2.20462 pounds equals 1.00 kilogram.")
print("All 5 assertions passed.")
运行时只需在 main.py 所在目录执行 python main.py 即可看到全部断言通过。这也验证了题目在设计上的自洽性:5 个用例覆盖了整数/零/浮点输入、四舍五入进位、小数补零、两侧单复数共 5 类边界,是一道测试驱动设计的典型样例。
从源码看这道题如何被加载与执行
理解「为什么这样实现」后,不妨从仓库源码追溯这道题的完整运行链路:
- 内容来源:挑战 Markdown 文件(含描述、hints、seed、solution)位于 curriculum/challenges/english/blocks/daily-coding-challenges-python 目录,由 curriculum 的构建脚本解析为结构化数据;
- 前端加载:客户端通过
GET /daily-coding-challenge/day/{MM-DD}从 API 拉取当日挑战数据(见 api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 与 api/src/daily-coding-challenge/README.md); - 数据校验:show-daily-coding-challenge.tsx 在渲染前用 daily-coding-challenge-validator.ts 校验响应结构;
- 挑战渲染:Python 版本被组装为
challengeType: 29、文件main.py、helpCategory: 'Python'的 challengeNode,交由经典的ShowClassic模板渲染(见 show-daily-coding-challenge.tsx); - 测试执行:
# --hints--中的断言通过runPython在浏览器内以unittest.TestCase运行,与你在本地执行的逻辑完全一致。
入口组件还支持语言切换(JavaScript / Python,记忆在 dailyCodingChallengeLanguage 存储键中),并且通过 helpers.ts 中的日期工具将「月-日」映射到当天挑战(如 2 月 29 日映射到 2 月 28 日),保证每日一题的稳定展示。
延伸思考:同一模式的其他挑战
磅转千克并不是孤立的一题。该 Python 块中还存在大量「单位换算 + 格式化输出」的姊妹题,例如 Challenge 113: Miles to Kilometers(英里转千米)与本体的差异仅在换算系数与输出模板,解题模式完全可复用;此外还有 Challenge 71: Tip Calculator、Challenge 77: Duration Formatter 等,都围绕「数值计算 + 严格文本格式」展开。刷题时只要掌握了「格式化字符串先行、再基于显示文本做判断」的思路,就能举一反三。
小结
Challenge 120: Pounds to Kilograms 是一道「小而美」的格式化编程题:它用 5 个断言把浮点精度、.2f 补齐、基于字符串的单复数判断全部串了起来。参考实现中「先 f"{...:.2f}" 再与 "1.00" 比较」的做法,是处理浮点边界问题的实用范式,值得在更复杂的单位换算、金额格式化场景中沿用。完整题目原文与官方解法可直接查阅仓库中的 691b559495c5cb5a37b9b480.md。