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
进行判断,下面是一个例子。
squares: list[int] = [x ** 2 for x in range(10) if x % 2 == 0]
生成式内部的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
的值,上述代码会依次输出1
、2
、3
。
注:生成器函数的类型标注中,Generator[str, int, None]
表示生成器类型,yield
返回值类型为str
,send
函数接收类型为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()
使用,它可以向生成器发送值并在生成器内部用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
方法向生成器内部抛出一个异常。
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
方法关闭生成器,关闭生成器后再次调用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))
实际上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)