做技术这几年来,我面试过不少人,也带过刚入行的新人。有一个保留题目我一直舍不得换,就是让候选人写一段温度转换代码:输入一个温度值和单位,转成摄氏或华氏输出。这个题目简单到没有任何算法门槛,正因为简单,它才能像一面镜子一样照出一个程序员的基本功到底扎不扎实。很多人觉得这题太基础,实际上一上来就翻车的并不少:有人忘记校验非法输入,有人把转换公式的系数直接用魔法数字堆在代码里,有人所有逻辑全部塞进 main 函数,还有人连负数、绝对零度这些边界都没想过。今天我把这个“小项目”从第一版到重构版完整拆开,讲清楚高质量代码到底是什么,以及一个合格程序员在这些代码里到底应该体现哪些基本要求。
1. 初版代码长什么样:一段只看得到“能运行”的温度转换
1.1 先看需求:并不是只有 C 和 F
先把这个需求补完整。题目通常长这样:写一个程序,让用户输入一个温度数值和一个单位标识(C 表示摄氏,F 表示华氏),输出另一种单位下的温度。这个需求看着很简单,但“用户输入”四个字就已经埋了很多坑。用户可能输入大写也可能输入小写;可能输成字符串“abc”;可能输一个负温度;可能输入一个无限大的数;也可能在单位标识里多敲了一个空格。这些都不是刁钻需求,而是现实中一定会发生的事。
真正考验人的地方,是你拿到需求后能不能先停下来想清楚程序的边界。谁能告诉我“温度值”的取值范围?如果我输入的是摄氏 1e308 度,再乘以 9/5,得到的是不是 Infinity?这些不是面试官故意抬杠,而是生产环境里每天都在发生的输入噪声。一个只写“能跑”的版本很简单,但是要写一个“在任何垃圾输入下都不会崩溃、不会给出错误结果”的版本,就需要程序员具备真正的工程意识。
1.2 多数新手给出的第一版
很多新入职的同事,第一次写这个题往往会交出类似下面的代码:
def main(): t = float(input("请输入温度值: ")) c = input("请输入单位(C/F): ") if c == "C": f = t * 9 / 5 + 32 print(str(t) + "C = " + str(f) + "F") elif c == "F": c = (t - 32) * 5 / 9 print(str(t) + "F = " + str(c) + "C") else: print("单位错误")这段代码如果在某个内部工具里跑,确实能完成基本换算。但离“高质量代码”还差得很远,甚至可以说是反面教材的典型。
我不说它“错”,我只说它“脆”。它的脆弱体现在很多层面:变量名用了同一个c,一会儿表示摄氏度的单位标识,一会儿表示摄氏温度的结果,后人读起来非常绕;计算公式直接裸写在判断分支里,没有常量,没有函数结构;一旦用户输入"c"这种小写字母,程序就直接进入 else 分支;float(input())如果遇到非数字输入,整个程序会以未处理的异常终止,界面崩得非常难看。这些细节单独看都不致命,但放在一起,就是一段“只能自己用、不敢上生产”的代码。
2. 从这段代码拆高质量代码的四条标准
2.1 函数应该只做一件事,main 不该什么都干
第一版代码里,main 函数几乎承担了一切:读取输入、解析单位、计算转换、输出结果。这在实际项目里是最让人头疼的写法,因为你根本没办法对它做单元测试。你没办法单独调用“把摄氏转华氏”这个功能,因为它的逻辑被埋在input()和print()之间。想测试,就得模拟用户输入,再从标准输出里抓结果,麻烦不说,还非常脆弱。
高质量代码的第一条标准就是职责单一。一个函数只能负责一个层次的事情:有的函数只负责单位换算,有的函数只负责解析用户输入,有的函数只负责提示输出。这样一来,哪个环节出问题就能马上定位,哪里需要复用就抽出来,哪个逻辑要扩展也不至于牵一发动全身。把 main 拆瘦,是重构这段代码的第一步。
2.2 命名是给下一个维护者阅读的,不是给解释器看的
变量名是程序员之间交流的最小单位。第一版代码里的c被反复复用,这种懒惰带来的是阅读成本暴涨。改完单位之后,后面的c到底代表什么?是摄氏度的标识,还是摄氏度的计算结果?只有写这段代码的人自己心里清楚,三天之后他自己可能也分不清。
高质量代码的命名原则很简单:变量名要能读得出语义。unit就是输入的单位标识,celsius就是摄氏温度结果,fahrenheit就是华氏温度结果。函数名也一样,不用写注释,读者看到convert_celsius_to_fahrenheit这个函数名就知道里面在干什么。这里不需要多高级的英文,够直白就行。让代码像大白话一样能读出来,才是真正成熟的表达。
2.3 魔法数字与常量:把公式变成语义
9 / 5、32、5 / 9这些数字,很多人觉得是常识,不需要解释。但“常识”恰恰是最危险的隐藏依赖。如果哪天公式调整,或者要支持另一种温标(比如开尔文),你会发现在多个分支里到处找数字。更麻烦的是,这些数字没有出处,后人不敢随便改,因为不知道它背后的设计意图。
我一般会把它们抽成带名字的常量或直接写成一个带语义的公式函数。比如:
def celsius_to_fahrenheit(celsius: float) -> float: return celsius * 9 / 5 + 32函数名本身就是注释,公式就写在那里,读代码的时候不需要在脑子里翻译“乘以 9 除以 5 加 32 是干嘛的”。如果未来支持开尔文,就再补一个函数,而不是往某个分支里塞新数字。这看起来是小改动,但正是这些细节决定了代码的长期可维护性。
2.4 容错不是面试加分项,而是生产环境的底线
很多新手把“能正确处理合法输入”当成程序成功的唯一标准,这恰恰是职业程序员和学生思维最大的区别。生产环境的程序,面对的输入从来不会严格按照你的预期。用户在输入框里粘贴了一段带空格的内容、输入了非数字字符、甚至不小心点了回车就提交,这些都属于正常情况。如果程序连这些都没准备,那它只能算一个玩具。
所以高质量代码必须在入口处建立防线:数字能不能正确转换,float转换失败时有没有人性化的错误提示,单位标识是否做了大小写和空格的兼容处理。这就叫防御性编程。不是说要把每个错误场景都处理得完美无缺,而是说核心入口必须可控,不能让异常状态悄悄渗透到流程中间,更不能让程序直接抛一堆晦涩堆栈给用户。
3. 优秀程序员在这里会额外考虑什么
3.1 边界条件:绝对零度、小数点、关键字
如果只是把函数拆干净,这题其实还没做完。真正的好程序员会主动去想边界和异常。温度这个东西,在物理上有下限:绝对零度是 -273.15 摄氏度,也就是 0 开尔文。如果用户输入的摄氏温度是 -300 度,这个数值在物理上不成立,程序要不要拦?这里必须跟需求方确认,如果产品只做日常天气换算,那 -300 度可以放行;如果做科学计算,就应该给出明确的报错。
还有一个容易被忽略的边界是浮点精度。比如你输入 0.1 摄氏度,程序输出的华氏温度可能是 32.18,这个看起来没问题。但如果是 97.8 华氏度转摄氏,结果会是 36.5555... 这串循环小数。要不要保留一位小数?要不要用四舍五入?甚至有的内部逻辑里 0.1 加 0.2 不等于 0.3,这些都属于浮点数的经典陷阱。高质量程序不是不管精度,而是明确自己的精度范围,并在文档或注释里告诉使用者。
3.2 测试用例:不该只写“正常运行”
我经常问候选人:你这段代码怎么证明它是对的?如果回答是“我运行了一下,输了 100C,输出 212F,没问题啊”,那就说明他没有测试意识。正确性的证明需要一组测试用例,而不是一次手工点击。针对温度转换,测试用例至少要有这么几类:
| 测试场景 | 输入 | 预期输出 |
|---|---|---|
| 常规摄氏转华氏 | (100, "C") | 212 F |
| 常规华氏转摄氏 | (212, "F") | 100 C |
| 零度换算 | (0, "C") | 32 F |
| 负数换算 | (-40, "C") | -40 F(这是一个很有意思的特殊值,摄氏和华氏在 -40 度相等) |
| 非法数字 | ("abc", "C") | 友好报错,程序不崩溃 |
| 单位大小写 | (10, "c") | 正常转换 |
这一组用例不是测试代码里的分支覆盖率,它反映了程序员对“正确”的定义。高质量代码必须能通过这样的测试矩阵,而不是只能跑通“正常情况”。
3.3 代码评审:从指标转换为评审清单
我自己在做代码评审的时候,不会只盯功能对不对,我最终要判断的是三件事:第一,这段代码能不能被测试;第二,它会不会因为未来一个需求改动而大面积重写;第三,新来的同事能不能不靠作者讲解就读懂它。这三件事跟你用了什么花哨语法没有关系。就算只写最简单的原生代码,只要函数结构清晰、命名准确、输入输出明确,以上三条就都能做到。
把这段代码放到评审清单里,具体来说就是:单元测试会不会被 IO 阻塞;函数是纯计算还是包含副作用;异常输入是否会绕过校验;数值范围有没有定义;注释是否在解释“为什么”而不是解释“是什么”。这五点基本能判断一个程序员从“会写代码”到“能写产品级代码”的跨度。
4. 实操记录:一次完整的“由简到完整”重构过程
4.1 第一轮:先保证接口清晰
我一般建议新手不要一上来就追求完美,先写一个能跑的版本,然后再一步一步改。第一步把核心转换逻辑从 main 里抽出来,转成两个独立函数。函数接收一个float,返回一个float,不碰输入,不碰输出,这样这两个函数就是天然的纯函数,可以被单测直接调用。
def celsius_to_fahrenheit(celsius: float) -> float: return celsius * 9 / 5 + 32 def fahrenheit_to_celsius(fahrenheit: float) -> float: return (fahrenheit - 32) * 5 / 9到这一步,转换逻辑已经可以脱离命令行环境单独验证。想用pytest写测试也行,想在 REPL 里手工调也行,都没有 IO 的阻碍。
4.2 第二轮:常量、异常与提示信息
接下来处理输入层。我不能直接把float(input(...))写在主流程里,因为一旦解析失败,用户看到的就是一个难看的异常终止。比较合理的做法是先读取原始字符串,再单独写一个安全转换函数,对非法输入做友好提示。
UNIT_CELSIUS = "C" UNIT_FAHRENHEIT = "F" def parse_temperature(raw_number: str) -> float | None: try: return float(raw_number) except ValueError: return None这类解析函数是有真实价值的:它把“能不能转成数字”和“转成数字之后怎么用”拆开了。以后如果你想让用户输入-40C这样连在一起字符串,只需要改这个解析函数,转换函数根本不用动。
4.3 第三轮:加注释但别写废话
好的注释从来不解释代码在做什么,而是解释这段代码为什么要这么做。例如“为什么单位统一转成大写”这句话可以不写,因为代码里已经写了.strip().upper();真正需要注释的是类似“物理学中摄氏温度下限是 -273.15,但本工具只做日常温度展示,因此不拦截负值”这样的设计决策。没有这句注释,后来人很容易“帮你”加上一个绝对零度校验,然后改变原有行为。
我还见过很多缩写变量、含义含糊的注释,比如# t 是温度,这完全是废话。命名已经表达了含义,注释再来一遍只会增加阅读负担。高质量注释应该是那些“代码无法表达,但维护者必须知道”的知识。
4.4 第四轮:用测试固定行为
重构的最后一定不是自查,而是写测试。测试的意义在于把行为固化下来,让后来的人敢于修改代码。没有测试的代码就像没有护栏的悬崖边,谁都不敢乱动。我给这个温度转换项目写过一组最小测试,大致长这样:
from temperature_converter import celsius_to_fahrenheit, fahrenheit_to_celsius def test_boiling_point(): assert celsius_to_fahrenheit(100) == 212 def test_freezing_point(): assert celsius_to_fahrenheit(0) == 32 def test_minus_40(): assert celsius_to_fahrenheit(-40) == -40 assert fahrenheit_to_celsius(-40) == -40这组测试不是给面试官看的,是给自己留的后路。以后想重构,先跑一遍测试;如果全绿,说明行为没变。这比任何代码规范都更能保证项目安全。
5. 常见问题与排查技巧实录
5.1 排查表:从现象到原因到修复思路
| 现象 | 可能原因 | 排查方向 | 建议修复 |
|---|---|---|---|
输入10c后报单位错误 | 没有对输入做大小写归一化 | 检查单位解析处是否调用upper()或lower() | 统一转大写或小写后再判断 |
输入abc直接崩了 | float()转换异常未捕获 | 在解析函数外找有没有try/except | 使用安全解析函数,失败时返回空值 |
输出显示100.00000000000001 | 浮点数二进制表示导致精度尾巴 | 检查格式化方式,是否直接print了原始浮点数 | 用round(value, 2)或字符串格式:.2f统一精度 |
| 华氏 32 度转摄氏后不是 0 | 公式或变量名混用 | 检查是(f - 32) * 5 / 9还是f * 5 / 9 - 32 | 对照公式逐项核对,并写单测固定结果 |
| 程序在黑窗口中一闪而过 | 缺少if __name__ == "__main__"或入口循环 | 检查主函数是否被执行 | 加入入口判断,并考虑是否要做循环重试 |
5.2 浮点数精度:怎么跟别人解释 0.1 + 0.2 不等于 0.3
温度转换里最容易踩的坑就是浮点数精度。比如华氏 97.8 度转摄氏,很多语言直接算出来会是 36.55555555555556。这不是公式错了,也不是语言错了,而是小数的二进制表示问题。就像十进制没法精确表示三分之一一样,二进制也没法精确表示所有小数部分。面对这个问题,不要试图“修好浮点数”,正确的做法是在输出层控制精度。默认保留两位小数或四位小数,内部计算继续使用浮点,但展示结果时格式化。要记住一件事:用户关心的是温度计上的读数,不是 float 寄存器里的二进制尾巴。
5.3 我常用的三个自查动作
写程序有一个很土但很有效的自查流程。第一,随意输入一堆乱码,看看程序会不会崩溃;第二,把输入值改成极端数值,比如百万、负百万、小数点后很多位,观察结果是否合理;第三,读一遍自己的函数名和变量名,想象自己是三个月后的另一个程序员,看能不能一眼读懂。这三个动作加起来用不了五分钟,但能挡住一大半“能跑但不敢动”的代码。我在实际工作里,很多同事就是在自查阶段发现了单位大小写、未捕获异常和魔法数字问题。
6. 最终参考版本:把重构后的代码完整串起来
说了这么多,我把一个经过了边界校验、命名规范、职责拆分和精度控制的参考版本整理如下。它不算什么精妙绝伦的代码,但它足够诚实,也足够见功力:
UNIT_CELSIUS = "C" UNIT_FAHRENHEIT = "F" def celsius_to_fahrenheit(celsius: float) -> float: return celsius * 9 / 5 + 32 def fahrenheit_to_celsius(fahrenheit: float) -> float: return (fahrenheit - 32) * 5 / 9 def parse_temperature(raw: str) -> float | None: try: return float(raw.strip()) except ValueError: return None def normalize_unit(raw_unit: str) -> str | None: unit = raw_unit.strip().upper() if unit not in (UNIT_CELSIUS, UNIT_FAHRENHEIT): return None return unit def main() -> None: raw_temperature = input("请输入温度值: ") temperature = parse_temperature(raw_temperature) if temperature is None: print("温度值必须是数字") return raw_unit = input("请输入温度单位(C/F): ") unit = normalize_unit(raw_unit) if unit is None: print("单位必须是 C 或 F") return if unit == UNIT_CELSIUS: result = celsius_to_fahrenheit(temperature) print(f"{temperature:.2f}C = {result:.2f}F") else: result = fahrenheit_to_celsius(temperature) print(f"{temperature:.2f}F = {result:.2f}C") if __name__ == "__main__": main()这个版本同样只有二三十行,但每一行都有明确存在理由。parse_temperature负责解析,normalize_unit负责归一化单位,两个转换函数负责纯计算,main只做流程编排。以后再接图形界面也好,接 Web 服务也好,直接复用上层函数,命令行入口只是其中一个调用者。
我特别建议新人把这个版本当作一面镜子,对照自己的代码逐条问一遍:我的输入解析安全吗?我的单位判断兼容大小写吗?我的转换函数可以被单测吗?我的变量名有没有重复使用?这四条真正做到位,说明你已经从“能运行”走到“可维护”这个阶段了。
说一个我自己的体会吧。我裁过很多份代码,见过形形色色的候选人,我并不指望谁能在十分钟里写出什么惊世骇俗的设计,我只想看他面对一个平凡得不能再平凡的需求时,有没有养成把边界、命名、结构和错误处理都纳入思考的习惯。温度转换这道题最妙的地方就在这里:它没有任何套路可背,所有真相都藏在代码的细节里。能把基础代码写干净的人,写复杂系统通常也不会太差;反过来,基础代码稀烂的人,就算会用一堆高大上框架,也大概率是给后面的同事埋雷。把这二三十行代码写明白,比背一百个面试题都有用。