4. Python 基础回顾(下):文件 I/O 与异常处理、多线程与多进程、装饰器与生成器、常用第三方库(NumPy, Matplotlib)
好,我们继续。上一节我们把 Python 的数据结构、函数和面向对象过了一遍。这一节的内容,说实话,才是真正让你从「写脚本」过渡到「写系统」的关键。
你想想看,一个航线规划系统,要读配置文件、要处理传感器数据、要并行计算航点、还要把结果可视化。这些能力,全在这一章里。我个人习惯把这部分叫做「Python 的工业级特性」——没有它们,你的代码只能在玩具项目里打转。
4.1 文件 I/O:读写配置与日志
无人机航线规划系统,启动时第一件事就是读配置文件。飞控日志、航点文件、地理围栏数据……全是文件操作。Python 的文件 I/O 其实很简单,但坑不少。
4.1.1 基础读写
最基础的,open() 函数。模式就那几个:'r' 读、'w' 写(覆盖)、'a' 追加、'rb' 二进制读。
# 读文件 - 老派写法
f = open('waypoints.txt', 'r')
content = f.read()
f.close() # 忘了 close?内存泄漏等着你
# 推荐写法 - with 语句
with open('waypoints.txt', 'r') as f:
lines = f.readlines()
# 自动关闭,省心
with open()。别问我为什么,我曾经在生产环境里见过一个忘了 close 的日志文件句柄,三天后把服务器的文件描述符吃光了。嗯,从那以后我写文件 I/O 只用 with。
4.1.2 读写 JSON 配置文件
航线规划系统的参数,我建议用 JSON 存。结构清晰,Python 原生支持。
import json
# 写入配置
config = {
"home_lat": 39.9042,
"home_lon": 116.4074,
"altitude": 120,
"speed": 15,
"waypoints": [
{"lat": 39.91, "lon": 116.40},
{"lat": 39.92, "lon": 116.41}
]
}
with open('flight_config.json', 'w') as f:
json.dump(config, f, indent=2)
# 读取配置
with open('flight_config.json', 'r') as f:
loaded_config = json.load(f)
print(loaded_config['home_lat'])
json.load() 遇到注释直接报错。如果你非要加注释,用 YAML 格式,或者单独写一个说明文档。
4.2 异常处理:让程序更健壮
无人机在天上飞,你的代码不能因为一个 GPS 丢星就崩溃。异常处理,说白了就是给程序穿上防弹衣。
4.2.1 try / except 基础
try:
altitude = float(sensor_data['altitude'])
print(f"当前高度: {altitude} 米")
except KeyError:
print("警告:传感器数据缺少高度字段")
except ValueError:
print("警告:高度数据格式异常")
except Exception as e:
print(f"未知错误: {e}")
这里有个细节:Exception 是万金油,但别滥用。我建议先捕获具体的异常类型,最后再用 Exception 兜底。这样调试的时候一眼就能看出问题在哪。
4.2.2 finally 与 else
finally 块——不管有没有异常,它都会执行。适合放清理操作,比如关闭串口、释放锁。
try:
ser = serial.Serial('/dev/ttyUSB0', 115200)
data = ser.read(1024)
except serial.SerialException:
print("串口打开失败")
else:
print("数据读取成功") # 没异常才执行
finally:
ser.close() # 无论如何都要关串口
4.3 多线程与多进程:并行处理传感器数据
航线规划系统里,你要同时处理 GPS 数据、IMU 数据、气压计数据,还要实时更新 UI。单线程?卡死你。这时候就需要多线程和多进程。
| 特性 | 多线程 (threading) | 多进程 (multiprocessing) |
|---|---|---|
| 内存共享 | 共享全局变量(需加锁) | 独立内存(需 IPC) |
| GIL 影响 | 受 GIL 限制,CPU 密集型不行 | 无 GIL 问题,充分利用多核 |
| 适用场景 | I/O 密集型(网络、文件、UI) | CPU 密集型(计算、图像处理) |
| 启动开销 | 低 | 高 |
4.3.1 多线程:用 ThreadPoolExecutor
我个人不太喜欢手动管理 threading.Thread,太麻烦。Python 3 的 concurrent.futures 才是正道。
from concurrent.futures import ThreadPoolExecutor
import time
def read_sensor(sensor_id):
time.sleep(1) # 模拟 I/O 等待
return f"sensor_{sensor_id}: OK"
# 开 4 个线程并行读取
with ThreadPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(read_sensor, i) for i in range(4)]
for future in futures:
print(future.result())
4.3.2 多进程:计算密集型任务
如果你要对航点做复杂的路径优化计算,多线程帮不了你——GIL 锁死了。这时候上多进程。
from concurrent.futures import ProcessPoolExecutor
def compute_optimal_path(waypoints):
# 假设这里有个复杂的 A* 算法
result = sum(waypoints) # 占位
return result
if __name__ == '__main__':
all_waypoints = [list(range(1000)) for _ in range(8)]
with ProcessPoolExecutor(max_workers=4) as executor:
results = executor.map(compute_optimal_path, all_waypoints)
for r in results:
print(r)
if __name__ == '__main__': 不是可选项,是必选项。Windows 上不加这行,进程池会无限递归创建子进程,直到内存爆炸。我踩过这个坑,别重蹈覆辙。
4.4 装饰器与生成器:优雅的 Python 特性
这两个特性,说实话,刚学的时候觉得「花里胡哨」。但用久了你会发现,它们能让代码变得极其优雅。
4.4.1 装饰器:给函数加「外挂」
装饰器,说白了就是一个函数,用来给另一个函数增加功能,而不修改原函数的代码。
import time
def timing_decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} 耗时: {elapsed:.3f} 秒")
return result
return wrapper
@timing_decorator
def calculate_route():
time.sleep(0.5) # 模拟计算
return "route_ok"
calculate_route() # 输出: calculate_route 耗时: 0.501 秒
我在项目中常用装饰器做三件事:日志记录、性能计时、权限校验。你想想看,如果每个函数里都手动写一遍计时代码,那得多痛苦?装饰器一次定义,到处复用。
4.4.2 生成器:省内存的迭代器
生成器用 yield 关键字。它不会一次性把所有数据加载到内存,而是「随用随取」。
def read_large_file(file_path):
"""逐行读取大文件,不爆内存"""
with open(file_path, 'r') as f:
for line in f:
yield line.strip()
# 处理 10GB 的日志文件?没问题
for line in read_large_file('flight_log.txt'):
if 'ERROR' in line:
print(line)
readlines() 一次性读,内存直接飙到 1GB+。换成生成器逐行处理,内存占用稳定在 10MB 以内。这就是生成器的威力。
4.5 常用第三方库:NumPy 与 Matplotlib
这两个库,是 Python 数据科学和工程计算的基石。航线规划里,坐标变换、轨迹绘制、数据分析,全得靠它们。
4.5.1 NumPy:高性能数组运算
Python 原生的列表做数学运算?慢得让人抓狂。NumPy 的 ndarray 底层用 C 实现,速度碾压。
import numpy as np
# 创建数组
waypoints = np.array([
[39.91, 116.40],
[39.92, 116.41],
[39.93, 116.42]
])
# 向量化运算(比 for 循环快 100 倍)
latitudes = waypoints[:, 0] # 取所有纬度
latitudes_rad = np.deg2rad(latitudes) # 批量转弧度
# 计算距离(简化版)
dlat = np.diff(latitudes_rad)
print(f"纬度变化量: {dlat}")
| 操作 | Python 列表 | NumPy 数组 |
|---|---|---|
| 加法 | [a+b for a,b in zip(x,y)] |
x + y |
| 平方 | [v**2 for v in x] |
x**2 |
| 均值 | sum(x)/len(x) |
np.mean(x) |
4.5.2 Matplotlib:把数据画出来
光看数字,你很难发现异常。画成图,一眼就能看出问题。Matplotlib 就是干这个的。
import matplotlib.pyplot as plt
# 模拟飞行轨迹
time = np.linspace(0, 100, 100) # 100 秒
altitude = 100 + 10 * np.sin(time * 0.1) # 模拟高度变化
plt.figure(figsize=(10, 5))
plt.plot(time, altitude, 'b-', linewidth=2, label='高度')
plt.xlabel('时间 (秒)')
plt.ylabel('高度 (米)')
plt.title('飞行高度曲线')
plt.grid(True, alpha=0.3)
plt.legend()
plt.show()
好了,这一节的内容就到这里。文件 I/O 让你能读写配置和日志;异常处理让程序更健壮;多线程/多进程解决并发问题;装饰器和生成器让代码更优雅;NumPy 和 Matplotlib 则是数据处理的左膀右臂。这些工具组合起来,你已经具备了搭建一个航线规划系统核心模块的能力。