《太空探索任务控制台》Python 教程
项目主题:地面控制中心给太空任务写一套"小工具集"。 目标:让讲师只看这一份文档就能独立写出全部 15 章的代码并完成讲解。
0. 课程理念:零跳跃学习
| 知识点 A → 知识点 C(中途发现需要 B,回头补) | A → B(A 的自然延伸)→ C(B 的必然发展)→ D(C 的合理进阶) |
| 突然冒出 lambda / *args / dataclass 等高级语法 | 每一节只引入当前必须的语法,禁止"伏笔" |
设计准则:
1. 项目初始化
mkdir python_code && cd python_code
uv init –python 3.14 –name python_code –no-readme
mkdir chapters docs
环境确认:
uv python list # 应能看到 cpython-3.14
uv run python -V # Python 3.14.x
2. 章节速览(15 章 · 由浅入深)
| 01 | chapter01_greetings.py | 起飞前的对话 | print + 字符串 + f-string + input |
| 02 | chapter02_variables.py | 任务变量 | 变量、动态类型、类型转换、命名规范 |
| 03 | chapter03_orbit_math.py | 计算轨道参数 | 算术运算符 + math 模块 + 物理公式 |
| 04 | chapter04_decisions.py | 发射窗口判断 | 布尔/比较/逻辑 + if-elif-else + match-case |
| 05 | chapter05_loops.py | 循环发令 | while + for + range + break/continue + enumerate/zip |
| 06 | chapter06_collections.py | 数据打包箱 | list + tuple + 解包 + 嵌套 + 排序 |
| 07 | chapter07_dictionaries.py | 字典通讯录 | dict + 嵌套 + 推导式 + Counter / defaultdict |
| 08 | chapter08_functions.py | 可重用的任务模块 | def + 参数 + return + 默认值 + *args/**kwargs + lambda |
| 09 | chapter09_errors.py | 错误处理 | 异常 + try-except-else-finally + raise + 自定义异常 + warnings |
| 10 | chapter10_files.py | 文件与日志 | pathlib + with + JSON + logging |
| 11 | chapter11_oop.py | 面向对象:航天器类 | class + __init__ + 继承 + @dataclass + Enum |
| 12 | chapter12_iterators.py | 迭代器与生成器 | 迭代协议 + yield + 生成器表达式 + itertools |
| 13 | chapter13_typing.py | 类型提示与契约 | 类型提示 + typing + Protocol + TypeGuard |
| 14 | chapter14_concurrency.py | 并发任务 | asyncio + async/await + gather + TaskGroup |
| 15 | chapter15_project.py | 综合实战项目 | dataclass + asyncio + argparse + CLI 完整流程 |
每章实战都紧扣太空探索主题,不出现学生/学校相关示例。
3. 备课方法论
- 5 分钟讲解本章解决的问题
- 15 分钟按代码段逐步讲解(边讲边让学生跟着敲)
- 20 分钟学生独立完成"小练习"
- 10 分钟答疑 + 总结 + 预告下一章
4. 全部章节代码(与仓库 chapters/*.py 一一对应)
以下每章都给出完整代码,可直接复制运行。 注释中 ~ 行表示可选讲解;课堂可酌情跳过。
目录
- 第 01 章 · 起飞前的对话
- 第 01 章 · 起飞前的对话
- 第 02 章 · 任务变量
- 第 03 章 · 计算轨道参数
- 第 04 章 · 发射窗口判断
- 第 05 章 · 循环发令
- 第 06 章 · 数据打包箱
- 第 07 章 · 字典通讯录
- 第 08 章 · 可重用的任务模块
- 第 09 章 · 错误处理
- 第 10 章 · 文件与日志
- 第 11 章 · 面向对象:航天器类
- 第 12 章 · 迭代器与生成器
- 第 13 章 · 类型提示与契约
- 第 14 章 · 并发任务
- 第 15 章 · 综合实战项目
第 01 章–起飞前的对话
知识点:print / 字符串 / f-string / input 实战主题:起飞前的对话
"""第 01 章:起飞前的对话。
本章学习目标(零跳跃路径):
print() 输出 → 字符串字面量 → f-string 格式化 → input() 交互
所有示例围绕"任务控制台欢迎语"展开,避免出现尚未学过的概念。
"""
# —- 演示用的容错输入:交互环境等待用户输入;非交互环境直接用默认值 —-
import sys as _sys
def prompt(question, default):
"""演示用的容错输入:拿到 EOF 时返回 default,便于无人值守运行。"""
try:
return input(question)
except EOFError:
print(question + f"[default: {default}]")
return default
# ———————————————————————-
# 1.1 最简单的输出:print 是 Python 内置的"喊话器"
# ———————————————————————-
print("== Mission Control Online ==") # print() 把括号里的内容原样显示在屏幕上
print("Welcome, Commander.") # 每调用一次 print 默认换行
# ———————————————————————-
# 1.2 字符串字面量:单引号 / 双引号 / 三引号
# ———————————————————————-
single_quoted = 'T-minus 10 seconds' # 单引号字符串:适合不含单引号的内容
double_quoted = "Rocket status: GO" # 双引号字符串:和单引号等价,按习惯选用
triple_quoted = """All systems nominal.
Awaiting final clearance.""" # 三引号可以跨行,常用于多行文本
print(single_quoted) # 把变量里的字符串显示出来
print(double_quoted)
print(triple_quoted)
# ———————————————————————-
# 1.3 字符串拼接:把几段文字拼成一句
# ———————————————————————-
rocket_name = "Artemis" # 火箭代号(变量先记住,第 02 章会深入)
mission_tag = "VII" # 任务编号
greeting = "Hello, " + rocket_name + "-" + mission_tag + "!" # 用 + 把字符串粘起来
print(greeting) # 结果:Hello, Artemis-VII!
# ———————————————————————-
# 1.4 f-string:Python 3.6+ 推荐的现代写法(PEP 498)
# 在字符串前加 f,把变量名放进 {} 里,运行时会自动替换成变量的值。
# ———————————————————————-
crew_count = 4 # 机组人数
duration_days = 7 # 任务天数
status_line = f"Rocket: {rocket_name}–{mission_tag} | Crew: {crew_count} | Duration: {duration_days} days"
print(status_line) # 一行里同时展示多个变量的值
# ———————————————————————-
# 1.5 f-string 中的表达式:{} 里不仅能放变量,还能放计算式
# ———————————————————————-
print(f"Mission ends in {duration_days * 24} hours.") # 表达式会在运行时求值
print(f"Crew per day of mission: {crew_count / duration_days:.2f}") # :.2f 表示保留 2 位小数
# ———————————————————————-
# 1.6 input():让程序等用户在终端输入一行文字
# ———————————————————————-
print("\\n– Identity Check –") # \\n 是换行符,让输出更整齐
callsign = prompt("Enter your callsign: ", default="Aurora") # 程序暂停,光标等待用户输入
print(f"Access granted. Welcome aboard, {callsign}.") # 把用户输入嵌入欢迎语
# ———————————————————————-
# 1.7 小练习(去掉下面的三引号注释即可运行)
# ———————————————————————-
"""
name = prompt("Astronaut name: ", default="Lyra")
favorite_planet = prompt("Favorite planet: ", default="Mars")
print(f"Commander {name} is heading to {favorite_planet}. Godspeed!")
"""
# —- 演示用的容错输入:交互环境等待用户输入;非交互环境直接用默认值 —-
def prompt(question, default):
"""演示用的容错输入:拿到 EOF 时返回 default,便于无人值守运行。"""
try:
return input(question)
except EOFError:
print(question + f"[default: {default}]")
return default
课堂建议:把代码贴到 chapters/chapter01_greetings.py,运行验证后再讲解每段作用。
第 02 章–任务变量
知识点:变量 / 类型 / 类型转换 / 命名规范 实战主题:任务变量
"""第 02 章:任务变量。
学习路径(零跳跃):
变量赋值 → 动态类型 → type() 检查 → 类型转换 → 命名规范
围绕"火箭代号与载荷清单"展开:先建立"变量就是带名字的盒子"的心智模型。
"""
# ———————————————————————-
# 2.1 变量赋值:把值贴上名字,方便后面反复使用
# ———————————————————————-
rocket_name = "Artemis-VII" # 字符串:火箭任务代号
crew_size = 4 # 整数 int:机组成员数量
fuel_mass_tons = 2670.5 # 浮点数 float:燃料质量(吨)
is_go_for_launch = True # 布尔 bool:是否可以发射
print(rocket_name, crew_size, fuel_mass_tons, is_go_for_launch)
# ———————————————————————-
# 2.2 动态类型:Python 不需要在声明时指定类型,运行时会自动推断
# ———————————————————————-
sample = 42 # 现在 sample 是 int
print(type(sample)) # <class 'int'>:type() 用来查看值的类型
sample = "forty-two" # 又可以变成 str:同一名字可以贴不同类型的值
print(type(sample)) # <class 'str'>
# ———————————————————————-
# 2.3 常见内置类型一览(本章先把名字认熟)
# ———————————————————————-
print(type(123)) # int:整数
print(type(3.14)) # float:浮点数
print(type(True)) # bool:布尔值(注意首字母大写)
print(type("hello")) # str:字符串
# 第 06 章会学 list,第 07 章会学 dict,本章先不出现
# ———————————————————————-
# 2.4 类型转换:在不同类型之间显式转换
# ———————————————————————-
crew_count_text = "4" # 来自配置文件或用户输入,通常是字符串
crew_count_number = int(crew_count_text) # int() 把字符串转为整数
print(crew_count_number + 1) # 5:可以参与数值运算了
fuel_tons = float("2670.5") # float() 把字符串转为浮点数
print(fuel_tons * 2) # 5341.0
score = 98
print("Score: " + str(score)) # str() 把数值转回字符串以便拼接
# ———————————————————————-
# 2.5 命名规范:让代码一眼就能读懂
# ———————————————————————-
# 合法但糟糕的命名(避免在真实项目中使用):
# x = 4 # 含义不明
# FuelMass = 2670.5 # 混合大小写风格不统一
# 推荐遵循 PEP 8 风格:
MAX_PAYLOAD_KG = 6000 # 全大写下划线:模块级常量
crew_callsign = "Aurora" # 小写+下划线(snake_case):变量与函数名
PrimaryMission = "Lunar Orbit" # 帕斯卡命名(PascalCase):留给第 11 章的"类"使用
print(MAX_PAYLOAD_KG, crew_callsign, PrimaryMission)
# ———————————————————————-
# 2.6 input() 返回的永远是字符串,需要时立刻转换
# ———————————————————————-
# payload_text = input("Payload mass (kg): ") # 取消注释即可交互
# payload_kg = float(payload_text) # 转为浮点数
# print(f"Within limit: {payload_kg <= MAX_PAYLOAD_KG}") # 比较运算符第 04 章细讲
# ———————————————————————-
# 2.7 多重赋值与解包(Python 语法糖,先体验,第 06 章会正式讲解包)
# ———————————————————————-
x, y, z = 10, 20, 30 # 一次给三个变量赋值
print(x, y, z)
a = b = c = 0 # 链式赋值:三个变量都指向同一个值 0
print(a, b, c)
# ———————————————————————-
# 2.8 实战小脚本:载荷清单(融合本章全部知识点)
# ———————————————————————-
satellite_mass = 4800 # 卫星质量 kg
propellant_mass = 1200 # 推进剂质量 kg
total_mass = satellite_mass + propellant_mass # 总质量 = 两者相加
print(f"Payload: satellite={satellite_mass}kg, propellant={propellant_mass}kg, total={total_mass}kg")
print("Type of total_mass:", type(total_mass).__name__) # 用 __name__ 只取类型名
课堂建议:把代码贴到 chapters/chapter02_variables.py,运行验证后再讲解每段作用。
第 03 章–计算轨道参数
知识点:算术 / math / 物理常量 / 格式化 实战主题:计算轨道参数
"""第 03 章:计算轨道参数。
学习路径(零跳跃):
算术运算符 → math 模块 → 物理常量 → 格式化输出 → f-string 进阶
实战:圆形轨道速度计算器。
圆形轨道速度公式 v = sqrt(GM / r)
"""
# —- 演示用的容错输入:交互环境等待用户输入;非交互环境直接用默认值 —-
import sys as _sys
def prompt(question, default):
"""演示用的容错输入:拿到 EOF 时返回 default,便于无人值守运行。"""
try:
return input(question)
except EOFError:
print(question + f"[default: {default}]")
return default
# ———————————————————————-
# 3.1 基础算术运算符
# ———————————————————————-
print(2 + 3) # 加
print(10 – 4) # 减
print(6 * 7) # 乘
print(20 / 6) # 除:结果总是 float,3.333…
print(20 // 6) # 整除:丢掉小数部分,3
print(20 % 6) # 取余:除不尽的部分,2
print(2 ** 10) # 幂:2 的 10 次方 = 1024
# ———————————————————————-
# 3.2 复合赋值运算符
# ———————————————————————-
countdown = 10
countdown -= 1 # 等价于 countdown = countdown – 1
print(countdown) # 9
countdown *= 2 # 18
print(countdown)
# ———————————————————————-
# 3.3 引入 math 模块:Python 自带的数学工具箱
# ———————————————————————-
# —- 演示用的容错输入:交互环境等待用户输入;非交互环境直接用默认值 —-
def prompt(question, default):
"""演示用的容错输入:拿到 EOF 时返回 default,便于无人值守运行。"""
try:
return input(question)
except EOFError:
print(question + f"[default: {default}]")
return default
import math # import 是"导入",把 math 这个工具箱拿来用
print(math.sqrt(144)) # sqrt = square root,开平方 → 12.0
print(math.pi) # 圆周率 π
print(math.sin(math.pi / 2)) # sin(π/2) = 1.0
# ———————————————————————-
# 3.4 圆形轨道速度计算
# 公式:v = sqrt(GM / r)
# GM 是地球引力参数(标准值) ≈ 3.986e14 m^3/s^2
# r = 地球半径 + 轨道高度
# ———————————————————————-
EARTH_RADIUS_M = 6.371e6 # 地球半径,单位米
GM_EARTH = 3.986e14 # 地球引力参数 m^3/s^2
altitude_km = float(prompt("Orbit altitude (km): ", default="540")) # 用户输入轨道高度
altitude_m = altitude_km * 1000 # km 转 m
radius_m = EARTH_RADIUS_M + altitude_m # 圆轨道半径
orbital_speed = math.sqrt(GM_EARTH / radius_m) # 圆形轨道速度 m/s
period_seconds = 2 * math.pi * radius_m / orbital_speed # 轨道周期 T = 2πr / v
# ———————————————————————-
# 3.5 f-string 格式化输出:控制小数位与对齐
# ———————————————————————-
# :>12 表示右对齐占 12 列;:.2f 表示保留 2 位小数;:.1f 表示 1 位小数
print(f"Orbit radius : {radius_m:>15,.0f} m")
print(f"Orbit speed : {orbital_speed:>15.2f} m/s")
print(f"Orbit period : {period_seconds:>15.2f} s ({period_seconds / 60:.2f} min)")
# ———————————————————————-
# 3.6 综合:地面控制快速评估
# ———————————————————————-
escape_speed = math.sqrt(2 * GM_EARTH / radius_m) # 第二宇宙速度(脱离速度)
print(f"Local escape speed : {escape_speed:.2f} m/s")
ratio = orbital_speed / escape_speed # 当前轨道速度占逃逸速度的比例
print(f"Speed / escape : {ratio:.3f} (< 1/sqrt(2) ≈ 0.707 才能维持圆轨道)")
print(f"Circular orbit OK : {ratio < 1 / math.sqrt(2)}")
课堂建议:把代码贴到 chapters/chapter03_orbit_math.py,运行验证后再讲解每段作用。
第 04 章–发射窗口判断
知识点:布尔 / 比较 / 逻辑 / if-elif-else / match-case 实战主题:发射窗口判断
"""第 04 章:发射窗口判断。
学习路径(零跳跃):
布尔值 → 比较运算符 → 逻辑运算符 (and/or/not) → if / elif / else → match-case
实战:根据天气、风速、能见度判断是否可以发射。
"""
# —- 演示用的容错输入:交互环境等待用户输入;非交互环境直接用默认值 —-
import sys as _sys
def prompt(question, default):
"""演示用的容错输入:拿到 EOF 时返回 default,便于无人值守运行。"""
try:
return input(question)
except EOFError:
print(question + f"[default: {default}]")
return default
# ———————————————————————-
# 4.1 布尔值:只有 True 和 False 两个取值(注意首字母大写)
# ———————————————————————-
launch_authorised = True
print(launch_authorised) # True
print(int(launch_authorised)) # 1:True 可以当整数 1 用
print(int(False)) # 0
# ———————————————————————-
# 4.2 比较运算符:得到一个布尔值
# ———————————————————————-
print(10 > 5) # True
print(10 == 10) # True,注意是两个等号"等于"
print(10 != 9) # True,不等于
print(7 <= 7) # True
print("a" < "b") # True,字符串按字典序比较
# ———————————————————————-
# 4.3 逻辑运算符:组合多个布尔条件
# ———————————————————————-
wind_speed_kmh = 18 # 风速
visibility_km = 12 # 能见度
cloud_cover_pct = 15 # 云量百分比
is_daylight = True # 是否白天
# 全部条件都满足才能发射
go_launch = (
wind_speed_kmh < 30 # 风速上限
and visibility_km >= 8 # 最低能见度
and cloud_cover_pct < 50 # 云量上限
and is_daylight # 必须是白天
)
print(f"All launch criteria met: {go_launch}")
# ———————————————————————-
# 4.4 if / elif / else:让程序在不同条件下走不同分支
# ———————————————————————-
if go_launch:
print(">>> LAUNCH AUTHORISED <<<")
elif wind_speed_kmh >= 30:
print("Hold: wind speed too high.")
elif visibility_km < 8:
print("Hold: visibility below minimum.")
elif cloud_cover_pct >= 50:
print("Hold: cloud cover too thick.")
else:
print("Hold: awaiting daylight.")
# ———————————————————————-
# 4.5 嵌套条件与"早返回"写法(让代码更扁平)
# ———————————————————————-
thunderstorm_alert = False
range_safety_clear = True
# 嵌套写法:可读但缩进会变深
if range_safety_clear:
if not thunderstorm_alert:
print("Range safety: clear and no thunderstorm.")
else:
print("Hold: thunderstorm alert active.")
else:
print("Hold: range safety not confirmed.")
# ———————————————————————-
# 4.6 match-case(Python 3.10+ 引入,3.14 推荐使用)
# 适合对同一个变量做"枚举式"判断
# ———————————————————————-
mission_phase = prompt("Current phase (prelaunch/ascent/orbit/landing): ", default="orbit").strip().lower()
match mission_phase:
case "prelaunch":
print("Final checks. Countdown active.")
case "ascent":
print("Vehicle climbing. Monitor Max-Q.")
case "orbit":
print("Coast phase. Payload operations allowed.")
case "landing" | "recovery": # 用 | 匹配多个值
print("Descent burn start.")
case _: # 默认分支,类似 switch 的 default
print("Unknown phase. Standby.")
# ———————————————————————-
# 4.7 真值测试(Truthiness):除了 False,以下值也视为"假"
# 0, 0.0, "", [], {}, None
# 其余对象都被视为"真"
# ———————————————————————-
battery_level = 0
if not battery_level:
print("Battery empty!")
checklist = ["oxygen", "hud", "comms"]
if checklist:
print(f"Checklist has {len(checklist)} items.") # len() 第 06 章讲
else:
print("Checklist empty.")
# —- 演示用的容错输入:交互环境等待用户输入;非交互环境直接用默认值 —-
def prompt(question, default):
"""演示用的容错输入:拿到 EOF 时返回 default,便于无人值守运行。"""
try:
return input(question)
except EOFError:
print(question + f"[default: {default}]")
return default
课堂建议:把代码贴到 chapters/chapter04_decisions.py,运行验证后再讲解每段作用。
第 05 章–循环发令
知识点:while / for / range / break-continue / enumerate / zip 实战主题:循环发令
"""第 05 章:循环发令。
学习路径(零跳跃):
while 循环 → for 循环 → range() → break / continue → enumerate → zip
实战:发射倒计时与多星状态轮询。
"""
# ———————————————————————-
# 5.1 while 循环:当条件满足时重复执行
# ———————————————————————-
countdown = 10
while countdown > 0: # 只要 countdown > 0 就继续
print(f"T-{countdown:02d} seconds") # :02d 表示至少占 2 位,不足补 0
countdown -= 1 # 自减;别忘了,否则会死循环
print("Liftoff!")
# ———————————————————————-
# 5.2 for 循环:遍历"序列"中的每一项
# ———————————————————————-
crew = ["Aurora", "Orion", "Vega", "Lyra"]
for callsign in crew: # 把列表里的元素逐个取出
print(f"Crew on board: {callsign}")
# ———————————————————————-
# 5.3 range():生成整数序列,常和 for 配合
# ———————————————————————-
for n in range(5): # 0,1,2,3,4
print("Stage check", n)
for n in range(2, 10, 2): # 从 2 开始,<10,步长 2 → 2,4,6,8
print("Even tick:", n)
for n in range(5, 0, –1): # 倒着数:5,4,3,2,1
print("Reverse:", n)
# ———————————————————————-
# 5.4 break 与 continue
# ———————————————————————-
# break:立即跳出整个循环
# continue:跳过本轮剩余代码,进入下一轮
print("\\n– Anomaly scan –")
readings = [98.6, 99.1, 105.3, 99.0, 102.7]
for r in readings:
if r >= 105.0:
print(f"Critical reading {r}! Aborting scan.")
break # 发现严重异常就停止
if r < 100.0:
continue # 正常读数不报告
print(f"Elevated reading: {r}")
# ———————————————————————-
# 5.5 enumerate():同时拿到"索引 + 值"
# ———————————————————————-
print("\\n– Stage ignition order –")
stages = ["Booster", "Core", "Upper"]
for index, stage in enumerate(stages, start=1): # start=1 让编号从 1 开始
print(f"Stage {index}: {stage} ignite")
# ———————————————————————-
# 5.6 zip():把多个序列并行遍历
# ———————————————————————-
print("\\n– Telemetry pairing –")
sensors = ["temp_c", "pressure_kpa", "voltage"]
values = [22.4, 101.3, 28.1]
for name, val in zip(sensors, values):
print(f"{name} = {val}")
# ———————————————————————-
# 5.7 循环里的 else 子句:循环"正常结束"才会执行
# (用 break 中断则不会执行 else)
# ———————————————————————-
print("\\n– Retry loop –")
attempts = 0
while attempts < 3:
attempts += 1
print(f"Attempt {attempts}")
if attempts == 2:
break # 中断
else:
print("All attempts succeeded.") # 不会执行,因为循环被 break 了
# ———————————————————————-
# 5.8 实战:实时模拟遥测流(不会真正阻塞,只是演示)
# ———————————————————————-
import time # time.sleep 用来暂停,第 12 章再深入
import random # random 用来生成随机数,本章只用一次
print("\\n– Live telemetry (press Ctrl+C to stop) –")
for tick in range(3):
altitude = 100 + tick * 50 + random.uniform(–5, 5)
velocity = 50 + tick * 10
print(f"t={tick}s alt={altitude:6.1f}km v={velocity:5.1f}m/s")
time.sleep(0.1) # 暂停 0.1 秒模拟间隔
课堂建议:把代码贴到 chapters/chapter05_loops.py,运行验证后再讲解每段作用。
第 06 章–数据打包箱
知识点:list / tuple / 解包 / 嵌套 / 排序 实战主题:数据打包箱
"""第 06 章:数据打包箱。
学习路径(零跳跃):
列表 list → 索引与切片 → 增删改查 → 元组 tuple → 解包 → 嵌套 → 排序
实战:管理一个卫星编队的轨道集合。
"""
# ———————————————————————-
# 6.1 列表 list:可以装任意类型的有序容器,用 [] 定义
# ———————————————————————-
orbits = [400, 550, 600, 800] # 4 颗卫星的轨道高度 (km)
print(orbits) # [400, 550, 600, 800]
print(len(orbits)) # 4,len() 返回元素个数
mixed = ["Artemis", 4, 95.5, True] # 列表可以混装不同类型(不推荐但合法)
print(mixed)
# ———————————————————————-
# 6.2 索引与切片
# ———————————————————————-
print(orbits[0]) # 第一个元素,索引从 0 开始
print(orbits[–1]) # 最后一个元素
print(orbits[1:3]) # 切片:索引 1 到 2(不含 3)→ [550, 600]
print(orbits[:2]) # 从头到索引 1 → [400, 550]
print(orbits[2:]) # 从索引 2 到末尾 → [600, 800]
print(orbits[::–1]) # 反转整个列表
# ———————————————————————-
# 6.3 修改、添加、删除
# ———————————————————————-
orbits[0] = 410 # 修改指定索引的元素
print(orbits)
orbits.append(1200) # 末尾追加一个
orbits.insert(1, 450) # 在索引 1 处插入
print(orbits)
last = orbits.pop() # 弹出最后一个元素,并返回它
print("Removed:", last, "Remaining:", orbits)
orbits.remove(550) # 按值删除(只删第一个匹配的)
print(orbits)
del orbits[0] # 按索引删除
print(orbits)
# ———————————————————————-
# 6.4 列表的常用方法
# ———————————————————————-
samples = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
print("count of 5:", samples.count(5)) # 统计某个值出现次数
print("index of 9:", samples.index(9)) # 找到值的第一个索引
samples.sort() # 原地升序排序
print("sorted:", samples)
samples.reverse() # 原地反转
print("reversed:", samples)
# 想保留原列表?用 sorted() 函数返回新列表
original = [5, 2, 9, 1]
new_sorted = sorted(original)
print("original:", original, "new:", new_sorted)
# ———————————————————————-
# 6.5 元组 tuple:用 () 定义,**不可变**
# ———————————————————————-
mission_window = ("2026-08-12", "2026-08-19") # 发射窗口起止
print(mission_window[0]) # 仍可索引
# mission_window[0] = "2026-09-01" # 这行会报错,元组不可改
lat, lon = (39.0, –120.0) # 元组解包:左右数量要一致
print(f"Launch site: lat={lat}, lon={lon}")
# 用 * 收集多余元素
first, *middle, last = [10, 20, 30, 40, 50]
print(first, middle, last)
# ———————————————————————-
# 6.6 嵌套:列表套列表 / 元组
# ———————————————————————-
satellites = [
("Hubble-Echo", 540, "imaging"),
("CommSat-1", 800, "comms"),
("Radar-Eye", 700, "sar"),
]
for name, alt, purpose in satellites: # 自动按元组解包
print(f"{name:<12} alt={alt:>4}km purpose={purpose}")
# ———————————————————————-
# 6.7 实战:把卫星编队按高度排序并打印表格
# ———————————————————————-
satellites.append(("Weather-9", 360, "weather"))
satellites.append(("GPS-III", 20200, "nav"))
satellites_sorted = sorted(satellites, key=lambda s: s[1]) # 按高度排序
print("\\nFleet roster (sorted by altitude):")
for name, alt, purpose in satellites_sorted:
print(f"{name:<12} {alt:>6} km {purpose}")
课堂建议:把代码贴到 chapters/chapter06_collections.py,运行验证后再讲解每段作用。
第 07 章–字典通讯录
知识点:dict / 嵌套 / 字典推导式 / Counter / defaultdict 实战主题:字典通讯录
"""第 07 章:字典通讯录。
学习路径(零跳跃):
字典 dict → 键值对操作 → 嵌套 → 字典推导式 → collections.Counter / defaultdict
实战:每颗卫星维护一份遥测字典。
"""
# ———————————————————————-
# 7.1 字典 dict:键值对 (key: value) 的映射,用 {} 定义
# ———————————————————————-
satellite = {
"name": "Hubble-Echo",
"altitude_km": 540,
"inclination_deg": 28.5,
"operational": True,
}
print(satellite)
# ———————————————————————-
# 7.2 访问与修改
# ———————————————————————-
print(satellite["name"]) # 通过键取值,键不存在会 KeyError
print(satellite.get("mass_kg", "unknown")) # get 不存在时返回默认值,更安全
satellite["altitude_km"] = 545 # 修改值
satellite["launch_date"] = "2024-03-14" # 新增键值对
print(satellite)
# ———————————————————————-
# 7.3 遍历字典
# ———————————————————————-
for key in satellite: # 默认遍历键
print(f"key: {key}")
for key, value in satellite.items(): # 同时拿键和值
print(f"{key:<14} = {value}")
for value in satellite.values(): # 只遍历值
print("value:", value)
# ———————————————————————-
# 7.4 嵌套字典:一颗卫星里再装子系统的遥测
# ———————————————————————-
telemetry = {
"sat_id": "SAT-001",
"position": {"lat": 12.34, "lon": –45.67, "alt_km": 540.0},
"systems": {
"power": {"voltage": 28.1, "current": 12.4},
"thermal": {"temp_c": –15.2, "heaters_on": True},
},
}
print(telemetry["systems"]["power"]["voltage"]) # 28.1
telemetry["systems"]["power"]["voltage"] = 28.3
print(telemetry["systems"]["power"]["voltage"])
# ———————————————————————-
# 7.5 字典推导式:快速生成字典
# ———————————————————————-
altitudes = {"Hubble-Echo": 540, "CommSat-1": 800, "Radar-Eye": 700}
altitude_m = {name: alt * 1000 for name, alt in altitudes.items()}
print(altitude_m) # 单位从 km 转为 m
# 加条件过滤
high_orbit = {k: v for k, v in altitudes.items() if v >= 700}
print(high_orbit)
# ———————————————————————-
# 7.6 collections 模块:defaultdict 与 Counter
# ———————————————————————-
from collections import defaultdict, Counter
# defaultdict:访问不存在的键时,自动生成默认值
events_by_day = defaultdict(list) # 默认值是空列表
events_by_day["2026-08-12"].append("launch")
events_by_day["2026-08-12"].append("orbit-insert")
events_by_day["2026-08-13"].append("payload-deploy")
print(dict(events_by_day))
# Counter:快速统计每个值出现多少次
events = ["launch", "orbit-insert", "comms-check", "launch", "comms-check", "launch"]
counts = Counter(events)
print(counts) # Counter({'launch': 3, 'comms-check': 2, 'orbit-insert': 1})
print(counts.most_common(2)) # 出现次数最多的前 2 项
# ———————————————————————-
# 7.7 实战:建立一份完整的卫星遥测档案
# ———————————————————————-
fleet = {
"SAT-001": {"alt_km": 540, "voltage": 28.1, "temp_c": –15.0},
"SAT-002": {"alt_km": 800, "voltage": 27.9, "temp_c": –22.4},
"SAT-003": {"alt_km": 360, "voltage": 28.4, "temp_c": –10.1},
}
# 找出电压最低的那颗
lowest_sat = min(fleet, key=lambda sid: fleet[sid]["voltage"])
print(f"\\nLowest voltage: {lowest_sat} -> {fleet[lowest_sat]['voltage']}V")
# 计算平均温度
avg_temp = sum(s["temp_c"] for s in fleet.values()) / len(fleet)
print(f"Average temperature: {avg_temp:.2f} °C")
课堂建议:把代码贴到 chapters/chapter07_dictionaries.py,运行验证后再讲解每段作用。
第 08 章–可重用的任务模块
知识点:def / 参数 / return / 默认值 / *args / lambda 实战主题:可重用的任务模块
"""第 08 章:可重用的任务模块(函数)。
学习路径(零跳跃):
def 定义 → 位置参数 / 关键字参数 → 默认值 → return → *args / **kwargs
→ 作用域 → 文档字符串 → lambda
实战:通用推进力与 Δv(速度增量)计算函数。
"""
# ———————————————————————-
# 8.1 最简单的函数
# ———————————————————————-
def greet(): # def 关键字 + 函数名 + () + :
print("Mission Control: all systems go.") # 函数体要缩进
greet() # 调用
# ———————————————————————-
# 8.2 参数与返回值
# ———————————————————————-
def orbital_period(radius_m: float, gm: float) –> float: # 类型提示(hint),运行时不强制
"""根据半径 r 与引力参数 gm 计算轨道周期。"""
import math
speed = math.sqrt(gm / radius_m)
return 2 * math.pi * radius_m / speed
period = orbital_period(radius_m=7.0e6, gm=3.986e14)
print(f"Period ≈ {period:.1f} s ({period/60:.2f} min)")
# ———————————————————————-
# 8.3 默认参数与关键字参数
# ———————————————————————-
def report(phase: str, *, verbose: bool = False, prefix: str = "STATUS") –> None:
"""用 * 强制后续参数必须以关键字形式传。"""
msg = f"[{prefix}] Phase = {phase}"
if verbose:
msg += " (verbose mode)"
print(msg)
report("orbit") # 用默认 verbose=False
report("ascent", verbose=True) # 关键字参数
# report("ascent", True) # 报错:verbose 必须是关键字
# ———————————————————————-
# 8.4 可变参数 *args / **kwargs
# ———————————————————————-
def total_payload(*masses_kg: float) –> float:
"""把任意多个载荷质量相加。"""
return sum(masses_kg)
print(total_payload(120, 250, 75)) # 445.0
print(total_payload(500)) # 500.0
def build_satellite(name: str, **attrs: object) –> dict:
"""用关键字参数自由扩展属性。"""
sat = {"name": name}
sat.update(attrs)
return sat
print(build_satellite("CommSat-1", mass_kg=480, alt_km=800, purpose="comms"))
# ———————————————————————-
# 8.5 返回多个值:实际是返回一个元组
# ———————————————————————-
def burn_budget(initial: float, exhaust_v: float, mass_flow: float, burn_time: float):
"""齐奥尔科夫斯基火箭方程:Δv = v_e * ln(m0 / m1)。"""
import math
final_mass = initial – mass_flow * burn_time
delta_v = exhaust_v * math.log(initial / final_mass)
return delta_v, final_mass
dv, mf = burn_budget(initial=500_000, exhaust_v=320, mass_flow=1200, burn_time=120)
print(f"Δv = {dv:.1f} m/s, final mass = {mf:.0f} kg")
# ———————————————————————-
# 8.6 作用域:局部 vs 全局
# ———————————————————————-
GROUND_LEVEL_PRESSURE = 101.3 # 全局常量
def cabin_pressure_sim(altitude_km: float) –> float:
pressure_at_alt = GROUND_LEVEL_PRESSURE * (1 – 2.2558e-5 * altitude_km) ** 5.2559
return pressure_at_alt # 局部变量用完即丢
print(cabin_pressure_sim(5))
# ———————————————————————-
# 8.7 文档字符串与 help
# ———————————————————————-
def escape_velocity(mass_kg: float, radius_m: float) –> float:
"""计算逃逸速度 v = sqrt(2GM/r)。
Args:
mass_kg: 天体质量(千克)。
radius_m: 天体半径(米)。
Returns:
逃逸速度(米/秒)。
"""
G = 6.67430e-11
return (2 * G * mass_kg / radius_m) ** 0.5
print(help(escape_velocity)) # help() 会显示文档字符串
# ———————————————————————-
# 8.8 lambda:单行匿名函数,常用于简短转换
# ———————————————————————-
altitudes_km = [400, 550, 800, 1200]
to_meters = lambda km: km * 1000
print(list(map(to_meters, altitudes_km))) # [400000, 550000, …]
# sorted 配合 key 使用最常见
altitudes_sorted = sorted(altitudes_km, key=lambda x: –x)
print(altitudes_sorted)
课堂建议:把代码贴到 chapters/chapter08_functions.py,运行验证后再讲解每段作用。
第 09 章–错误处理
知识点:异常 / try-except / raise / 自定义异常 / warnings 实战主题:错误处理
"""第 09 章:错误处理。
学习路径(零跳跃):
异常类型认知 → try / except / else / finally → 主动抛出 raise
→ 自定义异常 → except 链 → 警告 warnings
实战:遥测数据校验模块。
"""
# ———————————————————————-
# 9.1 常见内置异常
# ———————————————————————-
# 这些先"认识名字",看到能猜到大致问题即可:
# ZeroDivisionError 除以 0
# ValueError 值类型正确但内容不合适
# TypeError 类型错误
# KeyError / IndexError 取值不存在
# FileNotFoundError 文件未找到
# 故意触发一次看看效果(注释掉防止运行报错)
# 1 / 0
# ———————————————————————-
# 9.2 try / except:捕获并处理异常
# ———————————————————————-
def parse_altitude(text: str) –> float:
try:
return float(text)
except ValueError as e:
print(f"无法解析 '{text}': {e}")
return float("nan") # NaN = Not a Number,标记无效
print(parse_altitude("540.0")) # 540.0
print(parse_altitude("not-a-number")) # 触发 except
# ———————————————————————-
# 9.3 多个 except 与"裸 except"
# ———————————————————————-
def safe_div(a, b):
try:
return a / b
except ZeroDivisionError:
print("Cannot divide by zero.")
return float("inf")
except TypeError:
print("Both operands must be numbers.")
return float("nan")
print(safe_div(10, 2)) # 5.0
print(safe_div(10, 0)) # inf
print(safe_div(10, "x")) # NaN
# ———————————————————————-
# 9.4 else 与 finally
# else:try 块**没有**抛异常时执行
# finally:无论是否抛异常,最后一定会执行(适合清理资源)
# ———————————————————————-
def read_telemetry_line(line: str) –> float:
try:
value = float(line)
except ValueError:
print(f"Bad line: {line!r}") # !r 让字符串带上引号显示
raise # 把异常原样重新抛出
else:
print(f"Parsed OK: {value}") # 没出错才执行
return value
finally:
print("— telemetry read attempt done —") # 总执行
try:
read_telemetry_line("22.4")
read_telemetry_line("oops")
except ValueError:
pass # 外层已经记录过,这里静默
# ———————————————————————-
# 9.5 主动抛出 raise:把问题暴露给上层
# ———————————————————————-
def set_throttle(percent: float) –> None:
if not 0 <= percent <= 100:
raise ValueError(f"throttle must be 0-100, got {percent}")
print(f"Throttle set to {percent:.0f}%")
try:
set_throttle(120)
except ValueError as e:
print("Caught:", e)
# ———————————————————————-
# 9.6 自定义异常
# ———————————————————————-
class TelemetryError(Exception): # 继承 Python 内置的 Exception
"""遥测数据相关的异常基类。"""
class OutOfRangeError(TelemetryError):
"""数值越界。"""
class SensorTimeoutError(TelemetryError):
"""传感器超时。"""
def check_temperature(celsius: float) –> None:
if celsius < –100 or celsius > 150:
raise OutOfRangeError(f"Temperature {celsius}°C out of safe range")
print("Temperature nominal.")
try:
check_temperature(–250)
except TelemetryError as e: # 捕获整个家族的异常
print(f"Telemetry issue: {type(e).__name__}: {e}")
# ———————————————————————-
# 9.7 except 链:raise … from …
# ———————————————————————-
def load_config(path: str) –> dict:
try:
with open(path, encoding="utf-8") as f:
return eval(f.read()) # 仅作演示,真实场景应使用 json
except FileNotFoundError as e:
raise TelemetryError(f"Config missing: {path}") from e
try:
load_config("/no/such/file.cfg")
except TelemetryError as e:
print(f"Top-level: {e}")
print(f"Caused by: {type(e.__cause__).__name__}: {e.__cause__}")
# ———————————————————————-
# 9.8 warnings 模块:发"提醒"而不中断
# ———————————————————————-
import warnings
def low_fuel_alert(level: float) –> None:
"""燃料过低时发出 RuntimeWarning(业务提醒),不中断流程。
注意:DeprecationWarning 是给"已废弃 API"用的,不适合业务告警。
"""
if level < 10:
warnings.warn("Fuel level below 10%", RuntimeWarning, stacklevel=2)
low_fuel_alert(5) # 会输出警告,但默认只显示一次
print("Continuing despite warning.")
# 让 RuntimeWarning 默认总是显示(演示场景)
warnings.simplefilter("always", RuntimeWarning)
low_fuel_alert(3) # 第二次仍然会输出
print("Continuing despite warning.")
课堂建议:把代码贴到 chapters/chapter09_errors.py,运行验证后再讲解每段作用。
第 10 章–文件与日志
知识点:pathlib / with / JSON / logging 实战主题:文件与日志
"""第 10 章:文件与日志。
学习路径(零跳跃):
pathlib 路径 → 文本读写 → with 语句 → JSON 持久化 → logging 模块
实战:把任务事件写入本地日志文件。
"""
# ———————————————————————-
# 10.1 pathlib:面向对象的路径处理(取代 os.path)
# ———————————————————————-
from pathlib import Path
base_dir = Path(__file__).resolve().parent # 当前脚本所在目录
log_file = base_dir / "mission.log" # 用 / 拼接路径,跨平台
data_file = base_dir / "telemetry.json"
print("Log file will be at:", log_file)
# ———————————————————————-
# 10.2 写文本文件(with 语句自动关闭)
# ———————————————————————-
with log_file.open("w", encoding="utf-8") as f:
f.write("2026-08-12T09:00:00 LAUNCH OK\\n")
f.write("2026-08-12T09:05:30 ORBIT INSERT\\n")
f.write("2026-08-12T09:10:00 PAYLOAD DEPLOY\\n")
# ———————————————————————-
# 10.3 读文本文件
# ———————————————————————-
with log_file.open(encoding="utf-8") as f:
contents = f.read()
print(contents)
# 按行读取
with log_file.open(encoding="utf-8") as f:
for line in f:
print("LINE:", line.rstrip()) # rstrip() 去掉行尾换行
# ———————————————————————-
# 10.4 追加模式 "a" 与二进制模式 "wb"/"rb"
# ———————————————————————-
with log_file.open("a", encoding="utf-8") as f:
f.write("2026-08-12T10:00:00 COMMS CHECK OK\\n")
# 二进制:用 pickle 简单演示
import pickle
blob = {"sat_id": "SAT-001", "alt_km": 540.0}
bin_file = base_dir / "snapshot.bin"
with bin_file.open("wb") as f:
pickle.dump(blob, f)
with bin_file.open("rb") as f:
restored = pickle.load(f)
print("Restored:", restored)
# ———————————————————————-
# 10.5 JSON:跨语言通用的数据格式
# ———————————————————————-
import json
telemetry = {
"sat_id": "SAT-001",
"samples": [
{"t": 0, "alt_km": 540.0, "voltage": 28.1},
{"t": 1, "alt_km": 540.1, "voltage": 28.0},
{"t": 2, "alt_km": 540.3, "voltage": 27.9},
],
}
with data_file.open("w", encoding="utf-8") as f:
json.dump(telemetry, f, indent=2) # indent 让文件更易读
with data_file.open(encoding="utf-8") as f:
restored_json = json.load(f)
print("First sample:", restored_json["samples"][0])
# ———————————————————————-
# 10.6 logging:标准日志库(比 print 更专业)
# ———————————————————————-
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
filename=log_file,
filemode="a", # 追加
encoding="utf-8",
)
log = logging.getLogger("mission")
log.debug("debug-level detail") # 默认等级 INFO 不会显示
log.info("Mission clock started")
log.warning("Battery below 30%")
log.error("Lost contact with SAT-002")
log.critical("Range safety violated")
# ———————————————————————-
# 10.7 实战:把遥测样本写成 JSON 报告
# ———————————————————————-
report = {
"mission": "Artemis-VII",
"generated_at": "2026-08-12T09:30:00",
"events": ["launch", "orbit-insert", "payload-deploy", "comms-check"],
}
report_file = base_dir / "report.json"
with report_file.open("w", encoding="utf-8") as f:
json.dump(report, f, indent=2)
print("Wrote:", report_file)
print("Done. Cleaning up generated files:")
for p in (log_file, data_file, bin_file, report_file):
p.unlink(missing_ok=True) # missing_ok=True 让不存在的文件不报错
课堂建议:把代码贴到 chapters/chapter10_files.py,运行验证后再讲解每段作用。
第 11 章–面向对象:航天器类
知识点:class / init / 继承 / dataclass / Enum 实战主题:面向对象:航天器类
"""第 11 章:面向对象:航天器类。
学习路径(零跳跃):
class 定义 → __init__ 构造 → 实例方法 → 实例属性 vs 类属性
→ 继承 → 方法重写 → @dataclass → 枚举 Enum
实战:Spacecraft 家族(卫星、火箭、探测器)。
"""
# ———————————————————————-
# 11.1 最简单的类
# ———————————————————————-
class Spacecraft:
"""所有航天器的基类。"""
species = "spacecraft" # 类属性:所有实例共享
def __init__(self, name: str, mass_kg: float):
self.name = name # 实例属性:每个实例独有
self.mass_kg = mass_kg
self.operational = True
def status(self) –> str:
state = "operational" if self.operational else "offline"
return f"{self.name} [{state}] mass={self.mass_kg}kg"
def deactivate(self) –> None:
self.operational = False
sat = Spacecraft("Hubble-Echo", 12000)
print(sat.status())
print("Species:", sat.species)
sat.deactivate()
print(sat.status())
# ———————————————————————-
# 11.2 __repr__ / __str__:让打印结果可读
# ———————————————————————-
class Satellite(Spacecraft):
def __init__(self, name: str, mass_kg: float, alt_km: float):
super().__init__(name, mass_kg) # 调用父类 __init__
self.alt_km = alt_km
def __repr__(self) –> str: # 给开发者的精确表示
return f"Satellite(name={self.name!r}, mass={self.mass_kg}, alt={self.alt_km})"
def __str__(self) –> str: # 给用户的可读文本
return f"Satellite {self.name} @ {self.alt_km} km"
comm = Satellite("CommSat-1", 480, 800)
print(repr(comm))
print(str(comm))
# ———————————————————————-
# 11.3 继承与方法重写
# ———————————————————————-
class Rocket(Spacecraft):
"""有推力的航天器。"""
def __init__(self, name: str, mass_kg: float, thrust_kn: float):
super().__init__(name, mass_kg)
self.thrust_kn = thrust_kn
def status(self) –> str: # 重写父类方法
base = super().status()
return f"{base} thrust={self.thrust_kn}kN"
artemis = Rocket("Artemis-VII", 2670500, 39000)
print(artemis.status())
# ———————————————————————-
# 11.4 鸭子类型(Duck Typing)
# Python 不严格检查类型,只要对象有需要的属性/方法即可使用。
# ———————————————————————-
def report_status(obj) –> None:
print(obj.status()) # 不管 obj 是什么类型,只要它有 .status()
report_status(comm)
report_status(artemis)
# ———————————————————————-
# 11.5 @dataclass:减少样板代码(PEP 557,Python 3.7+)
# ———————————————————————-
from dataclasses import dataclass, field
@dataclass
class Probe:
name: str
target: str
payload_kg: float = 0.0
instruments: list[str] = field(default_factory=list) # 可变默认值要这样写
voyager = Probe("Voyager-3", "Europa", payload_kg=850)
voyager.instruments.append("spectrometer")
print(voyager)
# 自动生成的 __eq__ 让对象可以直接比较
print(voyager == Probe("Voyager-3", "Europa", payload_kg=850))
# ———————————————————————-
# 11.6 Enum:用名字代替"魔法数字"
# ———————————————————————-
from enum import Enum, auto
class MissionPhase(Enum):
PRELAUNCH = auto()
ASCENT = auto()
ORBIT = auto()
LANDING = auto()
print(MissionPhase.ORBIT) # MissionPhase.ORBIT
print(MissionPhase.ORBIT.name, MissionPhase.ORBIT.value)
current = MissionPhase.ORBIT
match current:
case MissionPhase.PRELAUNCH:
print("Final checks")
case MissionPhase.ASCENT:
print("Climbing")
case MissionPhase.ORBIT:
print("Coast")
case MissionPhase.LANDING:
print("Descent")
# ———————————————————————-
# 11.7 实战:把第 08 章的物理公式封装成方法
# ———————————————————————-
import math
@dataclass
class Orbiter(Spacecraft):
name: str
mass_kg: float
alt_km: float
@property
def orbital_speed(self) –> float: # @property 让方法像属性一样访问
GM = 3.986e14
r = 6.371e6 + self.alt_km * 1000
return math.sqrt(GM / r)
orb = Orbiter("SAT-001", 480, 540)
print(f"{orb.name} speed = {orb.orbital_speed:.0f} m/s")
课堂建议:把代码贴到 chapters/chapter11_oop.py,运行验证后再讲解每段作用。
第 12 章–迭代器与生成器
知识点:迭代协议 / yield / 生成器表达式 / itertools 实战主题:迭代器与生成器
"""第 12 章:迭代器与生成器。
学习路径(零跳跃):
迭代协议 → iter() / next() → 生成器函数 yield → 生成器表达式
→ itertools 工具 → 实时数据流模拟
实战:用生成器模拟卫星实时遥测流。
"""
# ———————————————————————-
# 12.1 迭代协议:能被 for 循环遍历的对象都实现了 __iter__
# ———————————————————————-
# for x in obj 等价于:
# it = iter(obj)
# while True:
# try:
# x = next(it)
# except StopIteration:
# break
nums = [10, 20, 30]
it = iter(nums)
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
# print(next(it)) # 触发 StopIteration
# ———————————————————————-
# 12.2 生成器函数:用 yield 一次返回一个值
# ———————————————————————-
def countdown(n: int):
while n > 0:
yield n # 暂停在此,把 n 返回给调用者
n -= 1
for x in countdown(5):
print("count:", x)
# ———————————————————————-
# 12.3 实战:实时遥测流
# ———————————————————————-
import random, math, time
def telemetry_stream(sat_id: str, base_alt_km: float = 540.0):
"""无限产出 (timestamp, alt_km, voltage) 元组的生成器。"""
t = 0
while True:
alt = base_alt_km + 5 * math.sin(t / 10.0) + random.uniform(–0.5, 0.5)
voltage = 28.0 + random.uniform(–0.3, 0.3)
yield t, sat_id, alt, voltage
t += 1
stream = telemetry_stream("SAT-001") # 创建生成器
for _ in range(3): # 拉取 3 次
t, sid, alt, v = next(stream)
print(f"t={t}s {sid} alt={alt:.2f}km v={v:.2f}V")
# ———————————————————————-
# 12.4 生成器表达式:像列表推导式,但用 () 包裹且惰性求值
# ———————————————————————-
# 列表推导式:立刻生成全部元素,耗内存
sq_list = [x * x for x in range(5)]
print(sq_list)
# 生成器表达式:按需生成
sq_gen = (x * x for x in range(5))
print(next(sq_gen), next(sq_gen)) # 0 1
print(sum(sq_gen)) # 0+1+4+9+16=30,已消耗两项
# 注:上面的 sum 会接着从 2*2=4 开始
# ———————————————————————-
# 12.5 itertools:标准库的迭代工具箱
# ———————————————————————-
import itertools
# count:无限计数器
counter = itertools.count(start=100, step=2)
print("first 5 from count:", [next(counter) for _ in range(5)])
# cycle:无限循环一个序列
cycler = itertools.cycle(["GO", "HOLD"])
print("first 4 from cycle:", [next(cycler) for _ in range(4)])
# chain:把多个迭代器串起来
combined = itertools.chain([1, 2], (3, 4), "ab")
print("chain:", list(combined))
# islice:切片一个无限迭代器
print("islice count 5:", list(itertools.islice(itertools.count(), 5)))
# batched(Python 3.13+):按批次取
for batch in itertools.batched(range(10), 3):
print("batch:", batch)
# ———————————————————————-
# 12.6 实战:从遥测流里只保留告警项
# ———————————————————————-
def filter_alerts(stream, voltage_low: float = 27.9):
"""消耗上游生成器,只把电压过低的样本透传出去。"""
for sample in stream:
t, sid, alt, v = sample
if v < voltage_low:
yield t, sid, v
# 重新创建一个新流(前面的 stream 已推进过)
fresh_stream = telemetry_stream("SAT-002", base_alt_km=600)
alert_pipe = filter_alerts(fresh_stream, voltage_low=27.9)
# islice 让无限生成器只取前若干项,避免挂死
import itertools
print("ALERT samples (first 5 checked):")
for item in itertools.islice(alert_pipe, 5):
print("ALERT:", item)
print("(可能 0 条:取决于随机数)")
# ———————————————————————-
# 12.7 send / close:进阶用法(向生成器回传值 / 关闭生成器)
# ———————————————————————-
def accumulator():
total = 0
while True:
value = yield total # 既能产出 total,又能接收外部 send 的值
if value is None:
break
total += value
acc = accumulator()
next(acc) # 启动生成器,跑到第一个 yield
print("acc send 10:", acc.send(10)) # total=10
print("acc send 5 :", acc.send(5)) # total=15
acc.close() # 关闭生成器
课堂建议:把代码贴到 chapters/chapter12_iterators.py,运行验证后再讲解每段作用。
第 13 章–类型提示与契约
知识点:类型提示 / typing / Protocol / TypeGuard 实战主题:类型提示与契约
"""第 13 章:类型提示与契约。
学习路径(零跳跃):
内置类型提示 → PEP 604 联合类型 → TypeAlias → Generic[T]
→ Protocol 协议 → TypeGuard → runtime 自检
实战:为遥测模块加上严格的类型契约。
"""
# ———————————————————————-
# 13.1 内置类型提示(PEP 585:Python 3.9+ 可直接用 list[int])
# ———————————————————————-
from typing import TypeAlias # 仅留 TypeAlias;X | None 用 PEP 604
def mean(values: list[float]) –> float:
return sum(values) / len(values)
print(mean([1.0, 2.0, 3.0]))
# ———————————————————————-
# 13.2 PEP 604 联合类型:X | None 表示"可能是 None"(Python 3.10+)
# 注:typing.Optional[X] 与 Union[X, None] 完全等价,但 PEP 604 更简洁。
# ———————————————————————-
def find_satellite(fleet: dict[str, dict], sat_id: str) –> dict | None:
return fleet.get(sat_id) # .get 不存在时返回 None
fleet = {"SAT-001": {"alt_km": 540}}
print(find_satellite(fleet, "SAT-001"))
print(find_satellite(fleet, "MISSING"))
# ———————————————————————-
# 13.3 TypeAlias:让复杂类型有名字(PEP 613)
# ———————————————————————-
Telemetry: TypeAlias = dict[str, float | int | str] # PEP 604 联合写法
Fleet: TypeAlias = dict[str, Telemetry]
def average_altitude(f: Fleet) –> float:
return sum(s["alt_km"] for s in f.values()) / len(f)
# ———————————————————————-
# 13.4 泛型函数(typing.Generic,PEP 484)
# ———————————————————————-
from typing import TypeVar, Generic
T = TypeVar("T") # 类型变量
def first(items: list[T]) –> T | None:
return items[0] if items else None
print(first([10, 20])) # int
print(first(["a", "b"])) # str
# ———————————————————————-
# 13.5 Protocol:结构化子类型(鸭子类型的"官方版")
# ———————————————————————-
from typing import Protocol, runtime_checkable
class TelemetrySource(Protocol):
"""任何定义了 read() -> dict 的对象都满足这个协议。"""
def read(self) –> dict: ...
@runtime_checkable
class Readable(Protocol):
def read(self) –> dict: ...
class MockSensor:
def read(self) –> dict:
return {"voltage": 28.1}
mock = MockSensor()
print(isinstance(mock, Readable)) # True,结构匹配即可
# ———————————————————————-
# 13.6 TypeGuard:自定义类型守卫
# ———————————————————————-
from typing import TypeGuard
def is_altitude_dict(obj: object) –> TypeGuard[dict[str, float]]:
return (
isinstance(obj, dict)
and "alt_km" in obj
and isinstance(obj["alt_km"], (int, float))
)
def describe(obj: object) –> None:
if is_altitude_dict(obj):
# 在这个分支里 obj 被收窄为 dict[str, float]
print(f"altitude = {obj['alt_km']:.1f} km")
describe({"alt_km": 540.0})
describe("not a dict")
# ———————————————————————-
# 13.7 运行时类型自检:typing.get_type_hints
# ———————————————————————-
import typing
def burn_budget(initial: float, exhaust_v: float, mass_flow: float, burn_time: float) –> float:
import math
final_mass = initial – mass_flow * burn_time
return exhaust_v * math.log(initial / final_mass)
print(typing.get_type_hints(burn_budget))
# {'initial': <class 'float'>, 'exhaust_v': <class 'float'>, …
# ———————————————————————-
# 13.8 实战:把第 11 章的 Orbiter 加上完整类型契约
# ———————————————————————-
from dataclasses import dataclass
@dataclass
class TypedOrbiter:
name: str
mass_kg: float
alt_km: float
def orbital_speed(self) –> float:
import math
GM = 3.986e14
r = 6.371e6 + self.alt_km * 1000
return math.sqrt(GM / r)
def summary(self) –> str:
return f"{self.name}: alt={self.alt_km}km, v={self.orbital_speed():.0f}m/s"
orb = TypedOrbiter("SAT-X", 480, 540)
print(orb.summary())
课堂建议:把代码贴到 chapters/chapter13_typing.py,运行验证后再讲解每段作用。
第 14 章–并发任务
知识点:asyncio / async-await / gather / TaskGroup 实战主题:并发任务
"""第 14 章:并发任务。
学习路径(零跳跃):
并发模型对比 → threading → asyncio 基础 → async/await → Task 编排
实战:并发监控多颗卫星的遥测。
"""
# ———————————————————————-
# 14.1 并发模型对比(先有概念,再写代码)
# ———————————————————————-
# 多线程 (threading) :I/O 密集;GIL 让 CPU 密集无法真正并行
# 多进程 (multiprocessing):CPU 密集;绕过 GIL,但开销大
# 异步 (asyncio) :单线程协作式调度,适合高并发 I/O
# 本章聚焦 asyncio:现代 Python 网络/任务编排首选
# ———————————————————————-
# 14.2 asyncio 基础:事件循环 + 协程
# ———————————————————————-
import asyncio
import random
async def fetch_telemetry(sat_id: str, delay: float) –> dict:
"""模拟从一颗卫星读取遥测。async def 定义的就是协程函数。"""
await asyncio.sleep(delay) # 异步等待,不阻塞事件循环
return {"sat_id": sat_id, "voltage": 28.0 + random.uniform(–0.5, 0.5)}
# ———————————————————————-
# 14.3 asyncio.run 启动事件循环
# ———————————————————————-
async def main_simple() –> None:
result = await fetch_telemetry("SAT-001", 0.1)
print("simple:", result)
asyncio.run(main_simple())
# ———————————————————————-
# 14.4 gather:并发等待多个协程
# ———————————————————————-
async def gather_demo() –> None:
tasks = [fetch_telemetry(f"SAT-{i:03d}", random.uniform(0.05, 0.2))
for i in range(1, 6)]
results = await asyncio.gather(*tasks) # 并发执行,全部完成后返回列表
for r in results:
print("gather:", r)
asyncio.run(gather_demo())
# ———————————————————————-
# 14.5 create_task:手动调度
# ———————————————————————-
async def task_demo() –> None:
task1 = asyncio.create_task(fetch_telemetry("SAT-A", 0.1))
task2 = asyncio.create_task(fetch_telemetry("SAT-B", 0.05))
print("waiting A…")
a = await task1
print("waiting B…")
b = await task2
print(a, b)
asyncio.run(task_demo())
# ———————————————————————-
# 14.6 TaskGroup:Python 3.11+ 推荐的并发写法
# 任何一个子任务抛异常,其余会自动取消,结构更清晰。
# ———————————————————————-
async def tg_demo() –> None:
async with asyncio.TaskGroup() as tg:
results = [tg.create_task(fetch_telemetry(f"SAT-{i}", 0.05))
for i in range(3)]
# 退出 with 块说明全部成功
for r in results:
print("tg:", r.result())
asyncio.run(tg_demo())
# ———————————————————————-
# 14.7 带超时的并发
# ———————————————————————-
async def slow_sensor() –> dict:
await asyncio.sleep(1.0)
return {"ok": True}
async def with_timeout() –> None:
try:
result = await asyncio.wait_for(slow_sensor(), timeout=0.2)
print("got:", result)
except TimeoutError:
print("Sensor timed out.")
asyncio.run(with_timeout())
# ———————————————————————-
# 14.8 实战:并发监控 5 颗卫星
# ———————————————————————-
async def monitor_fleet() –> None:
sats = [f"SAT-{i:03d}" for i in range(1, 6)]
async def poll(sat_id: str) –> None:
for tick in range(2):
data = await fetch_telemetry(sat_id, 0.05)
print(f" {sat_id} tick={tick} voltage={data['voltage']:.2f}V")
async with asyncio.TaskGroup() as tg:
for s in sats:
tg.create_task(poll(s))
print("Fleet poll complete.")
asyncio.run(monitor_fleet())
# ———————————————————————-
# 14.9 线程小例子:阻塞 I/O 也可放线程里跑
# ———————————————————————-
import threading
def blocking_io(name: str) –> None:
import time
time.sleep(0.2)
print(f"[thread {name}] done")
threads = [threading.Thread(target=blocking_io, args=(f"T{i}",)) for i in range(3)]
for t in threads: t.start()
for t in threads: t.join()
print("All threads finished.")
课堂建议:把代码贴到 chapters/chapter14_concurrency.py,运行验证后再讲解每段作用。
第 15 章–综合实战项目
知识点:dataclass / asyncio / argparse / 综合 实战主题:综合实战项目
"""第 15 章:综合实战项目 —— Mission Control CLI。
学习路径(零跳跃):
包结构 → 模块拆分 → CLI 参数解析 argparse → 单元测试 → 打包
本章将前面的所有知识组合成一个完整的命令行小工具。
为了方便直接运行,本文件采用"单文件可执行"形式,但内部按模块风格组织。
"""
import argparse
import asyncio
import json
import math
import random
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Iterable
# ———————————————————————-
# 15.1 数据模型:dataclass 让实体定义简洁
# ———————————————————————-
@dataclass
class Telemetry:
sat_id: str
alt_km: float
voltage: float
temp_c: float
@dataclass
class Fleet:
crafts: list[Telemetry] = field(default_factory=list)
def add(self, t: Telemetry) –> None:
self.crafts.append(t)
def lowest_voltage(self) –> Telemetry | None:
return min(self.crafts, key=lambda t: t.voltage) if self.crafts else None
def average_altitude(self) –> float:
return sum(t.alt_km for t in self.crafts) / len(self.crafts) if self.crafts else 0.0
# ———————————————————————-
# 15.2 业务函数:复用第 03/08 章的物理公式
# ———————————————————————-
GM_EARTH = 3.986e14
EARTH_RADIUS_M = 6.371e6
def orbital_speed(alt_km: float) –> float:
r = EARTH_RADIUS_M + alt_km * 1000
return math.sqrt(GM_EARTH / r)
async def fetch_telemetry(sat_id: str, alt_km: float) –> Telemetry:
await asyncio.sleep(random.uniform(0.01, 0.05))
return Telemetry(
sat_id=sat_id,
alt_km=alt_km + random.uniform(–1.0, 1.0),
voltage=28.0 + random.uniform(–0.4, 0.4),
temp_c=–15.0 + random.uniform(–3.0, 3.0),
)
async def poll_fleet(altitudes: Iterable[float]) –> Fleet:
fleet = Fleet()
sats = list(altitudes)
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch_telemetry(f"SAT-{i+1:03d}", alt))
for i, alt in enumerate(sats)]
for task in tasks:
fleet.add(task.result())
return fleet
# ———————————————————————-
# 15.3 报告输出:格式化表格 + JSON
# ———————————————————————-
def print_report(fleet: Fleet) –> None:
print(f"{'ID':<10} {'ALT (km)':>10} {'VOLT (V)':>10} {'TEMP (C)':>10} {'SPEED (m/s)':>14}")
print("-" * 56)
for t in fleet.crafts:
speed = orbital_speed(t.alt_km)
print(f"{t.sat_id:<10} {t.alt_km:>10.2f} {t.voltage:>10.2f} {t.temp_c:>10.2f} {speed:>14.1f}")
print("-" * 56)
print(f"Average altitude : {fleet.average_altitude():.2f} km")
low = fleet.lowest_voltage()
if low:
print(f"Lowest voltage : {low.sat_id} ({low.voltage:.2f} V)")
def save_report(fleet: Fleet, path: Path) –> None:
payload = {
"crafts": [asdict(t) for t in fleet.crafts],
"summary": {
"average_altitude_km": fleet.average_altitude(),
"lowest_voltage": asdict(fleet.lowest_voltage()) if fleet.lowest_voltage() else None,
},
}
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
# ———————————————————————-
# 15.4 CLI 入口:argparse
# ———————————————————————-
def build_parser() –> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="missionctl", description="Mission Control CLI")
parser.add_argument("–altitudes", nargs="+", type=float,
default=[400, 540, 800, 1200],
help="目标轨道高度列表(km)")
parser.add_argument("–out", type=Path, help="可选:把报告写入 JSON 文件")
return parser
async def amain(args: argparse.Namespace) –> int:
fleet = await poll_fleet(args.altitudes)
print_report(fleet)
if args.out:
save_report(fleet, args.out)
print(f"Report saved to: {args.out}")
return 0
def main(argv: list[str] | None = None) –> int:
parser = build_parser()
args = parser.parse_args(argv)
return asyncio.run(amain(args))
if __name__ == "__main__":
raise SystemExit(main())
# ———————————————————————-
# 15.5 内置冒烟测试(取消注释运行)
# ———————————————————————-
"""
def _smoke_test() -> None:
fleet = Fleet([Telemetry("SAT-001", 540, 28.0, -15.0),
Telemetry("SAT-002", 800, 27.8, -22.0)])
assert fleet.average_altitude() == 670.0
assert fleet.lowest_voltage().sat_id == "SAT-002"
print("smoke test passed")
_smoke_test()
"""
课堂建议:把代码贴到 chapters/chapter15_project.py,运行验证后再讲解每段作用。
5. 章节串联的"叙事弧线"
为了让学员学完后能记住整体结构,备课时可以用这条故事线:
6. 常见学生问题 & 应对话术
| 「为什么 Python 不需要声明类型?」 | 引到"鸭子类型 + 动态类型",并预告第 13 章会重新"加回类型提示"做平衡 |
| 「f-string 嵌表达式会不会很难读?」 | 鼓励"短表达式可以嵌,长表达式应该先赋给变量" |
| 「列表和元组到底选谁?」 | “要改用 list,不会改用 tuple;元组也常用于函数返回多个值” |
| 「生成器和迭代器看起来很玄?」 | 把 yield 比喻成"暂停键",调用方 next() 就像"再按一下播放" |
| 「asyncio 和线程到底用哪个?」 | “高并发 I/O 用 asyncio;阻塞型 I/O 且数量少时用线程;CPU 密集才用进程” |
7. 进阶延伸(学员学有余力时布置)
- 给第 10 章的日志加上"按日期轮转"(logging.handlers.TimedRotatingFileHandler)
- 给第 15 章加 pytest 单元测试
- 把第 15 章发布到 PyPI:uv build && uv publish
- 在第 14 章基础上加上 aiohttp 真实抓取某公开遥测 API(推荐 https://api.wheretheiss.at/v1/satellites/25544)
7.5 废弃 API 与最新语法自检(讲师必看)
Python 3.14 仍保留许多"能用但已废弃"的旧写法。课程绝不能让学生学到这些 API。 本教程在交付前已通过以下三层检查:
| 静态 AST | 自研扫描脚本(/tmp/scan_deprecated.py) | 扫 imp / optparse / distutils / asyncore / typing.Dict / Optional[X] / Union[X,Y] 等 |
| 编译期 | python -m py_compile chapters/chapter*.py | 语法错误直接报错 |
| 运行期 | python -W error::DeprecationWarning -W error::SyntaxWarning -W error::FutureWarning | 三类"未来必删"的警告升为错误,运行时拦截 |
讲师自检命令(建议每次开课前跑一次):
cd python_code
# 1) 静态扫描:应输出 "✅ 未发现明显的废弃 API 使用"
uv run python /tmp/scan_deprecated.py
# 2) 全章语法编译
uv run python -m py_compile chapters/chapter*.py
# 3) 严格警告模式跑全部章节
for n in 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15; do
f=chapters/chapter${n}_*.py
if [ "$n" = "15" ]; then
timeout 15 uv run python -W error::DeprecationWarning -W error::SyntaxWarning -W error::FutureWarning $f –altitudes 400 540 800
else
timeout 15 uv run python -W error::DeprecationWarning -W error::SyntaxWarning -W error::FutureWarning $f < /dev/null
fi
done
# 全部应输出 exit=0
本项目使用的"现代首选"语法清单(避免学生学到旧写法)
| 容器类型提示 | typing.List[int]、typing.Dict[str, int] | list[int]、dict[str, int](PEP 585,Python 3.9+) |
| 联合类型 | Optional[X]、Union[A, B] | `X |
| 路径处理 | os.path.join(…) | pathlib.Path + / 拼接 |
| 命令行参数 | optparse(已废弃) | argparse(标准库) |
| 并发模型 | asyncore(3.12 已移除) | asyncio + TaskGroup(Python 3.11+) |
| 异常告警 | warnings.warn(…, DeprecationWarning) 当业务告警用 | RuntimeWarning 才是业务告警首选 |
| 字符串格式化 | "%s = %d" % (name, value) | f-string:f"{name} = {value}" |
| 协程并行 | asyncio.gather(*coros) 单独使用 | async with asyncio.TaskGroup() 上下文(Python 3.11+) |
| JSON 输出 | 手写 json.dumps(…) 后手工美化 | json.dump(obj, f, indent=2) |
| 数据类 | 手写 __init__ + __repr__ + __eq__ | @dataclass(PEP 557,Python 3.7+) |
| 枚举 | 用类属性手写常量 | enum.Enum + auto() |
| 文件关闭 | f = open(…); …; f.close() | with open(…) as f: |




