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

简介: 本文探讨了在处理大文件时计算MD5值的实现方法,并展示了如何通过回调函数、生成器和类等方式输出进度。首先介绍了通过回调函数更新进度的方式,然后优化为使用生成器简化调用者代码,最后对比了两种方式的优缺点。虽然生成器使代码更简洁,但在异常处理上不如回调函数灵活。作者通过实例分析,帮助开发者根据需求选择合适的方式。

假设有这么一个场景,需要计算一个非常大的文件的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的代码不应该去处理用户代码的异常,但是在稳定性要求非常高的代码中,可能开发者必须处理用户的异常,这时,使用生成器来替代回调函数,则并不合理。

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


转载来源:https://juejin.cn/post/7483454392980307980

相关文章
|
8月前
|
人工智能 数据安全/隐私保护 Python
小红书图文生成器,小红书AI图文生成工具,python版本软件
Pillow库自动生成符合平台尺寸要求的配图7;3)利用Playwright实现自动化发布流程6。
|
8月前
|
数据采集 NoSQL 调度
当生成器遇上异步IO:Python并发编程的十大实战兵法
本文通过十大实战场景,详解Python中生成器与异步IO的高效结合。从协程演进、背压控制到分布式锁、性能剖析,全面展示如何利用asyncio与生成器构建高并发应用,助你掌握非阻塞编程核心技巧,提升I/O密集型程序性能。
282 0
|
6月前
|
存储 大数据 Unix
Python生成器 vs 迭代器:从内存到代码的深度解析
在Python中,处理大数据或无限序列时,迭代器与生成器可避免内存溢出。迭代器通过`__iter__`和`__next__`手动实现,控制灵活;生成器用`yield`自动实现,代码简洁、内存高效。生成器适合大文件读取、惰性计算等场景,是性能优化的关键工具。
344 2
|
7月前
|
传感器 数据采集 监控
Python生成器与迭代器:从内存优化到协程调度的深度实践
简介:本文深入解析Python迭代器与生成器的原理及应用,涵盖内存优化技巧、底层协议实现、生成器通信机制及异步编程场景。通过实例讲解如何高效处理大文件、构建数据流水线,并对比不同迭代方式的性能特点,助你编写低内存、高效率的Python代码。
299 0
|
6月前
|
大数据 数据处理 数据安全/隐私保护
Python3 迭代器与生成器详解:从入门到实践
简介:本文深入解析Python中处理数据序列的利器——迭代器与生成器。通过通俗语言与实战案例,讲解其核心原理、自定义实现及大数据处理中的高效应用。
315 0
|
8月前
|
存储 API 数据库
自动发短信的软件,批量自动群发短信,手机号电话号生成器【python框架】
这个短信群发系统包含以下核心功能: 随机手机号生成器(支持中国号码) 批量短信发送功能(使用Twilio API)
|
9月前
|
数据采集 搜索推荐 调度
当生成器遇上异步IO:Python并发编程的十大实战兵法
生成器与异步IO是Python并发编程中的两大利器,二者结合可解决诸多复杂问题。本文通过十个真实场景展示其强大功能:从优雅追踪日志文件、API调用流量整形,到实时数据流反压控制、大文件分片处理等,每个场景都体现了生成器按需生成数据与异步IO高效利用I/O的优势。两者配合不仅内存可控、响应及时,还能实现资源隔离与任务独立调度,为高并发系统提供优雅解决方案。这种组合如同乐高积木,虽单个模块简单,但组合后却能构建出复杂高效的系统。
209 0
|
存储 索引 Python
Python生成器、装饰器、异常(2)
【10月更文挑战第16天】
232 1
Python生成器、装饰器、异常(2)
|
大数据 数据处理 开发者
Python中的迭代器和生成器:不仅仅是语法糖####
本文探讨了Python中迭代器和生成器的深层价值,它们不仅简化代码、提升性能,还促进了函数式编程风格。通过具体示例,揭示了这些工具在处理大数据、惰性求值及资源管理等方面的优势。 ####
|
Python
Python生成器、装饰器、异常
【10月更文挑战第15天】
202 2

推荐镜像

更多