1557 字
约 5 分钟
3
双返回契约:一个函数「交差」时,同时报告「产物」和「副作用」

⑨ 双返回契约:一个函数「交差」时,同时报告「产物」和「副作用」

目标:学会一个工程习惯——让函数返回时不只是说「我干完了」,而是明确说清楚 **「我产生了什么(产物)」**和 「旧的要扔什么(副作用)」。yt-dlp 的后处理器用 run() 返回一个二元组,把这两件事一次交代清楚,调度器才能统一收尾。

搭配 [[08后处理流水线范式]]。那一篇讲「流水线怎么串」,这一篇讲「每个环节怎么向流水线交代自己的工作」。


一、问题:函数只想返回一个值,容易说不清

上一讲说过,每个后处理器要做加工,比如转码

进来:input.webm
加工:用 ffmpeg 重编码
出去:output.mp4   ← 新文件

做完这件活,它至少有两件事要告诉外部

  1. 得意的事(产物):我产出的是 output.mp4
  2. 扫尾的事(副作用):原来的 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)

问题在哪?

  1. 调度器失去控制:用户可能想要 -k/keepvideo(保留原文件),可你已经偷偷删了,想留都留不回来;
  2. 不可测试:删东西的副作用散落在每个函数里,你想 mock 都不好 mock;
  3. 不可统计 / 不可通知:调度器要知道「删了啥」才能记日志、报进度,偷偷删它永远不知道。

双返回契约的意义,就是把「删不删、什么时候删、要不要保留」这笔大决定的执行权收回调度器统一做。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.pyreturn [], information 默认)
  • 调度器统一收尾:YoutubeDL.run_pp()yt_dlp/YoutubeDL.py
  • 转码 PP 的「改 info + 返回待删」范例:yt_dlp/postprocessor/ffmpeg.py
  • 学习库 → [[Postprocessor-Subsystem]]

八、一句话带走

一个做加工的函数,别再只回一个值、更别自己偷偷删文件。学会双返回契约:返回 (新状态, 待删/可淘汰清单),把「清理」的决定权交回调用方统一做。以后凡是「生产新东西又废弃旧东西」的模块,都用这套——产物走共享状态传递,副作用进统一收尾
(配套流水线怎么串:[[08后处理流水线范式]])

双返回契约:一个函数「交差」时,同时报告「产物」和「副作用」
http://clxhxhhr.top/posts/532/
作者
clxstart
发布于
2026-09-08
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。