4. Python 基础回顾(下):文件 I/O 与异常处理、多线程与多进程、装饰器与生成器、常用第三方库(NumPy, Matplotlib)

好,我们继续。上一节我们把 Python 的数据结构、函数和面向对象过了一遍。这一节的内容,说实话,才是真正让你从「写脚本」过渡到「写系统」的关键。

你想想看,一个航线规划系统,要读配置文件、要处理传感器数据、要并行计算航点、还要把结果可视化。这些能力,全在这一章里。我个人习惯把这部分叫做「Python 的工业级特性」——没有它们,你的代码只能在玩具项目里打转。

第4章 知识体系结构 Python 进阶核心 文件 I/O & 异常 多线程 & 多进程 装饰器 & 生成器 NumPy / Matplotlib open / with try / except Thread / Pool Process / Queue @decorator yield ndarray plt.plot

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 文件里不要写注释。Python 的 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()  # 无论如何都要关串口
避坑指南:我曾经在写飞控地面站时,忘了在 finally 里关串口。结果程序异常退出后,串口一直被占用,第二次启动永远连不上。排查了半小时才发现是句柄没释放。从那以后,所有资源操作我都用 try/finally 或者 with 语句。

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)
我的经验:处理无人机日志文件时,一个架次可能产生几百 MB 的 CSV 数据。如果用 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 则是数据处理的左膀右臂。这些工具组合起来,你已经具备了搭建一个航线规划系统核心模块的能力。