Python小记:星号解包的妙用

最近在学习 Python asyncio,过程中遇到一段代码:

await asyncio.gather(
        *(generate_random(num) for num in values)
    )

初看这段代码就有一种熟悉的感觉,但是太久没有写 Python,并没有立刻反应过来 * 在这里到底在做什么。再三思考之后才回忆起来:这里的 * 是迭代器解包(iterable unpacking),把生成器表达式产出的多个值解包成多个独立参数,再传递给外层调用函数。写下这篇文章做个记录,方便以后温故知新。


为了真正理解 *,先来看一个场景。假设有一个函数需要接收多个位置参数:

def demo(a, b, c):
    print(a, b, c)

如果已经有一个可迭代对象,比如一个列表:

values = [1, 2, 3]

那么下面两种写法是等价的:

demo(*values)   # 1 2 3
demo(1, 2, 3)   # 1 2 3

这里 * 的作用就是:把可迭代对象”拆开”,变成多个独立的位置参数再传进去。

上面的代码也可以写成列表推导式:

demo(*[v+1 for v in values])

完整的示例代码:

import asyncio
import random

async def main():
    values = [1, 2, 3]
    await asyncio.gather(
        *(generate_random(num) for num in values)
    )

async def generate_random(num):
    print(f"{num} random number is {random.uniform(0.1, 1.0)}")
    await asyncio.sleep(2)

if __name__ == "__main__":
    import time

    start = time.perf_counter()
    random.seed(49)
    asyncio.run(main())
    end = time.perf_counter()
    print(f"\n==> Total time: {end - start:.2f} seconds")

两种写法在运行结果上没有区别,不过有一个点要留意:生成器表达式是惰性的。

  • 列表推导式会一次性把所有对象生成出来,先构建一个完整列表;
  • 生成器表达式则是每次迭代时才真正创建对象。

asyncio.gather 中,无论使用生成器表达式还是列表推导式,所有协程都会并发执行,生成器的惰性特质主要体现在内存使用上。当 values 数量比较多或者 generate_random 函数运行比较耗时(比如涉及 I/O)的情况下,生成器不会一次性占用大量内存,这个特质作用就会很大。


最后小结

总体来看 * 不只是”不定参数”,还可以用来做可迭代对象/列表的解包。在使用 asyncio.gather 时,* 可以将生成器表达式或列表推导式产生的多个协程对象解包为独立参数传递给 gather 函数,实现并发执行。

附参考

文章摘自:https://www.cnblogs.com/ilovetech/p/20364886