3、Python基础速通(下):函数定义、面向对象基础、文件读写、异常处理

好,咱们接着往下聊。上一章我们把 Python 的基本数据类型和流程控制捋了一遍,算是把「地基」打好了。这一章,我们要开始搭「框架」了——函数、类、文件操作、异常处理,这四个东西在 OpenCV 和 YOLO 的实战里,几乎每一行代码都会用到。

我个人习惯把这一章叫做「实战前的最后一块拼图」。你想想看,没有函数,你的检测代码会变成一坨无法复用的面条;没有类,你没法优雅地管理模型和摄像头;不会文件读写,你连图片都存不下来;不处理异常,程序一崩就全完了。

所以,这一章很重要。咱们一个一个来。

3.1 函数定义:把逻辑装进「黑盒子」

函数是什么?说白了,就是把一段重复用的代码包起来,给它起个名字。以后想用,直接喊名字就行。

在 OpenCV 里,我经常把「读取图片 → 预处理 → 检测 → 画框 → 显示」这五步封装成一个函数。这样主程序就干净多了。

3.1.1 基本定义与调用

def detect_and_show(image_path):
    img = cv2.imread(image_path)
    # 这里放检测逻辑
    cv2.imshow('Result', img)
    cv2.waitKey(0)

嗯,这里要注意:Python 用 def 关键字定义函数,后面跟函数名和冒号。函数体要缩进。调用的时候直接写函数名加括号。

3.1.2 参数与返回值

我在项目中遇到过一个问题:同一个检测函数,有时候要处理图片文件,有时候要处理视频帧。怎么办?用参数控制。

参数类型写法说明
位置参数def func(a, b)按顺序传参
默认参数def func(a, b=10)不传就用默认值
关键字参数func(a=1, b=2)明确指定参数名
可变参数def func(*args, **kwargs)接收任意数量参数
我的习惯:写检测函数时,把置信度阈值、NMS 阈值都设成默认参数。这样大部分情况直接用默认值,调参时再传参,灵活又干净。

3.1.3 作用域与 global

函数内部的变量是局部变量,外面看不到。但有时候你需要修改全局变量——比如一个全局的帧计数器。这时候用 global 声明一下。

frame_count = 0

def process_frame():
    global frame_count
    frame_count += 1
    print(f'处理了第 {frame_count} 帧')
避坑:我曾经在一个大项目里滥用 global,结果调试时变量被到处改,查了两天才找到 bug。能用参数传递就别用 global,这是血的教训。

3.2 面向对象基础:用「类」管理复杂逻辑

面向对象编程(OOP)听起来高大上,其实核心就一句话:把数据和操作数据的方法打包在一起。

在 YOLO 检测系统里,我会把检测器设计成一个类。这个类里有模型加载、预处理、推理、后处理等方法,还有模型路径、置信度阈值等属性。用起来特别顺手。

3.2.1 类与对象

class YOLODetector:
    def __init__(self, model_path, conf_thresh=0.5):
        self.model_path = model_path
        self.conf_thresh = conf_thresh
        self.net = cv2.dnn.readNet(model_path)
    
    def detect(self, image):
        # 检测逻辑
        pass

# 使用
detector = YOLODetector('yolov8n.onnx', 0.5)
result = detector.detect(frame)

__init__ 是构造方法,创建对象时自动调用。self 代表实例本身,所有实例方法第一个参数都是 self。

3.2.2 封装、继承、多态

这三个概念是 OOP 的基石。我简单说一下在实战中的用法:

  • 封装:把检测器的内部实现藏起来,外部只调用 detect() 方法。改内部逻辑不影响外部代码。
  • 继承:比如我写了一个 BaseDetector 基类,然后 YOLODetector 和 SSDDetector 都继承它,共用接口。
  • 多态:同一个 detect() 方法,不同子类有不同的实现。调用者不用关心具体是哪个检测器。
实战建议:刚开始学 OOP,别想太多设计模式。先把「把检测器写成类」这件事做好,后面自然就理解了。

3.3 文件读写:让数据「持久化」

检测结果要保存吧?模型配置要读取吧?日志要写吧?这些都离不开文件操作。

3.3.1 文本文件读写

# 写文件
with open('results.txt', 'w') as f:
    f.write(f'检测到 {count} 个目标\n')

# 读文件
with open('config.yaml', 'r') as f:
    config = f.read()

with 语句会自动关闭文件,不用手动 f.close()。模式 'w' 是写入(覆盖),'r' 是读取,'a' 是追加。

3.3.2 图片与视频文件

OpenCV 有自己的文件读写方式:

操作代码说明
读图片cv2.imread('image.jpg')返回 numpy 数组
写图片cv2.imwrite('out.jpg', img)支持 jpg/png 等格式
读视频cv2.VideoCapture('video.mp4')返回 VideoCapture 对象
写视频cv2.VideoWriter('out.avi', fourcc, fps, size)需要指定编码器
小技巧:写视频时 fourcc 编码器经常搞错。我一般用 cv2.VideoWriter_fourcc(*'XVID'),兼容性最好。

3.4 异常处理:让程序「摔倒了能爬起来」

程序总会出错。文件找不到、模型加载失败、摄像头断开……如果不处理异常,程序直接崩溃,用户体验极差。

3.4.1 try-except 结构

try:
    img = cv2.imread('nonexistent.jpg')
    if img is None:
        raise ValueError('图片读取失败')
except FileNotFoundError:
    print('文件不存在,请检查路径')
except ValueError as e:
    print(f'值错误:{e}')
except Exception as e:
    print(f'未知错误:{e}')
else:
    print('一切正常')
finally:
    print('无论如何都会执行')

这里有几个要点:

  • try 里放可能出错的代码
  • except 捕获特定异常,可以写多个
  • else 在没异常时执行
  • finally 不管有没有异常都执行——我常用它来释放摄像头资源

3.4.2 自定义异常

有时候内置异常不够用。比如检测器加载失败,我想抛一个 ModelLoadError:

class ModelLoadError(Exception):
    pass

def load_model(path):
    if not os.path.exists(path):
        raise ModelLoadError(f'模型文件 {path} 不存在')
我曾经踩过的坑:在 except 里直接 pass 忽略异常。结果程序不报错也不干活,查了半天才发现异常被吞了。至少打个日志,别静默处理。

3.5 本章知识体系

下面这张图把本章四个核心知识点串起来了。你可以看到它们如何在 YOLO 检测系统中协同工作:

Python 基础速通(下)知识体系 函数定义 面向对象 文件读写 异常处理 YOLO 检测系统 封装检测逻辑(函数) 管理模型状态(类) 保存检测结果(文件) 核心思想:四个模块协同工作 函数封装逻辑 → 类管理状态 → 文件持久化 → 异常保稳定

嗯,这一章的内容就这些。函数、类、文件、异常——这四个东西是 Python 实战的四大支柱。你可能会觉得概念有点多,但别急,后面每一章都会反复用到它们。用着用着就熟了。

记住:写代码不是背语法,而是用语法解决问题。把这一章的例子自己敲一遍,比看十遍都管用。