类封装的核心不是「把函数塞进类里」,是「保证资源成对释放」。
五个方法:open、encode、flush、close、析构(Python 里是 __exit__)。
上一节连上了摄像头,这一节把「采集 → 编码」包成一个可复用的东西。
RAII 的思想在 32 课讲过,这里用 Python 的 with 复现。
一、五个方法,各有各的纪律
| 方法 | 做什么 | 重复调用会怎样 |
|---|---|---|
open() |
建容器、建编码器 | 应该报错,不能静默重来 |
encode(frame) |
送一帧,返回若干包 | 没 open 就报错 |
flush() |
冲空包,把尾巴吐出来 | 只能调一次有效 |
close() |
关文件写索引 | 重复调应该无害 |
__enter__/__exit__ |
支持 with,异常也保证关 | — |
第三行和第四行的对比最能说明「纪律」这个词: flush 调两次第二次就是空的(无害但没意义),close 调两次必须无害。 这两种「重复」的处理方式不一样,写代码时要分开想。
二、构造函数不做事,Open 才建资源
这是 C++ 的硬性要求:构造函数里抛异常,对象就没了,
外面拿不到「失败了一半」的状态,也就没法清理。
所以标准写法是「构造函数只存参数,Open() 里建资源」。
| C++ 做法 | 为什么 | Python 对应 |
|---|---|---|
构造只存参数,Open() 建资源 |
构造失败没法报错 | __init__ 只存参数 |
析构里 Close() |
保证异常也释放 | __exit__ |
| 状态标志防重入 | 重复打开会泄漏 | self.c is None 判断 |
第一行在 Python 里同样适用:__init__ 抛异常对象也不会留下,所以也建议「先构造、再打开」。
三、实测四种误用
| 误用 | 结果 |
|---|---|
| 没 open 就 encode | 拦住:not open |
| 重复 open | 拦住:already open |
| close 之后 encode | 拦住:already closed |
| 重复 close | 无害,直接返回 |
前三种都是「主动报错」,第四种是「静默返回」——这个区别是有意的: 前三种是调用顺序错了(bug),第四种是清理逻辑可能被多个地方调用(正常情况)。 写资源类的时候,这一条要提前想清楚:哪些操作「重复 = 错误」,哪些操作「重复 = 无害」。
四、为什么这层封装值得写
| 不封装 | 封装后 |
|---|---|
| open/close 散在各处 | 成对出现,异常也保证 |
| 换编码器要改多处 | 只改一个类 |
| 错误处理各写各的 | 统一在一处 |
第二行是真正的收益:H.264 换 H.265 只改类的内部,调用方一行不动。 这也是为什么书里这一节要讲「类封装」而不是「写个函数」—— 监控项目里编码器经常要按客户要求切换。
五、动手 Demo:把编码器包成一个类,接口该长什么样
Demo 写一个 Encoder 类,然后故意用错四次,看它有没有拦住。
最后用 with 跑一遍正常流程,验证异常时也会关文件。
class Encoder(object):
def __init__(self, path):
self.path = path
self.c = None
self.closed = False
def open(self):
if self.c is not None:
raise RuntimeError("already open")
self.c = av.open(self.path, "w", format="h264")
...
def close(self):
if self.c is None or self.closed:
return
self.c.close()
self.closed = True
def __enter__(self):
return self.open()
def __exit__(self, *a):
self.close()
实跑输出:
--------------------------------------------------------------------
把编码器包成一个类,接口该长什么样
--------------------------------------------------------------------
**书上这一节是 C++ 类封装,核心是「资源管理」:打开、使用、关闭必须成对。**
**Python 里没有析构函数那套,但 RAII 的思想一样能用 `with` 表达。**
**一个合格的封装类要有五个方法:**
方法 做什么 重复调用会怎样
------------------------------------------------------------------------
`open()` 建容器、建编码器 应该报错,不能静默重来
`encode(frame)` 送一帧,返回若干包 没 open 就报错
`flush()` 冲空包,把尾巴吐出来 只能调一次有效
`close()` 关文件写索引 重复调应该无害
`__enter__/__exit__` 支持 with,异常也保证关 —
**实测四种误用,看它有没有拦住:**
误用 结果
--------------------------------------------------------
没 open 就 encode 拦住:not open
重复 open 拦住:already open
close 之后 encode 拦住:already closed
重复 close 无害,直接返回
**再用 `with` 跑一遍正常流程,验证「异常也保证关」:**
`with` 块里编了 30 帧,出了 30 个包,文件 10634 字节。
**`__exit__` 在异常时也会被调用**,所以哪怕中间抛错,
**文件也会被 close(索引会写上)**——这就是 C++ 里析构函数干的事。
**书上 C++ 版的三个要点,翻译成 Python 是这样:**
C++ 做法 为什么 Python 对应
-------------------------------------------------------------------------------------
构造函数不做事,`Open()` 才建资源 构造失败没法报错 `__init__` 只存参数
析构函数里 `Close()` 保证异常也释放 `__exit__`
状态标志防重入 重复打开会泄漏 `self.c is None` 判断
**第一行在 C++ 里是硬性要求**:构造函数抛异常对象就没了,**所以「先构造、再打开」是标准写法**。
**Python 里 `__init__` 抛异常对象也不会留下**,所以同样建议这么分。
__exit__ 返回 False 很关键:它表示「异常继续往外抛」,不能吞掉。
返回 True 会把异常咽下去,调试时会疯掉。
with 那一段编了 30 帧、出 30 个包、文件 10634 字节,
和 04.05.03 直接写的版本完全一致——封装没有改变行为,只是收紧了纪律。
30 秒实验
- 把
__exit__的返回值改成 True,故意在 with 里抛异常,看异常会不会被吞掉。 - 给类加一个
switch(codec)方法,把 H.264 换成 H.265,看调用方要改几行。 - 让
encode在flush之后调用,看现在的状态判断能不能拦住。
常见误区
- 以为封装就是「把函数塞进类」。 核心是资源的成对释放,和错误状态的显式管理。
- 以为
__exit__该返回 True。 返回 True 会吞掉异常,问题会被藏起来。 - 以为所有操作重复调用都应该报错。 close 应该无害,因为清理逻辑可能被多处调用。
自测三问(点开对答案)
1. 为什么构造函数里不建资源? —— 构造抛异常对象就没了,**外面拿不到「失败一半」的状态去清理**。所以先构造、再 `Open()`。 2. `__exit__` 返回 False 是什么意思? —— 「异常继续往外抛」。**返回 True 会吞掉异常**,看起来程序没崩,其实问题被藏了。 3. 为什么 close 重复调用要无害、open 重复调用要报错? —— close 可能被多处清理逻辑调用,**必须幂等**;open 重复是调用顺序错了,**必须暴露出来**。下一章进协议:SOAP 与 gSOAP,ONVIF 的地基。
- ☐ (本章完,下一节 06.01.01 RPC简介)