⑨ 双返回契约:一个函数「交差」时,同时报告「产物」和「副作用」
目标:学会一个工程习惯——让函数返回时不只是说「我干完了」,而是明确说清楚 **「我产生了什么(产物)」**和 「旧的要扔什么(副作用)」。yt-dlp 的后处理器用
run()返回一个二元组,把这两件事一次交代清楚,调度器才能统一收尾。
搭配 [[08后处理流水线范式]]。那一篇讲「流水线怎么串」,这一篇讲「每个环节怎么向流水线交代自己的工作」。
一、问题:函数只想返回一个值,容易说不清
上一讲说过,每个后处理器要做加工,比如转码:
进来:input.webm
加工:用 ffmpeg 重编码
出去:output.mp4 ← 新文件
做完这件活,它至少有两件事要告诉外部:
- 得意的事(产物):我产出的是
output.mp4; - 扫尾的事(副作用):原来的
input.webm没用了,可以删了。
如果这个函数只返回一个值,这两个信息里必有一个说不清——要么临时用「全局变量」「干脆自己 os.remove 顺手删掉」等脏办法出口,要么调度器根本不知道什么叫「整洁收尾」。
二、解法:返回一个二元组,一次交代两件事
yt-dlp 让每个 run(info) 返回 (files_to_delete, info):
def run(self, info):
...
return [旧文件, 可以删的], info # 二元组
# 具体:转码的 PP
def run(self, info):
...
info['filepath'] = outpath # 产物:新文件记进 info
return [filename], info # 副作用:旧的 input 文件 → 列入可删
返回值这两个位置,分工明确:
| 返回位置 | 含义 | 谁消费 |
|---|---|---|
第一个 files_to_delete |
本次加工不用的旧文件清单 | 调度器打包删掉 |
第二个 info |
更新后的数据,也就是下一个环节的输入 | 喂给下一个 PP |
[!tip] 核心逻辑一句话:函数交差时,把「我做的功劳」写进 info,把「没用的旧东西」放进待删列表。两件事各归其位,绝不偷偷在函数里自己
os.remove。
三、为什么不能「自己顺手删文件」
新手最容易犯的错:转码完直接 os.remove(old_file)。
问题在哪?
- 调度器失去控制:用户可能想要
-k/keepvideo(保留原文件),可你已经偷偷删了,想留都留不回来; - 不可测试:删东西的副作用散落在每个函数里,你想 mock 都不好 mock;
- 不可统计 / 不可通知:调度器要知道「删了啥」才能记日志、报进度,偷偷删它永远不知道。
双返回契约的意义,就是把「删不删、什么时候删、要不要保留」这笔大决定的执行权收回调度器统一做。PP 只说「这些可以删」,删不删由调度器按用户配置决定:
# 调度器 run_pp 里统一收尾:
files_to_delete, infodict = pp.run(infodict)
if self.params.get('keepvideo', False): # 用户要保留?
for f in files_to_delete:
move(f, '') # 不删,改移动
else:
delete(*files_to_delete) # 否则统一删
return infodict # 传给下一个 PP
四、同一份 info 被一路改写——这才是「契约」的精髓
第二个返回值 info 不是新造一份,而是改动后的同一份。调度器把它原样喂给下一个 PP:
for pp in pps:
info = self.run_pp(pp, info) # 每个 PP 都拿着这份 info 加工
return info
所以「产物」的交接方式,其实是写在 info 里的。比如转码 PP 会做:
info['filepath'] = outpath # 告诉全世界:现在的文件在 outpath
info['ext'] = target_ext # 以及:它现在是 mp4 而不是 webm
[!important] 这就是「数据契约」思维的现场版(对照文档 ③):加工结果不是靠口头传话,而是写进大家共同认的那份 info。所有 PP 都只改这同一份 info,改完自然就是下一个环节的「成品描述」。
五、这类「双输出」思维在哪都能用
不只是视频下载。别只盯着 (files_to_delete, info) 这个例子,要学的是它背后的本能:
| 场景 | 一个调用该同时交代的两件事 |
|---|---|
| 转码 / 压缩图片 | 新文件路径 + 旧文件可删 |
| 缓存刷新 | 新缓存值 + 旧缓存可淘汰 |
| 数据清洗 ETL | 清洗后的行 + 被过滤掉的脏行 |
| 构建产物 | 新产物清单 + 过期的旧产物 |
通用规律:函数只要产生了「新东西」又废弃了「旧东西」,别憋着只回一个值、更别自己偷偷删。返回一个结构:新状态 + 待清理清单,把「清理」这个权限交给调用方统一管。
六、核心金句
双返回契约 = 让函数交差时说清楚两件事:「我做了什么(产物写进共享状态)」和「什么没用了(放入待删清单)」。 作用是:把「删不删」的权限从各个函数手里收回来,交给一个统一的地方做,系统才能被可靠地控制和编排。
七、在 yt-dlp 里对应哪里(供回溯)
run()契约定义:yt_dlp/postprocessor/common.py(return [], information默认)- 调度器统一收尾:
YoutubeDL.run_pp()(yt_dlp/YoutubeDL.py) - 转码 PP 的「改 info + 返回待删」范例:
yt_dlp/postprocessor/ffmpeg.py - 学习库 → [[Postprocessor-Subsystem]]
八、一句话带走
一个做加工的函数,别再只回一个值、更别自己偷偷删文件。学会双返回契约:返回
(新状态, 待删/可淘汰清单),把「清理」的决定权交回调用方统一做。以后凡是「生产新东西又废弃旧东西」的模块,都用这套——产物走共享状态传递,副作用进统一收尾。
(配套流水线怎么串:[[08后处理流水线范式]])