手里已经有一个跑得好好的下载器类,现在要拿它去处理另一种来源的数据。你会觉得:都是下载,接口应该差不多,直接复用就行了。结果一分片就报错,而且报的错跟你想排查的方向完全对不上。
现象
用现成的下载器类去处理另一类数据源时,程序在分片阶段全部失败:
TypeError: key must be bytes-like
对应的代码大致是这样:
data = downloader.fetch_chunk(index)
# 内部调用了 self.decrypt(chunk, key),此时 key 为 None
报错点落在解密函数里,但你压根没打算给这类来源做解密——它就是明文。
根因
问题出在被复用类的隐含前提上。
那个下载器原本是为"加密来源"写的,因此它在流程里无条件假设:每个分片都必须经过解密,并且一定存在一个密钥。它的内部实现大致是:
class Downloader:
def fetch_chunk(self, index):
chunk = self._http_get(index)
return self.decrypt(chunk, self.key) # 无条件解密
def decrypt(self, data, key):
return some_cipher.do(data, key) # key 必须是 bytes
现在换了一类来源:它的索引文件是明文的,没有密钥,所以 self.key 是 None。于是 decrypt 拿到 None 当密钥,底层密码库要求"key 必须是 bytes-like 对象",None 不满足,直接抛 TypeError,整段流程崩掉。
关键在于:这个前提从来没写在文档或注释里。类的接口签名是 fetch_chunk(index),看起来跟具体来源无关,但它的实现里藏着一个"所有分片都加密"的假设。复用者只看了接口,没看实现,就把它套到了不满足前提的场景上。
解决
不要改动原类(它服务加密来源时是对的),而是派生子类,覆盖掉那个假设不成立的方法:
class PlainDownloader(Downloader):
def decrypt(self, data, key):
if key is None:
return data # 明文来源:原样返回,不解密
return super().decrypt(data, key)
这样只改动了"解密"这一个环节的行为:有密钥时沿用父类逻辑,没密钥时直接放行。调用方换成 PlainDownloader 即可,其余流程一字不改。
如果不想用继承,也可以给父类加一个"该来源是否加密"的开关参数,在 fetch_chunk 里据此决定要不要调 decrypt。但继承 + 覆盖的好处是不动已验证过的代码,风险更小。
延伸与预防
这条坑的真正教训不是"要会写子类",而是一句更普适的话:复用别人的代码时,最危险的不是接口不匹配,而是那些没写出来的假设。
接口签名会告诉你"怎么调",但不会告诉你"在什么前提下调才成立"。常见的隐含前提包括:
- 数据一定加密 / 一定非空 / 一定是某种编码;
- 字段一定存在、类型一定是 XX;
- 调用顺序有要求(必须先初始化、先建连接);
- 环境里一定装了某个依赖、某个路径一定可写。
要识别它们,最直接的办法是把要复用的方法读一遍,尤其是异常分支和条件判断。凡是看到"无条件执行"的步骤(比如这里无条件解密),都要追问一句:它凭什么敢无条件执行?我的场景满足这个前提吗?
落到工程习惯上:给这类"有前提"的封装补上一行注释或用 assert 显式声明前提,下一个复用者就不会再踩同样的坑——前提写出来了,就不再是"隐含"的。