生成式和生成器

Python中生成式(Comprehensions)和生成器(Generators)是两种常用的语法,用于处理集合数据和构建可迭代对象。合理使用生成式和生成器可以简化代码提高可读性,不过“炫技”式的使用这种语法则可能适得其反。

生成式

Python的生成式语法比较灵活,难以用语言来描述,这里我们直接看几个例子。

列表生成式

下面生成式创建了0-9的平方列表。

squares: list[int] = [x ** 2 for x in range(10)]

它生成的是一个完整的列表,类型为list[int]

集合生成式

下面生成式和上面类似,但是生成的是集合。

squares: set[int] = {x ** 2 for x in range(10)}

字典生成式

下面例子使用生成式生成了字典。

squares: dict[int:int] = {x: x ** 2 for x in range(10)}

带if判断的生成式

生成式内部可以使用if进行判断,下面是一个例子。

squares: list[int] = [x ** 2 for x in range(10) if x % 2 == 0]

带嵌套for循环的生成式

生成式内部的for循环还可以嵌套使用,下面是一个例子。

squares: list[int] = [x * y for x in range(10) for y in range(10)]

生成器

生成式会一次生成整个列表,但有时我们不希望一次性生成所有元素,这可能十分耗费内存空间。生成器和生成式作用相同,但是生成的元素是按需生成,随取随用。

使用生成器函数

下面代码是一个最简单的生成器。

from typing import Generator


def gen_num() -> Generator[int, None, None]:
    yield 1
    yield 2
    yield 3


# 创建一个生成器对象
gen: Generator[int, None, None] = gen_num()

# 逐步获取生成器的值
print(next(gen))
print(next(gen))
print(next(gen))

代码中的gen_num()是生成器函数,这个函数和普通函数不同,它的内部使用yield关键字返回数据,而且yield之后还有其它代码逻辑,这种生成器函数的返回值是一个生成器对象,我们可以用next()函数调用它。

对于生成器它的执行逻辑是这样的,每次调用next()函数生成器都就会执行到下一个yield语句,然后返回yield的值,上述代码会依次输出123

注:生成器函数的类型标注中,Generator[str, int, None]表示生成器类型,yield返回值类型为strsend函数接收类型为int,最后一个是生成器return的返回值类型,有关send函数和生成器的return返回值将在后文介绍。

对于生成器对象,除了使用next()调用它,我们还可以直接用for循环对其进行迭代,下面是一个例子,这段代码我们迭代这个生成器3次。

for i in gen:
    print(i)

使用for对生成器进行迭代时,循环会在生成器对象被耗尽时自动结束。

此外我们还要了解,生成器在yield处暂停时会记住函数的执行状态,包括局部变量和当前指令位置。

from typing import Generator


def gen_num(n: int) -> Generator[None, None, None]:
    while n > 0:
        print(f'n: {n}')
        yield
        n -= 1


gen: Generator[None, None, None] = gen_num(3)

for i in gen:
    pass

上面代码调用了3次生成器,生成器函数内部的n会被记住并递减。

使用send()函数

生成器可以配合send()使用,它可以向生成器发送值并在生成器内部用yield接收,下面是一个例子。

from typing import Generator


def generator_with_send() -> Generator[str, int, None]:
    value = yield 'Ready to receive'
    print('Received value:', value)
    yield 'Done'


gen: Generator[str, int, None] = generator_with_send()
print(next(gen))  # 输出: Ready to receive
print(gen.send(42))  # 输出: Received value: 42, 返回 Done

代码中我们调用了两次生成器,第一次返回Ready to receive,第二次调用我们使用send()函数发送了数字42,生成器内部使用第一个yield接收该值并打印,最后生成器返回Done执行结束。

使用throw()函数

在生成器外部我们可以使用throw方法向生成器内部抛出一个异常。

from typing import Generator


def generator_with_exception() -> Generator[str, None, None]:
    try:
        yield 'Start'
    except ValueError as e:
        yield f'Exception caught: {e}'


gen: Generator[str, None, None] = generator_with_exception()
print(next(gen))
print(gen.throw(ValueError('Oops!')))

上面代码的输出如下。

Start
Exception caught: Oops!

使用close()函数

在生成器外部我们可以使用close方法关闭生成器,关闭生成器后再次调用next()会抛出StopIteration异常。

from typing import Generator


def gen_num() -> Generator[int, None, None]:
    yield 1
    yield 2
    yield 3


gen: Generator[int, None, None] = gen_num()

print(next(gen))
gen.close()
print(next(gen))  # 该处会抛出StopIteration异常
print(next(gen))

生成器的return返回值

实际上Python提供了多种方式获取生成器的return返回值,其中一种比较常用的是通过捕获StopIteration异常来获取,返回值会被设置在异常对象的value属性中,下面是一个例子。

from typing import Generator


def gen_num() -> Generator[int, None, str]:
    yield 1
    yield 2
    yield 3
    return "Done"


gen: Generator[int, None, str] = gen_num()
while True:
    try:
        result = next(gen)
        print(result)
    except StopIteration as e:
        print(e.value)
        break

代码中我们的生成器函数在yield返回3个值后就会耗尽,如果再次使用next()函数访问将抛出StopIteration异常,此时我们就可以通过异常对象的value属性获取生成器函数的return返回值。

生成器表达式

生成器表达式的语法类似于列表生成式,但使用小括号()而不是方括号[]。生成器表达式在生成元素时也不会占用内存来存储整个列表,而是按需生成每个元素。

from typing import Generator

squares: Generator[int, None, None] = (x ** 2 for x in range(10))

for x in squares:
    print(x)
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap