[Python代码设计] 使用生成器替代回调函数

假设有这么一个场景,需要计算一个非常大的文件的md5值,这个文件非常大,如果一次性读取到内存中,可能会导致内存溢出。同时,我们需要在屏幕中输出计算md5的进度,使得用户有耐心等待这个md5计算完成。

最常规的做法就是在计算md5的同时传一个回调函数,让回调函数在屏幕上输出计算进度:

python 复制代码
import hashlib
from typing import Callable, Any

def compute_md5(

    file: str, callback: Callable[[bytes], Any], *, block_size: int = 256

) -> str:
    md5 = hashlib.md5()
    with open(file, "rb") as f:
        while chunk := f.read(block_size):
            md5.update(chunk)
            callback(chunk)

    return md5.hexdigest()

这个函数在每一次循环中,会读取一部分文件内容然后更新md5值并传入到回调函数中。

对于这个函数的调用者来说,要在屏幕上显示计算md5的进度,还要写以下代码来作为回调函数传入到compute_md5中:

python 复制代码
class UpdateMd5Progress:

    def __init__(self, total_size: int):
        self.total_size = total_size
        self.progress = 0

    def __call__(self, chunk):
        self.progress += len(chunk)
        print(f"{self.progress / self.total_size:.2%}")

完整代码如下:

python 复制代码
import hashlib
import os
from typing import Callable, Any

class UpdateMd5Progress:
    
    def __init__(self, total_size: int):
        self.total_size = total_size
        self.progress = 0

    def __call__(self, chunk):
        self.progress += len(chunk)
        print(f"{self.progress / self.total_size:.2%}")


def compute_md5(
    file: str, 
    callback: Callable[[bytes], Any], 
    *, 
    block_size: int = 256
) -> str:

    md5 = hashlib.md5()
    with open(file, "rb") as f:
        while chunk := f.read(block_size):
            md5.update(chunk)
            callback(chunk)
    return md5.hexdigest()

  
  


def main():
    md5 = compute_md5(
        __file__, 
        UpdateMd5Progress(os.path.getsize(__file__))
    )

    print(md5)

if __name__ == "__main__":
    main()

对于函数调用者来说,这个过程还是略微有些复杂了,需要写一个类记录进度和文件大小。

可以考虑使用生成器来简化函数调用者的代码, 计算md5的代码如下:

python 复制代码
import hashlib
import os


class Md5Calculator:

    def __init__(self, file: str, *, block_size: int = 256):
        self.file = file
        self.block_size = block_size

    def run(self):
        md5 = hashlib.md5()
        with open(self.file, 'rb') as f:
            while chunk:=f.read(self.block_size):
                md5.update(chunk)
                yield chunk
        self.hexdigest = md5.hexdigest()

  


def main():
    md5_calculator = Md5Calculator(__file__)
    total_size = os.path.getsize(__file__)
    progress = 0
    
    for chunk in md5_calculator.run():
        progress += len(chunk)
        print(f"{progress / total_size:.2%}")
    print(md5_calculator.hexdigest)

if __name__ == '__main__':
    main()

在以上代码中,新建了一个Md5Calculator类,来计算md5。以前callback里的内容会通过这个for循环来运行,最后再通过Md5Calculator的hexdigest属性来获取最后的值。

这里为了获取最后md5的方便而没有使用生成器函数,而是直接写了一个类,用户可以通过 Md5Calculator 的 hexdigest属性来获取结果。

如果写为生成器函数,想要获取函数的返回值,需要从StopIteration 这个异常中取出。

代码如下:

python 复制代码
import hashlib
import os
from typing import Generator


def compute_md5(
    file: str, *, block_size: int = 256
) -> Generator[bytes, None, str]:

    md5 = hashlib.md5()
    with open(file, "rb") as f:
        while chunk := f.read(block_size):
            md5.update(chunk)
            yield chunk

    return md5.hexdigest()

  


def main():
    md5_calculator = compute_md5(__file__)
    total_size = os.path.getsize(__file__)
    progress = 0
    while True:
        try:
            chunk = next(md5_calculator)
            progress += len(chunk)
            print(f'{progress / total_size:.2%}')
        except StopIteration as e:
            result = e.value
            break
    print(result)

if __name__ == '__main__':
    main()

这三种的总代码量是差不多的,但是对于用户来说,使用for循环是更为简单直接的选择。

不过如果使用for循环的方式,来让用户处理回调的数据,计算md5时就无法处理用户代码中引起的异常。

接下来可以对比一下,使用回调函数处理异常的方式和使用for循环的方式处理异常的方式:

回调函数:

python 复制代码
import hashlib
from typing import Callable, Any
from traceback import print_exception


def compute_md5(

    file: str, 
    callback: Callable[[bytes], Any], 
    *, 
    block_size: int = 256

) -> str:

    md5 = hashlib.md5()
    with open(file, "rb") as f:
        while chunk := f.read(block_size):
            md5.update(chunk)
        try:
            callback(chunk)
        except Exception as e:
            print_exception(e)

    return md5.hexdigest()

从上面的代码可以看出,compute_md5可以处理用户回调函数中的异常,以保证md5计算完成。

但是使用生成器来代替回调函数,是无法处理用户代码的异常:

python 复制代码
class Md5Calculator:

    def __init__(self, file: str, *, block_size: int = 256):
        self.file = file
        self.block_size = block_size

    def run(self):
        md5 = hashlib.md5()
        with open(self.file, 'rb') as f:
            while chunk:=f.read(self.block_size):
                md5.update(chunk)
                try:
                    yield chunk
                except Exception:
                    print("无法捕获ValueError")
        self.hexdigest = md5.hexdigest()

  
def main():
    md5_calculator = Md5Calculator(__file__)
    total_size = os.path.getsize(__file__)
    progress = 0

    for chunk in md5_calculator.run():
        progress += len(chunk)
        print(f"{progress / total_size:.2%}")
        raise ValueError

    print(md5_calculator.hexdigest)

在这个例子中Md5Calculator.run这个生成器函数,完全捕获不到用户代码中的异常。在大多数情况下,这是合理的,因为异常是由用户代码引起的,计算md5的代码不应该去处理用户代码的异常,但是在稳定性要求非常高的代码中,可能开发者必须处理用户的异常,这时,使用生成器来替代回调函数,则并不合理。

而且,使用生成器来替代回调函数,并不是常见的做法,可能会增加理解的成本,并且写上额外的注释,不过也可以让用户的代码更加流畅,如何处理需要开发者权衡。

相关推荐
在世修行17 小时前
智学文枢:页面切换与跨页数据预填
python·桌面应用·signal·1024程序员节·pyside·跨页跳转·状态预填
2601_9628857218 小时前
如何用 Python 把 K 线重采样成任意周期?
开发语言·python
打工仔折腾 AI18 小时前
在 Ubuntu ARM64 部署 Q1 V3.0 MuJoCo 模型:加载、单步推理与执行器响应解析
linux·运维·人工智能·python·深度学习·ubuntu·性能优化
信誓旦旦的程序猿18 小时前
【Python量化实战宝典 #21】北向资金跟随手册:聪明钱择时信号(Python 可跑)
python·1024程序员节·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
严谨的麻辣烫19 小时前
静态 ISP 代理请求超时怎么排查?Python 与 curl 实战
python·http·requests·1024程序员节·网络排障
KeepRobots20 小时前
11-01 温哥华不回拨:用 60 行 Python 查你的工单系统会不会差一小时(KeepRobots 工程笔记)
python
xiao77mwq20 小时前
你以为 OCR 慢?实测 81.3 s 里全是故障代价:如何把「定格态」消掉,让耗时从 6.5 s 到 13 倍差距显出原形
python
深圳市益普科技有限公司21 小时前
半导体良率管理YMS:良率与设备参数的相关性,先拉哪几组数据
人工智能·python·算法
“AI国潮设计-小江”21 小时前
【AIGC实战】Python+SDXL打造“潮汕英歌舞IP”万能立牌插件,批量生成“发财元宝蛋糕”视觉营销方案(附源码)
开发语言·人工智能·python·prompt·aigc
砚底藏山河1 天前
python量化实操:冷热分层存储
java·开发语言·python·金融