python 生成器 generators

python 生成器 Generators

How to Use Generators and yield in Python

生成器适合在以下场景中使用:

  1. 处理过大到超出机器内存的数据集
  2. 有一个复杂的函数,每次调用它时都需要维护一个内部状态,但该函数太小而无法证明创建自己的类是合理的

生成器的特点是不将其内容存储在内存中。生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。

读取大文件

统计文件的行数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 第一种方法,一次读入
def csv_reader_1(file_name):
file = open(file_name)
result = file.read().split("\n")
return result

# 第二种方法
def csv_reader_2(file_name):
for row in open(file_name, "r"):
yield row




csv_gen = csv_reader_1("some_csv.txt")
# csv_gen = csv_reader_2("some_csv.txt")
row_count = 0

for row in csv_gen:
row_count += 1

print(f"Row count is {row_count}")


第一种方法中,file.read().split()一次将所有内容加载到内存中,导致MemoryError.

第二种方法,用了yield。执行顺序是,csv_gen = csv_reader_2("some_csv.txt")时,并未跳入csv_reader_2函数!!注意这里还没跳入函数,因为这个函数里面有yield,只是把这个函数赋给一个对象,然后直到for row in csv_gen(等价于next()),才跳入csv_reader_2函数内部,然后执行到yield这条,就返回,然后再遇到for row in csv_gen:,再跳入yield位置的下一条开始执行。

生成无限序列

获得有限序列:

1
2
3
>>> a = range(5)
>>> list(a)
[0, 1, 2, 3, 4]

然而,生成无限序列需要使用生成器,因为您的计算机内存是有限的

1
2
3
4
5
def infinite_sequence():
num = 0
while True:
yield num
num += 1

除了使用for循环,您还可以next()直接调用生成器对象。这对于在控制台中测试生成器特别有用:

1
2
3
4
5
6
7
8
9
>>> gen = infinite_sequence()
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
3

在这里,您有一个名为 的生成器gen,您可以通过重复调用 来手动迭代它next()

注意:当您使用 时next(),Python 会调用.__next__()您作为参数传入的函数。此参数化允许产生一些特殊效果,但这超出了本文的范围。尝试更改传递给的参数next(),看看会发生什么!

示例 3:检测回文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def is_palindrome(num):
# Skip single-digit inputs
if num // 10 == 0:
return False
temp = num
reversed_num = 0

while temp != 0:
reversed_num = (reversed_num * 10) + (temp % 10)
temp = temp // 10

if num == reversed_num:
return num
else:
return False

该函数接受一个输入数字,将其反转,并检查反转后的数字是否与原始数字相同。现在您可以使用无限序列生成器来获取所有数字回文的运行列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> for i in infinite_sequence():
... pal = is_palindrome(i)
... if pal:
... print(i)
...
11
22
33
[...]
99799
99899
99999
100001
101101
102201
KeyboardInterrupt
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "<stdin>", line 5, in is_palindrome

了解生成器

生成器函数的外观和行为就像常规函数一样,但具有一个定义特征。生成器函数使用 Pythonyield关键字而不是return. 回想一下您之前编写的生成器函数:

1
2
3
4
5
def infinite_sequence():
num = 0
while True:
yield num
num += 1

这看起来像一个典型的函数定义,除了 Python yield 语句和它后面的代码。yield指示将值发送回调用方的位置,但与 不同的是return,之后您不会退出该函数。

相反,函数的状态会被记住。这样,当在生成器对象上调用(在循环next()中显式或隐式)时,先前生成的变量会递增,然后再次生成。由于生成器函数看起来像其他函数并且与它们的行为非常相似,因此您可以假设生成器表达式与 Python 中可用的其他理解非常相似。

使用生成器表达式构建生成器

与列表推导式一样,生成器表达式允许您仅用几行代码即可快速创建生成器对象。它们在使用列表理解的相同情况下也很有用,还有一个额外的好处:您可以创建它们而无需在迭代之前构建并将整个对象保存在内存中。换句话说,当你使用生成器表达式时,你不会有内存损失。以对一些数字进行平方为例:

1
2
>>> nums_squared_lc = [num**2 for num in range(5)]	# list 列表
>>> nums_squared_gc = (num**2 for num in range(5)) # 生成器

执行时:

1
2
3
4
>>> nums_squared_lc
[0, 1, 4, 9, 16]
>>> nums_squared_gc
<generator object <genexpr> at 0x107fbbc78>

第一个对象使用括号构建列表,而第二个对象使用括号创建生成器表达式。输出确认您已经创建了一个生成器对象并且它不同于列表。

比较列表和生成器各自生成对象的大小:

1
2
3
4
5
6
7
>>> import sys
>>> nums_squared_lc = [i ** 2 for i in range(10000)]
>>> sys.getsizeof(nums_squared_lc)
87624
>>> nums_squared_gc = (i ** 2 for i in range(10000))
>>> print(sys.getsizeof(nums_squared_gc))
120

可以看出,生成器对象比列表对象小

比较列表和生成器各自求值的速度快慢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> import cProfile
>>> cProfile.run('sum([i * 2 for i in range(10000)])')
5 function calls in 0.001 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
1 0.001 0.001 0.001 0.001 <string>:1(<listcomp>)
1 0.000 0.000 0.001 0.001 <string>:1(<module>)
1 0.000 0.000 0.001 0.001 {built-in method builtins.exec}
1 0.000 0.000 0.000 0.000 {built-in method builtins.sum}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}


>>> cProfile.run('sum((i * 2 for i in range(10000)))')
10005 function calls in 0.003 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
10001 0.002 0.000 0.002 0.000 <string>:1(<genexpr>)
1 0.000 0.000 0.003 0.003 <string>:1(<module>)
1 0.000 0.000 0.003 0.003 {built-in method builtins.exec}
1 0.001 0.001 0.003 0.003 {built-in method builtins.sum}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}

可以看出,列表求值比生成器求值快

yield

How to Use Generators and yield in Python

runoob Python yield 使用浅析

调用生成器函数或使用生成器表达式时,会返回一个称为生成器的特殊迭代器。可以将此生成器分配给一个变量以便使用它。当在生成器上调用特殊方法时,例如next(),函数内的代码将执行到yield

当命中 Python yield 语句时,程序暂停函数执行并将产生的值返回给调用者。(相反,return完全停止函数执行。)当一个函数被挂起时,该函数的状态被保存。这包括生成器本地的任何变量绑定、指令指针、内部堆栈和任何异常处理。

这允许在调用生成器的方法之一时恢复函数执行。这样,所有函数评估都会在 之后立即恢复yield。可以通过使用多个 Python yield 语句来查看实际效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> def multi_yield():
... yield_str = "This will print the first string"
... yield yield_str
... yield_str = "This will print the second string"
... yield yield_str
...
>>> multi_obj = multi_yield()
>>> print(next(multi_obj))
This will print the first string
>>> print(next(multi_obj))
This will print the second string
>>> print(next(multi_obj))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

仔细看看最后一次调用next(). 您可以看到执行已因回溯而爆炸。这是因为生成器和所有迭代器一样,可能会被耗尽。除非你的生成器是无限的,否则你只能迭代一次。一旦所有值都被评估,迭代将停止并且for循环将退出。如果您使用next(),那么您将得到一个显式StopIteration异常。

注意: StopIteration是一个自然异常,它被引发以表示迭代器结束。for例如,循环是围绕StopIteration. for您甚至可以使用循环来实现自己的while循环

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> letters = ["a", "b", "c", "y"]
>>> it = iter(letters)
>>> while True:
... try:
... letter = next(it)
... except StopIteration:
... break
... print(letter)
...
a
b
c
y

一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def fab(max): 
n, a, b = 0, 0, 1
while n < max:
yield b # 使用 yield
# print b
a, b = b, a + b
n = n + 1

for n in fab(5):
print (n)
# 或者
>>>f = fab(5)
>>> f.next()
1
>>> f.next()
1
>>> f.next()
2

使用高级生成器方法

除了yield,生成器对象还可以使用以下方法:

  • .send()
  • .throw()
  • .close()