C00 · 第 1 章 · 第一编 程序与数据
程序、类型、控制流与函数
把程序视为从输入和初始状态到输出与新状态的可检查转换,以 Python 风格代码说明表达式、基本类型、分支、循环、函数、作用域和契约,并处理边界输入、终止与受控副作用。
报告页面错误本章目标
- 区分表达式、变量名、值与状态更新,正确使用整数、浮点、布尔、文本和空值。
- 设计覆盖全部合法输入的分支,明确相等边界、非法输入和不可达分支。
- 用循环不变量和单调变元说明循环正确性与终止,避免越界和无限循环。
- 用参数、返回值、局部作用域及前置/后置条件设计单一职责函数。
- 区分纯函数与输入输出等副作用,使失败可传播、行为可测试且资源修改受控。
本页目录
程序是可检查的状态转换
程序接收输入,在一系列规则下计算,并产生返回值、输出或新状态。最简单的纯计算可写成
若程序还修改文件、随机数状态或设备,就应把这些外部状态也视为输入和输出的一部分。只看屏幕上最后一个数字,会隐藏程序依赖的初始条件和副作用。
一个可复算的小程序至少说明:接受什么输入、类型与单位;对非法或边界输入做什么;成功返回什么类型和值;是否修改外部状态;是否保证终止。语法正确只表示解释器能读懂代码,不表示契约正确。程序可能正常结束却返回错误单位,也可能对常见输入正确而在空值、零或负数处失败。
表达式、值、名字与基本类型
字面量 3、2.5、True 和 "mass" 分别表示整数、浮点、布尔和文本值。表达式把值和运算组合起来:
kinetic_energy = 0.5 * mass * speed**2
右侧先求值,结果再绑定到名字 kinetic_energy。赋值符号是状态更新,不是数学恒等式;判断相等使用两个等号。执行
count = count + 1
表示读取旧值、加一、再把名字绑定到新值,不能当成代数方程消去 count。
整数适合计数和离散索引;浮点数近似表示实数;布尔值只表达真假;文本是字符序列;空值常表示“没有结果”。不同意义即使底层都能相加,也不应混用。粒子数 10、长度 和字符串 "10" 不是同一个对象。
浮点比较要反映问题尺度。计算结果 x 与期望 y 接近时,可同时使用绝对与相对容差:
abs(x - y) <= absolute_tolerance + relative_tolerance * abs(y)
容差需要与量纲一致;温度绝对容差有开尔文单位,相对容差无量纲。对计数或枚举值则通常要求精确相等。
先写输入输出契约
前置条件说明调用者必须提供什么;后置条件说明函数成功返回时保证什么。非法输入不应悄悄改成看似合理的值,除非契约明确规定修正策略。
目标是把摄氏温度转换为开尔文。物理前置条件是输入为有限数且不低于绝对零点;后置条件是返回有限、非负的开尔文值。
def celsius_to_kelvin(celsius):
if not is_finite_number(celsius):
raise TypeError("celsius must be a finite number")
if celsius < -273.15:
raise ValueError("temperature is below absolute zero")
kelvin = celsius + 273.15
return kelvin
输入 20.0 返回 293.15;边界 -273.15 返回 0.0;输入文本 "20" 失败于类型检查;-300.0 失败于取值范围。函数不打印、不写文件,只返回值或抛出明确异常,因此调用者可决定如何显示错误。
这里的 is_finite_number 是契约辅助函数,排除无穷和非数值。若直接比较非法值,错误可能在不相关运算处出现,定位信息更差。
前置条件不等于“希望用户不要输错”。程序能廉价检查的类型、范围和形状应在边界处检查;无法自动检查的数学假设,例如“函数在区间连续”,应写进文档并用可观测的必要条件辅助诊断。
分支必须覆盖边界
分支按布尔条件选择路径:
if temperature < lower:
...
elif temperature <= upper:
...
else:
...
条件顺序有语义。第二个分支只在第一个为假时判断,因此其真实条件是 。边界属于哪个区间必须明确;重叠条件和遗漏相等号是常见错误。
函数接收实数 ,分类 。若 ,问题退化,不应继续套二次公式。判别式 决定根的类型。
def classify_quadratic(a, b, c):
require_finite_numbers(a, b, c)
if a == 0:
if b == 0:
return "all real numbers" if c == 0 else "no solution"
return "linear"
discriminant = b*b - 4*a*c
if discriminant > 0:
return "two distinct real roots"
if discriminant == 0:
return "one repeated real root"
return "two complex conjugate roots"
当 时返回线性;三个系数全零返回所有实数; 命中重复根边界。若系数来自浮点测量,判别式是否等于零取决于误差模型,应由调用者给容差,而不是在函数内部任意选一个“很小”的常数。
默认返回某类只有在前面条件逻辑上覆盖其余全部输入时安全。若输入还可能是缺失值或非有限浮点数,应先验证,否则比较可能全部为假而误入默认分支。
循环:不变量与终止变元
循环重复一段状态转换。说明循环正确常用两个工具:循环不变量是在每次迭代开始或结束都为真的命题;终止变元取自不能无限下降的集合,并在每一步严格下降。
for 循环适合遍历已知有限范围;while 循环适合由条件决定步数。Python 风格半开范围 range(n) 产生 ,共 个索引。空范围执行零次,因此初始化必须让空输入也得到定义良好的结果。
对非负整数 计算 :
def factorial(n):
if not is_integer(n):
raise TypeError("n must be an integer")
if n < 0:
raise ValueError("n must be nonnegative")
product = 1
k = 1
while k <= n:
product = product * k
k = k + 1
return product
循环开始处不变量是 product 等于 且 。初始化 时 product 等于 ;一次更新后不变量从 推到 。终止时 ,所以返回 。终止变元 在每次进入循环时为非负整数并下降一。边界 时循环零次,正确返回 1。
只说“看起来每次都会更接近”不足以证明终止。浮点迭代可能因舍入停在两个状态间;网络或用户输入循环也可能永久等待。此类程序应设置最大步数、超时或取消机制,并把未收敛作为显式结果。
一个带数值终止条件的循环
二分法在连续函数 的异号区间 内寻找根。核心不变量是 ,终止变元是区间宽度。每步把宽度减半。
def bisect(f, left, right, tolerance, max_steps):
require_finite_numbers(left, right, tolerance)
if left >= right:
raise ValueError("left must be smaller than right")
if tolerance <= 0:
raise ValueError("tolerance must be positive")
if not is_integer(max_steps) or max_steps < 1:
raise ValueError("max_steps must be a positive integer")
f_left = f(left)
f_right = f(right)
require_finite_numbers(f_left, f_right)
if f_left == 0:
return left
if f_right == 0:
return right
if f_left * f_right > 0:
raise ValueError("endpoints do not bracket a sign change")
for _ in range(max_steps):
middle = (left + right) / 2
f_middle = f(middle)
if not is_finite_number(f_middle):
raise ArithmeticError("function returned a non-finite value")
if f_middle == 0 or (right - left) / 2 <= tolerance:
return middle
if f_left * f_middle < 0:
right = middle
else:
left = middle
f_left = f_middle
raise RuntimeError("bisection did not meet tolerance")
对 、区间 ,异号条件成立。宽度经过 步变为 ;若要求半宽不超过 ,代码最多需要约 20 次中点评估。max_steps 提供第二道终止边界。函数连续性无法由有限采样证明,因此仍是文档化前置条件。
函数、参数、返回值与作用域
函数把一段行为命名,并通过参数接收依赖。参数名是局部绑定;函数内部创建的普通局部名在返回后不可见。返回值是调用者继续组合计算的接口,而 print 只是输出副作用。
def kinetic_energy(mass, speed):
require_finite_numbers(mass, speed)
if mass < 0 or speed < 0:
raise ValueError("mass and speed must be nonnegative")
return 0.5 * mass * speed**2
这个函数的结果只由参数决定,是纯函数。它易于对零质量、负速度失败、极大数值和单位换算分别测试。若函数偷偷读取全局 mass 或修改全局容器,同样调用可能得到不同结果,推理和并行执行都会更困难。
函数应单一职责:解析文本、验证数据、计算结果、写出文件可以分为不同函数。这样计算核心保持纯净,输入输出边界集中管理。确需缓存、日志或设备控制时,应在名字和契约中说明副作用,并让调用者注入所需对象,而不是从任意全局位置获取。
默认返回空值通常表示函数没有显式 return。调用者若期待数值,错误可能延迟到下一次运算才出现。所有成功路径应返回同一种语义的结果;失败路径则抛出异常或返回明确结果类型,不能一会返回数值、一会返回错误字符串。
前置条件、后置条件和断言
输入验证面向可能发生的外部失败,应给稳定异常和上下文。断言更适合表达程序内部不变量,例如“前一步已保证索引在范围内”。不能依赖断言承担用户数据验证,因为运行环境可能改变断言行为,而且断言失败通常表示程序员假设被破坏。
后置条件可在测试或调试中核对。例如归一化函数成功返回后应满足长度不变、元素有限、范数接近 1。若输入零向量不允许,前置条件应明确拒绝;若允许,应定义返回零向量还是特殊状态,不能让除零异常偶然决定接口。
纯函数与受控副作用
纯函数不修改外部可见状态,同样输入产生同样输出。时间、随机数、文件和网络使结果依赖额外状态;可把这些依赖作为参数传入,或把副作用封装在程序边界。
一个清晰流程是:读取输入并验证;调用纯计算函数;验证输出契约;在单一位置写出或显示结果。如果写文件失败,计算函数本身不应吞掉异常并假装成功。受控副作用意味着修改位置少、顺序明确、失败可传播和清理可保证,不意味着程序完全不做输入输出。
边界输入与总正确性
对数值函数,应主动考虑零、负数、最小和最大允许值、非有限数、单位不一致和精度边界;对循环,应考虑空范围、一步范围、最后一个索引和最大迭代;对分支,应考虑每条相等边界;对函数,应考虑每个失败路径是否给出一致错误。
终止只是正确性的一半。部分正确性说明“若程序结束,则结果满足后置条件”;终止性说明“在前置条件下程序一定结束”。两者合起来才是总正确性。真实程序还需资源和时间上可接受,但性能优化不能先于语义正确和边界定义。
常见误区
“变量就是装着数值的数学字母。”程序变量名可在时间中重新绑定,赋值是状态变化,不是恒等式。
“循环在样例上停了就证明会终止。”需要对所有满足前置条件的输入给出下降变元、有限范围或明确上限。
“函数打印了答案就等于返回答案。”打印产生输出副作用,返回值才可被调用者组合、验证和测试。
练习:契约、控制流与函数
说明整数 3、浮点 3.0、文本 "3"、相等比较和赋值的类型或语义差别。
查看提示
查看解答
设计互斥分支,把有限实数分成负数、零、开区间 和不小于 1 四类。
查看提示
查看解答
为遍历长度为 的序列求和写出循环不变量,并解释空序列结果。
查看提示
查看解答
说明二分法为何需要宽度容差和最大步数两种终止条件。
查看提示
查看解答
max_steps 仍保证终止并抛出未收敛错误。为只处理实数的平方根函数写前置条件、后置条件和失败行为。
查看提示
查看解答
把“读取温度文件、换算、写出结果”拆为纯计算与受控副作用,并说明错误传播。
查看提示
查看解答
知识连接与资源
- 函数与图像 提供输入输出、复合和定义域语言。
- 命题、量词与证明语言 表达分支条件、契约和循环不变量。
- 集合数据、文件与错误处理 将把控制流用于容器遍历、输入验证和资源清理。
- 测试、版本控制与环境管理 使用边界契约设计测试并保留变更记录。
- 编程与科学计算综合项目 综合函数分解、数据流和可复算交付。
Introduction to Computer Science and Programming in Python
Ana Bell, Eric Grimson, John Guttag
用于核对 C00 的基础程序语义、函数分解、集合与文件处理、异常、测试和可执行例题。
打开官方来源MIT OpenCourseWare 6.0001 覆盖基本类型、控制流、函数、异常与程序设计,可用于核对本章 Python 风格语义。示例强调可迁移的基础结构,不依赖特定解释器版本的冷门语法。