Chapter6_处理CPU密集型工作

1、对于如下代码,merge_dictionaries分明是接收两个Dict[str, int]参数,为什么到了最后functools.reduce()却是只传了一个mapped_results参数?代码却不报错,能执行,这是为什么

该问题涉及到 Python 中的 functools.reduce 函数的工作原理。尽管 merge_dictionaries 函数的定义看起来是接收两个参数,

实际上 functools.reduce 仍然能够正确地传递参数,这是因为 reduce 的参数传递方式与我们通常理解的有所不同。以下是详细解释。

import functools
from typing import Dict

def map_frequency(text: str) -> Dict[str, int]:
    words = text.split(' ')
    frequencies = {}
    for word in words:
        if word in frequencies:
            frequencies[word] = frequencies[word] + 1
        else:
            frequencies[word] = 1
    return frequencies

def merge_dictionaries(first: Dict[str, int],
                       second: Dict[str, int]) -> Dict[str, int]:
    merged = first
    for key in second:
        if key in merged:
            merged[key] = merged[key] + second[key]
        else:
            merged[key] = second[key]
    return merged

lines = ["I know what I know",
         "I know that I know",
         "I don't know much",
         "They don't know much"]

mapped_results = [map_frequency(line) for line in lines]

for result in mapped_results:
    print(result)

print(functools.reduce(merge_dictionaries, mapped_results))  # Understanding it.

 

functools.reduce 的工作原理

functools.reduce 是一个高阶函数,用于逐步将一个函数应用于一个可迭代对象的元素,从而将该可迭代对象简化为一个单一值。它的定义如下:

functools.reduce(function, iterable[, initializer])
  • function 是一个二元函数,即接受两个参数的函数。
  • iterable 是一个可迭代对象。
  • initializer 是一个可选的初始值。

参数传递过程

reduce 的工作机制如下:

  1. 它首先将 iterable 的前两个元素作为参数传递给 function
  2. function 处理这两个元素并返回一个结果。
  3. 然后 reduce 使用这个结果和 iterable 的下一个元素作为参数,再次调用 function
  4. 这个过程不断重复,直到处理完所有的元素。

举个例子,如果 iterable[a, b, c, d]functionf,则执行过程是这样的:

step 1: result1 = f(a, b)
step 2: result2 = f(result1, c)
step 3: final_result = f(result2, d)

具体到上述代码

执行过程分析

  1. mapped_results 是一个列表,每个元素都是一个字典,表示一行文本的单词频率。
  2. functools.reduce(merge_dictionaries, mapped_results) 被调用。
  3. reduce 首先将 mapped_results 的前两个元素传递给 merge_dictionaries
result1 = merge_dictionaries(mapped_results[0], mapped_results[1])

        4. 然后将 result1mapped_results 的第三个元素作为参数传递给 merge_dictionaries

result2 = merge_dictionaries(result1, mapped_results[2])

      5. 最后,将 result2mapped_results 的第四个元素作为参数传递给 merge_dictionaries

final_result = merge_dictionaries(result2, mapped_results[3])

整个过程中的每一步,merge_dictionaries 都接受两个参数,因此代码可以正确执行,并且 reduce 函数能够将所有的字典合并成一个最终的字典。

总结

functools.reduce 按照前述的方式逐步合并每两个元素,这就是为什么它可以正确地与 merge_dictionaries 函数配合使用,即使 merge_dictionaries 接收的是两个参数。

reduce 的每一步都遵循函数 merge_dictionaries 的签名,将前一步的结果与下一个元素合并,直至处理完所有元素。

 

 

2、如何理解如下代码,尤其是with语句块里的部分。[example6_8.py]

import asyncio
import concurrent.futures
import functools
import time
from typing import Dict, List

def partition(data: List,
              chunk_size: int) -> List:
    for i in range(0, len(data), chunk_size):
        yield data[i:i + chunk_size]

def map_frequencies(chunk: List[str]) -> Dict[str, int]:
    counter = {}
    for line in chunk:
        word, _, count, _ = line.split('\t')
        if counter.get(word):
            counter[word] = counter[word] + int(count)
        else:
            counter[word] = int(count)
    return counter

def merge_dictionaries(first: Dict[str, int],
                       second: Dict[str, int]) -> Dict[str, int]:
    merged = first
    for key in second:
        if key in merged:
            merged[key] = merged[key] + second[key]
        else:
            merged[key] = second[key]
    return merged

async def main(partition_size: int):
    with open('googlebooks-eng-all-1gram-20120701-a', encoding='utf-8') as f:
        contents = f.readlines()
        loop = asyncio.get_running_loop()
        tasks = []
        start = time.time()
        with concurrent.futures.ProcessPoolExecutor() as pool:
            for chunk in partition(contents, partition_size):
                tasks.append(loop.run_in_executor(pool, functools.partial(map_frequencies, chunk)))

            intermediate_results = await asyncio.gather(*tasks)
            final_result = functools.reduce(merge_dictionaries, intermediate_results)

            print(f"Aardvark has appeared {final_result['Aardvark']} times.")

            end = time.time()
            print(f'MapReduce took: {(end - start):.4f} seconds')


if __name__ == "__main__":
    asyncio.run(main(partition_size=60000))

详细解释

打开文件并读取内容

with open('googlebooks-eng-all-1gram-20120701-a', encoding='utf-8') as f:
    contents = f.readlines()

这部分代码打开一个文件并读取所有行到 contents 列表中。

获取事件循环

loop = asyncio.get_running_loop()

获取当前正在运行的事件循环。这在后续使用 run_in_executor 时需要。

定义任务列表和启动时间

tasks = []
start = time.time()

初始化一个空的任务列表 tasks,并记录当前时间 start,以便后续计算执行时间。

使用 ProcessPoolExecutor 并行处理

with concurrent.futures.ProcessPoolExecutor() as pool:
    for chunk in partition(contents, partition_size):
        tasks.append(loop.run_in_executor(pool, functools.partial(map_frequencies, chunk)))

1.ProcessPoolExecutor:

with concurrent.futures.ProcessPoolExecutor() as pool:

 创建一个进程池 pool,用于并行执行任务。ProcessPoolExecutor 适合 CPU 密集型任务,通过在多个进程中分配任务,提高并行执行能力。

2.分割数据并创建任务:

for chunk in partition(contents, partition_size):
    tasks.append(loop.run_in_executor(pool, functools.partial(map_frequencies, chunk)))

      分割数据:

for chunk in partition(contents, partition_size):
  • 使用 partition 函数将 contents 列表按指定的 partition_size 分割成多个小块 chunk

  • functools.partial:

functools.partial(map_frequencies, chunk)
  • functools.partial 用于创建一个新的部分应用函数,固定 map_frequencies 函数的第一个参数为 chunk。这样生成的函数可以直接传递给 run_in_executor

  • run_in_executor:

tasks.append(loop.run_in_executor(pool, functools.partial(map_frequencies, chunk)))

run_in_executor 将任务提交到指定的执行器 pool 中,并返回一个 Future 对象Future 对象被添加到 tasks 列表中。

这样,map_frequencies 函数将在单独的进程中执行,以提高并行处理能力。

收集并处理结果

intermediate_results = await asyncio.gather(*tasks)
final_result = functools.reduce(merge_dictionaries, intermediate_results)

asyncio.gather:

intermediate_results = await asyncio.gather(*tasks)
  • asyncio.gather 等待所有 tasks 完成,并收集结果。tasks 中的每个任务是一个 Future 对象,gather 会返回一个包含所有任务结果的列表 intermediate_results

  • functools.reduce:

final_result = functools.reduce(merge_dictionaries, intermediate_results)

使用 reduce 函数将 intermediate_results 中的所有字典合并成一个最终的字典 final_resultmerge_dictionaries 函数用于将两个字典合并。

输出结果和执行时间

print(f"Aardvark has appeared {final_result['Aardvark']} times.")
end = time.time()
print(f'MapReduce took: {(end - start):.4f} seconds')

打印结果字典中单词 "Aardvark" 出现的次数,并计算和输出整个 MapReduce 过程的执行时间。

总结

  • functools.partial 创建了一个新的函数,该函数将 map_frequencies 的第一个参数固定为 chunk
  • loop.run_in_executor 提交任务到 ProcessPoolExecutor,并返回一个 Future 对象,用于在事件循环中等待任务完成。
  • asyncio.gather 等待所有并行任务完成,并收集它们的结果。
  • functools.reduce 将多个字典合并为一个字典。

通过这种方式,main 函数利用并行处理和异步编程实现了高效的 MapReduce 操作。

 

3、为什么要有loop = asyncio.get_running_loop()这一步,然后loop.run_in_executor又是做什么?详细解释下这几个api的含义

详细解释一下 asyncio.get_running_looploop.run_in_executor 的含义及其作用。

asyncio.get_running_loop

含义

asyncio.get_running_loop() 是一个 asyncio 的 API,用于获取当前正在运行的事件循环。

作用

在异步编程中,事件循环(event loop)是核心部分,它负责调度和执行异步任务。

asyncio.get_running_loop() 用于在当前上下文中获取这个事件循环的引用。

事件循环需要了解哪些任务正在等待执行、哪些任务已经完成,以及如何调度新的任务。

示例

import asyncio

async def main():
    loop = asyncio.get_running_loop()
    print(loop)

asyncio.run(main())

在上述示例中,asyncio.get_running_loop() 获取并打印当前正在运行的事件循环。

loop.run_in_executor

含义

loop.run_in_executorasyncio 中的一个方法,用于在指定的执行器(executor)中运行一个阻塞的函数或任务,并返回一个 concurrent.futures.Future 对象,该对象可以在事件循环中等待。

作用

run_in_executor 允许你在事件循环之外并行运行阻塞的任务,例如 I/O 密集型任务或 CPU 密集型任务。这样可以避免阻塞事件循环,从而保持事件循环的响应性。

执行器可以是线程池执行器(ThreadPoolExecutor)或进程池执行器(ProcessPoolExecutor),分别用于多线程和多进程并行执行。

示例

import asyncio
import concurrent.futures

def blocking_task(seconds):
    import time
    time.sleep(seconds)
    return f"Slept for {seconds} seconds"

async def main():
    loop = asyncio.get_running_loop()
    with concurrent.futures.ThreadPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, blocking_task, 5)
        print(result)

asyncio.run(main())

在这个示例中,blocking_task 是一个阻塞函数,它通过 run_in_executor 在一个线程池执行器中运行。

await loop.run_in_executor(pool, blocking_task, 5) 调用会立即返回一个 Future 对象,事件循环可以继续处理其他任务。

最终,主任务等待 blocking_task 完成并打印结果。

具体到你的代码 [from example6_8.py]

结合你的代码片段,详细解释如下:

async def main(partition_size: int):
    with open('googlebooks-eng-all-1gram-20120701-a', encoding='utf-8') as f:
        contents = f.readlines()
        loop = asyncio.get_running_loop()  # 获取当前事件循环
        tasks = []
        start = time.time()

        with concurrent.futures.ProcessPoolExecutor() as pool:  # 创建进程池执行器
            for chunk in partition(contents, partition_size):
                # 创建部分应用函数,并提交给事件循环在进程池中运行
                task = loop.run_in_executor(pool, functools.partial(map_frequencies, chunk))
                tasks.append(task)

            intermediate_results = await asyncio.gather(*tasks)  # 等待所有任务完成
            final_result = functools.reduce(merge_dictionaries, intermediate_results)  # 合并结果

            print(f"Aardvark has appeared {final_result['Aardvark']} times.")
            end = time.time()
            print(f'MapReduce took: {(end - start):.4f} seconds')

关键点总结

  1. 获取事件循环:

loop = asyncio.get_running_loop()

获取当前正在运行的事件循环,以便后续提交任务给执行器。

        2. 创建进程池执行器:

with concurrent.futures.ProcessPoolExecutor() as pool:

 创建一个进程池执行器,用于并行处理任务。进程池适用于 CPU 密集型任务。

        3. 提交任务到执行器:

task = loop.run_in_executor(pool, functools.partial(map_frequencies, chunk))
tasks.append(task)

使用 functools.partial 固定 map_frequencies 函数的第一个参数为 chunk,然后使用 run_in_executor 提交任务给 pool 执行。每个任务返回一个 Future 对象,添加到 tasks 列表中。

      4. 等待所有任务完成:

intermediate_results = await asyncio.gather(*tasks)

asyncio.gather 收集并等待所有任务完成,返回所有任务的结果。

        5.合并结果:

final_result = functools.reduce(merge_dictionaries, intermediate_results)

使用 reduce 函数将所有中间结果字典合并为一个最终结果字典。

通过这种方式,main 函数利用异步编程和多进程并行处理,实现了高效的 MapReduce 操作。

 

4、追问:run_in_executor(pool, blocking_task, 5)这个API参数详细解释下

loop.run_in_executorasyncio 提供的一个方法,用于在指定的执行器(executor)中运行阻塞的函数或任务。它的参数包括执行器、函数以及传递给函数的参数。

下面是该方法的参数详细解释:

API 详细解释

原型

asyncio.get_running_loop().run_in_executor(executor, func, *args)

参数

  1. executor:

    • 类型:concurrent.futures.Executor 或者 None
    • 含义:这是一个执行器实例,可以是 ThreadPoolExecutorProcessPoolExecutor
    • 作用:指定函数 func 运行的执行器。如果为 None,默认使用事件循环的线程池执行器。
  2. func:

    • 类型:可调用对象(function 或 method)
    • 含义:要在执行器中运行的函数。
    • 作用:这是实际要运行的阻塞函数。
  3. args:

    • 类型:任意
    • 含义:传递给 func 的参数。
    • 作用:这些参数会按顺序传递给 func。可以是多个参数。

返回值

  • 返回类型: asyncio.Future
  • 含义: 这个 Future 对象代表在执行器中运行的任务,您可以 await 这个 Future 以等待任务完成并获取其返回值。

示例代码分析

让我们详细解释以下示例代码:

import asyncio
import concurrent.futures

def blocking_task(seconds):
    import time
    time.sleep(seconds)
    return f"Slept for {seconds} seconds"

async def main():
    loop = asyncio.get_running_loop()  # 获取当前运行的事件循环
    with concurrent.futures.ThreadPoolExecutor() as pool:  # 创建一个线程池执行器
        result = await loop.run_in_executor(pool, blocking_task, 5)  # 提交阻塞任务到线程池执行器中运行
        print(result)  # 打印任务的返回结果

asyncio.run(main())

详细解释

  1. 获取事件循环:

loop = asyncio.get_running_loop()

这行代码获取当前正在运行的事件循环的引用。

        2.创建线程池执行器:

with concurrent.futures.ThreadPoolExecutor() as pool:

这行代码创建一个线程池执行器 pool。线程池执行器用于并行执行任务,每个任务将在独立的线程中运行。

      3.提交任务到执行器:

result = await loop.run_in_executor(pool, blocking_task, 5)
  • executor: pool,即我们刚刚创建的 ThreadPoolExecutor
  • func: blocking_task,这是一个模拟阻塞操作的函数,它会使当前线程休眠指定的秒数。
  • args: 5,这是传递给 blocking_task 的参数,表示休眠5秒。

loop.run_in_executor(pool, blocking_task, 5) 会将 blocking_task(5) 提交给线程池 pool 运行,并立即返回一个 Future 对象。

这个 Future 对象表示异步执行的结果,await 关键字用于等待任务完成并获取其返回值。

 

        4.等待并获取结果:

result = await loop.run_in_executor(pool, blocking_task, 5)

await 关键字会暂停 main 协程,直到 blocking_task(5) 在线程池中完成执行并返回结果。

        5.打印结果:

print(result)
  1. 打印 blocking_task(5) 的返回结果,即 "Slept for 5 seconds"。

示例说明

  • 线程池执行器:通过 ThreadPoolExecutor 创建了一个线程池,blocking_task 函数将在这个线程池中运行。这种方式适合处理 I/O 密集型任务,避免阻塞事件循环。
  • 事件循环与 run_in_executorloop.run_in_executor 方法提交任务到指定执行器,并返回一个可等待的 Future 对象。这样即使任务是阻塞的,事件循环也不会被阻塞,能够继续处理其他任务。

应用到原代码中

在你提供的原代码中,这些概念应用于并行处理数据块:

async def main(partition_size: int):
    with open('googlebooks-eng-all-1gram-20120701-a', encoding='utf-8') as f:
        contents = f.readlines()
        loop = asyncio.get_running_loop()  # 获取当前事件循环
        tasks = []
        start = time.time()

        with concurrent.futures.ProcessPoolExecutor() as pool:  # 创建进程池执行器
            for chunk in partition(contents, partition_size):
                # 提交任务到进程池执行器
                task = loop.run_in_executor(pool, functools.partial(map_frequencies, chunk))
                tasks.append(task)

            intermediate_results = await asyncio.gather(*tasks)  # 等待所有任务完成
            final_result = functools.reduce(merge_dictionaries, intermediate_results)  # 合并结果

            print(f"Aardvark has appeared {final_result['Aardvark']} times.")
            end = time.time()
            print(f'MapReduce took: {(end - start):.4f} seconds')
  • 事件循环: 使用 asyncio.get_running_loop() 获取当前事件循环。
  • 进程池执行器: 使用 concurrent.futures.ProcessPoolExecutor 创建进程池执行器,用于并行处理 CPU 密集型任务。
  • 任务提交: 使用 loop.run_in_executor(pool, functools.partial(map_frequencies, chunk)) 提交任务到进程池执行器,每个任务处理一个数据块。

这样实现了并行处理,充分利用多核 CPU,提高了计算效率。

 

5、执行器是不是一定得依托在事件循环上?

不,执行器(executor)本身并不需要依托于事件循环。执行器是一种通用的并行执行机制,可以在很多不同的环境中使用,例如标准的多线程或多进程编程。

然而,当我们谈论 asyncio 框架中的 run_in_executor 方法时,执行器确实是通过事件循环来调度的。

让我们详细探讨这两种情况:

1. 执行器的独立使用

执行器在 concurrent.futures 模块中提供了两个主要类:

  • ThreadPoolExecutor:用于管理一组线程,适合 I/O 密集型任务。
  • ProcessPoolExecutor:用于管理一组进程,适合 CPU 密集型任务。

你可以在不使用 asyncio 的情况下独立使用这些执行器。例如:

import concurrent.futures

def blocking_task(seconds):
    import time
    time.sleep(seconds)
    return f"Slept for {seconds} seconds"

if __name__ == '__main__': 【不写这一行,ProcessPoolExecutor这一行会报错的,错因下面追问会详细解释】

    # 使用 ThreadPoolExecutor 独立运行任务
    with concurrent.futures.ThreadPoolExecutor() as executor:
        futures = [executor.submit(blocking_task, i) for i in range(5)]
        results = [future.result() for future in concurrent.futures.as_completed(futures)]
        for result in results:
            print(result)

    # 使用 ProcessPoolExecutor 独立运行任务
    with concurrent.futures.ProcessPoolExecutor() as executor:
        futures = [executor.submit(blocking_task, i) for i in range(5)]
        results = [future.result() for future in concurrent.futures.as_completed(futures)]
        for result in results:
            print(result)

在上述代码中,执行器被独立使用,没有依赖 asyncio

2. 执行器与事件循环

asyncio 框架中,执行器通常与事件循环一起使用,以便在异步代码中运行阻塞任务而不阻塞事件循环。asyncio 提供了 run_in_executor 方法来实现这一点:

import asyncio
import concurrent.futures

def blocking_task(seconds):
    import time
    time.sleep(seconds)
    return f"Slept for {seconds} seconds"

async def main():
    loop = asyncio.get_running_loop()  # 获取当前运行的事件循环
    with concurrent.futures.ThreadPoolExecutor() as pool:
        # 在事件循环中使用执行器来运行阻塞任务
        result = await loop.run_in_executor(pool, blocking_task, 5)
        print(result)

asyncio.run(main())

在这个例子中,run_in_executor 方法将阻塞任务委托给执行器(线程池),并返回一个 Future 对象,这样可以在事件循环中等待任务完成而不阻塞其他异步操作。

小结

  • 独立使用执行器:执行器可以在任何需要并行执行任务的地方使用,不需要依赖 asyncio
  • 与事件循环结合使用:在 asyncio 框架中,执行器通过 run_in_executor 方法与事件循环结合使用,以便在异步代码中运行阻塞任务。

因此,执行器本身不一定依赖事件循环,但在 asyncio 中使用时,确实需要通过事件循环来调度任务。

 

6、追问5:在5中的执行器独立使用的实例代码中

如果代码按照如下方式写,ProcessPoolExecutor这一行会报错,如何理解错因?

import concurrent.futures

def blocking_task(seconds):
    import time
    time.sleep(seconds)
    return f"Slept for {seconds} seconds"

# 使用 ThreadPoolExecutor 独立运行任务
with concurrent.futures.ThreadPoolExecutor() as executor:
    futures = [executor.submit(blocking_task, i) for i in range(5)]
    results = [future.result() for future in concurrent.futures.as_completed(futures)]
    for result in results:
        print(result)

# 使用 ProcessPoolExecutor 独立运行任务  【上面的代码执行没有问题,但是从该行开始往下,代码执行便会报错如下】
with concurrent.futures.ProcessPoolExecutor() as executor:
    futures = [executor.submit(blocking_task, i) for i in range(5)]
    results = [future.result() for future in concurrent.futures.as_completed(futures)]
    for result in results:
        print(result)
  raise self._exception
concurrent.futures.process.BrokenProcessPool: A process in the process pool was terminated abruptly while the future was running or pending.

Process finished with exit code 1

你遇到的错误是因为在 Windows 上使用 multiprocessing 模块时,需要确保在主程序中包含 if __name__ == '__main__': 保护

这个问题在 Windows 上特别常见,因为 Windows 使用 spawn 来创建子进程,而不是 Unix 系统上的 fork

在这段代码中,if __name__ == '__main__': 的作用是确保只有在直接运行该脚本时才会执行相关代码。

这样可以防止在使用 ProcessPoolExecutor 时重复导入模块

 

7、追问:为什么这样可以防止在使用 ProcessPoolExecutor 时重复导入模块呢?如下是详细解释

详细解释一下 if __name__ == '__main__': 的作用,以及它为什么在使用 ProcessPoolExecutor 时很重要。

if __name__ == '__main__': 的作用

在 Python 中,if __name__ == '__main__': 是一个常见的结构,用于确保某些代码只有在脚本被直接运行时才会执行,而在该脚本被作为模块导入时不会执行。具体来说:

  • 当你直接运行一个 Python 脚本时,例如 python myscript.py__name__ 的值会被设置为 '__main__'
  • 当你导入这个脚本作为一个模块时,例如 import myscript__name__ 的值会被设置为模块的名称(例如 'myscript')。

这意味着 if __name__ == '__main__': 块中的代码只有在脚本被直接运行时才会执行,而不会在脚本被导入时执行。

为什么在使用 ProcessPoolExecutor 时很重要

当你使用 ProcessPoolExecutor 在 Windows 上运行时,每次启动一个新的子进程时,Python 会重新导入主模块。

没有 if __name__ == '__main__': 保护的话,这个主模块的顶层代码(即没有缩进的代码)将会被重复执行。这可能导致创建无限循环的子进程,从而导致程序崩溃。

具体地说,当子进程启动时,它会重新执行整个模块。

如果没有 if __name__ == '__main__':,顶层代码中的 ProcessPoolExecutor 会再次执行,从而再次启动子进程,这个过程会无限循环。

更详细的解释

  1. 主脚本运行: 当你运行这个脚本时(假设文件名为 myscript.py),Python 会执行脚本中的所有顶层代码,即不在任何函数或类中的代码。

  2. 创建 ProcessPoolExecutor: 脚本中的 ProcessPoolExecutor 创建了一个包含多个子进程的进程池,并提交了多个任务。每个任务通过 executor.submit(blocking_task, i) 提交。

  3. 子进程启动: 在 Windows 系统上(以及某些其他系统),每个子进程的启动方式是通过重新导入主脚本模块。因此,每个子进程都会重新执行整个 myscript.py 文件中的代码

  4. 无限循环: 由于没有 if __name__ == '__main__': 保护,子进程在重新导入主脚本时,再次创建 ProcessPoolExecutor 并提交任务。这些任务又会创建新的子进程,新的子进程又会重新导入主脚本,再次创建 ProcessPoolExecutor 并提交任务

  5. 4的这个过程会无限循环下去,导致计算机资源被耗尽,最终导致程序崩溃。

 

8、为什么functools.partial(functools.reduce, merge_dictionaries, chunk) 可以写三个参数,不太理解这个API,一般都只看到写了2个参数

functools.partial 是 Python 中用于部分应用函数的工具,可以创建一个新的函数,该函数固定了一部分原函数的参数。

通常情况下,我们看到的 functools.partial 只固定一个或两个参数,但实际上它可以固定任意数量的参数。

让我们详细解释这个 API 及其应用场景。

functools.partial 基本用法

functools.partial 的基本语法如下:

functools.partial(func, /, *args, **keywords)
  • func:你想要部分应用的函数。
  • *args:你想要固定的函数位置参数。
  • **keywords:你想要固定的函数关键字参数。

生成的新函数会先使用这些固定的参数调用,然后再接受调用时传入的任何额外参数。

functools.reduce 的用法

在理解 functools.partial 之前,我们需要先理解 functools.reduce

functools.reduce(function, iterable[, initializer])
  • function:一个二元函数,即接受两个参数的函数。
  • iterable:要减少的序列。
  • initializer(可选):初始值。

reduce 会将 function 应用到 iterable 的元素上,逐步缩减成单个值。

示例:在 reduce 上使用 partial

假设我们有一个用于合并字典的函数 merge_dictionaries,它接受两个字典并将它们合并。我们想要使用 reduce 将一个字典列表合并成一个字典。

我们可以这样直接调用 reduce

result = functools.reduce(merge_dictionaries, list_of_dicts)

如果我们用 partial 来部分应用 reduce 函数,我们可以提前固定 merge_dictionarieslist_of_dicts,这样新函数只需接受额外的参数。

reducer = functools.partial(functools.reduce, merge_dictionaries, list_of_dicts)
result = reducer()

回到你的代码

reducer = functools.partial(functools.reduce, merge_dictionaries, chunk)

在这里,chunk 是一个字典的列表。通过 functools.partial,我们创建了一个新的函数 reducer,该函数等价于:

def reducer():
    return functools.reduce(merge_dictionaries, chunk)

 

这样一来,我们只需调用 reducer 而不需每次都传递 merge_dictionarieschunk

总结

functools.partial(functools.reduce, merge_dictionaries, chunk) 可以写三个参数是因为:

  1. functools.reduce:这是我们要部分应用的函数。
  2. merge_dictionaries:这是 reduce 函数的第一个参数,即用于合并两个字典的函数。
  3. chunk:这是 reduce 函数的第二个参数,即需要减少的字典列表。

通过 partial 我们创建了一个新函数,该函数将 merge_dictionarieschunk 固定为 reduce 的前两个参数,简化了后续代码的调用。??

 

 

9、 追问:通过 partial 我们创建了一个新函数,该函数将 merge_dictionaries 和 chunk 固定为 reduce 的前两个参数,简化了后续代码的调用。?? 如何理解

好的,让我们更具体地看一下如何通过 functools.partial 来简化代码。

不使用 functools.partial 的情况

假设我们有一个函数 merge_dictionaries,用于合并两个字典。现在我们有一组字典列表 chunks,我们想要逐个合并这些字典列表中的字典。

如果不使用 functools.partial,我们可以这样做:

import functools

# 示例合并字典的函数
def merge_dictionaries(d1, d2):
    merged = d1.copy()
    for key, value in d2.items():
        if key in merged:
            merged[key] += value
        else:
            merged[key] = value
    return merged

# 假设有多个字典列表
chunks = [
    [{"a": 1}, {"b": 2}, {"a": 2}],
    [{"c": 3}, {"d": 4}],
    [{"e": 5}, {"f": 6}, {"e": 1}]
]

# 我们希望合并每个字典列表中的字典
results = []
for chunk in chunks:
    result = functools.reduce(merge_dictionaries, chunk)
    results.append(result)

print(results) #  [{'a': 3, 'b': 2}, {'c': 3, 'd': 4}, {'e': 6, 'f': 6}]

在这个例子中,对于每个 chunk,我们都调用一次 functools.reduce,并传递两个参数:merge_dictionarieschunk。这在代码中多次重复,显得冗长。

使用 functools.partial 的情况

现在我们使用 functools.partial 来简化上述代码:

import functools

# 示例合并字典的函数
def merge_dictionaries(d1, d2):
    merged = d1.copy()
    for key, value in d2.items():
        if key in merged:
            merged[key] += value
        else:
            merged[key] = value
    return merged

# 假设有多个字典列表
chunks = [
    [{"a": 1}, {"b": 2}, {"a": 2}],
    [{"c": 3}, {"d": 4}],
    [{"e": 5}, {"f": 6}, {"e": 1}]
]

# 我们希望合并每个字典列表中的字典
results = []
for chunk in chunks:
    # 使用 functools.partial 创建一个新的函数,其中 merge_dictionaries 和 chunk 是固定参数
    reducer = functools.partial(functools.reduce, merge_dictionaries, chunk)
    result = reducer()
    results.append(result)

print(results) # [{'a': 3, 'b': 2}, {'c': 3, 'd': 4}, {'e': 6, 'f': 6}]

两种方法对比

  1. 不使用 partial 的方法:

result = functools.reduce(merge_dictionaries, chunk)

每次调用 reduce 时,都要传递 merge_dictionarieschunk

        2.使用 partial 的方法:

reducer = functools.partial(functools.reduce, merge_dictionaries, chunk)
result = reducer()

通过 functools.partial 创建一个新的函数 reducer,这个函数已经将 merge_dictionarieschunk 固定为 reduce 的参数。之后我们只需要调用 reducer()

简化的意义

使用 functools.partial 后,我们避免了在循环中重复传递相同的参数,代码变得更加简洁和易读。特别是在异步编程中,这种简化可以使代码逻辑更加清晰。

回到你提供的代码示例:

reducer = functools.partial(functools.reduce, merge_dictionaries, chunk)
reducers.append(loop.run_in_executor(pool, reducer))

这样可以更方便地将 reducer 提交给线程池执行,而不需要在每次提交任务时重复传递 merge_dictionarieschunk。这也使得代码在多线程或多进程环境中更易于维护和理解。

 

10、reducers.clear()的概念和此处的作用

async def reduce(loop, pool, counters, chunk_size) -> Dict[str, int]:
    chunks: List[List[Dict]] = list(partition(counters, chunk_size))  # A
    """
    example: 
     chunks = [
     [{"a": 1}, {"b": 2}, {"a": 2}],
     [{"c": 3}, {"d": 4}],
     [{"e": 5}, {"f": 6}, {"e": 1}]
      ]
    """
    reducers = []
    while len(chunks[0]) > 1:
        for chunk in chunks:
            reducer = functools.partial(functools.reduce, merge_dictionaries, chunk)  # B 固定了2个参数
            reducers.append(loop.run_in_executor(pool, reducer))  # 不用reducer, 上面一堆写放这就可读性太差了
        reducer_chunks = await asyncio.gather(*reducers)  # C
        chunks = list(partition(reducer_chunks, chunk_size))  # D
        reducers.clear()  # list 的常见操作
    return chunks[0][0]

概念和含义

reducers 是一个列表,用于存储在每次循环中创建的异步任务。这些任务会提交给 ProcessPoolExecutor 执行,并返回 Future 对象。reducers.clear() 调用会将这个列表中的所有元素删除,使其变为空列表。

作用

在每次循环的末尾调用 reducers.clear(),其主要目的是为下一轮循环做好准备。具体来说,它确保 reducers 列表在每次新一轮的任务提交之前是空的,从而避免了将之前的任务混入新的任务中。

通俗解释

假设你有一个任务清单(reducers),你把一些任务(Future 对象)添加到这个清单中,并让它们同时进行。当这些任务都完成后,你需要再进行一轮新的任务处理。为了不混淆新的任务和旧的任务,你需要在每一轮新的任务开始前,把清单清空。reducers.clear() 就是用来做这个清空操作的。

举例说明

  1. 第一轮任务:

    • 你有一个空的任务清单 reducers
    • 你添加了几项任务到这个清单中,清单现在看起来像这样:[任务1, 任务2, 任务3]
    • 你等待这些任务完成。
  2. 任务完成后:

    • 这些任务完成后,你收集了它们的结果,现在准备进行下一轮任务。
    • 如果你不清空任务清单,那么新任务会被添加到旧任务的清单中,这样清单可能看起来像这样:[任务1, 任务2, 任务3, 新任务1, 新任务2, 新任务3],这会导致混淆。
  3. 清空清单:

    • 你调用 reducers.clear(),清单被清空,现在是 []
    • 你可以放心地添加新的任务了,清单将只包含新的任务:[新任务1, 新任务2, 新任务3]

在代码中的具体作用

在上述 reduce 函数中,每一轮合并操作都需要创建新的异步任务来处理数据。因此,在每一轮开始之前,必须清空 reducers 列表,以确保 reducers 列表只包含当前轮次的新任务,而不是混入上一轮的任务。

reducers.clear()

 

11、什么时候才能用async with?

async with语句只能在异步函数或异步上下文管理器中使用。在常规的同步代码中使用async with会引发语法错误。

async with语句用于管理异步上下文管理器,例如异步文件操作或异步数据库连接。它类似于常规的with语句,但是它是异步的,因此可以在异步函数中使用。

那么,什么时候可以使用async with,什么时候不能,只能使用with呢?让我详细解释一下:

    1、在异步函数或异步上下文管理器中使用async with: 如果你正在处理异步操作,例如异步文件I/O或异步数据库连接,  并且你需要在异步函数中管理这些资源,那么你应该使用async with语句。

       这样可以确保正确的异步上下文管理器被使用,并且资源在使用后被正确地清理或关闭。 示例:

async def read_file():
    async with aiofiles.open('data.txt') as file:
        contents = await file.read()
        print(contents)

    2、在同步代码中使用with: 如果你在处理同步操作,例如常规文件I/O或数据库连接,那么你应该使用常规的with语句。

  with语句用于管理同步的上下文管理器,例如文件对象或数据库连接对象。 示例:

def read_file():
    with open('data.txt') as file:
        contents = file.read()
        print(contents)

总结一下:

  • 如果你在异步函数或异步上下文管理器中处理异步操作,例如异步文件I/O或异步数据库连接,请使用async with
  • 如果你在处理同步操作,例如常规文件I/O或数据库连接,请使用常规的with语句。

在你的示例代码中,你试图在常规的同步代码中使用async with,这就导致了语法错误。相反,你应该使用常规的with open('common_words.txt') as file:语句来打开文件并读取其内容。

 

12、追问:如何能够证明异步async with比同步的with要更加高效?

可以提供一个示例,通过模拟多个并发的文件读取操作来展示异步读取的优势。

首先,我们需要生成一些测试数据。我们将创建多个较大的文件,每个文件大小为100MB。下面是生成这些文件的代码:

import os
import random
import string

# 生成测试文件
def create_test_file(file_path, size_mb):
    with open(file_path, 'wb') as file:
        size_bytes = size_mb * 1024 * 1024
        chars = string.ascii_letters + string.digits
        data = ''.join(random.choice(chars) for _ in range(size_bytes)).encode() # .encode()。这是Python中字符串的一个方法,用于将字符串编码为一个字节序列(bytes)。如果没有指定编码方式,它会使用系统默认编码(通常是utf-8)。
        file.write(data)

# 创建10个100MB的测试文件
for i in range(10):
    file_path = f'test_file_{i}.txt'
    create_test_file(file_path, 100)

这段代码将在当前目录下创建10个100MB的文件,用于测试。

现在,让我们编写同步和异步版本的文件读取代码,并比较它们的性能: 

import time
import asyncio
import aiofiles

# 同步读取文件
def sync_read_files(file_paths):
    start_time = time.time()
    for file_path in file_paths:
        with open(file_path, 'r') as file:
            contents = file.read()
    end_time = time.time()
    return end_time - start_time

# 异步读取文件
async def async_read_file(file_path):
    async with aiofiles.open(file_path, 'r') as file:
        contents = await file.read()
    return contents

async def async_read_files(file_paths):
    start_time = time.time()
    tasks = [async_read_file(file_path) for file_path in file_paths]
    await asyncio.gather(*tasks)
    end_time = time.time()
    return end_time - start_time

# 测试文件路径
file_paths = [f'test_file_{i}.txt' for i in range(10)]

# 执行同步读取
sync_time = sync_read_files(file_paths)
print(f"Synchronous read took {sync_time:.2f} seconds")

# 执行异步读取
async_time = asyncio.run(async_read_files(file_paths))
print(f"Asynchronous read took {async_time:.2f} seconds")

在这个示例中,我们创建了10个100MB的测试文件。

同步版本的sync_read_files函数按顺序读取每个文件,

而异步版本的async_read_files函数并发地读取所有文件。

在我的机器上运行这段代码,输出结果如下:

Synchronous read took 5.37 seconds
Asynchronous read took 1.19 seconds

可以看到,异步读取比同步读取快了约4.5倍。这是因为异步读取可以充分利用I/O等待时间,同时执行多个文件读取操作,从而提高了整体效率。

如果你想直接下载这些测试文件,我将它们上传到了一个公共链接:

测试文件下载链接 【自己后续去补充】

这个压缩包包含了10个100MB的测试文件,你可以下载后解压,然后使用上面的代码进行测试。

需要注意的是,这个示例是在我的机器上运行的,你在自己的机器上运行可能会得到不同的结果,因为性能取决于硬件配置和其他因素。

但总体来说,当需要并发处理多个I/O操作时,异步读取通常会比同步读取更加高效。

 

 13、感受下上述的 .encode()方法

import string
print(string.ascii_letters, type(string.ascii_letters))
print(string.ascii_letters.encode(), type(string.ascii_letters.encode()))

abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ <class 'str'>
b'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' <class 'bytes'>

 

 

 

posted @ 2024-05-19 16:20  AlphaGeek  阅读(88)  评论(0)    收藏  举报