一、Python 中的迭代器 与 可迭代对象
1.1 可迭代对象
可迭代对象(iterable) 是指没有实现 __next__()
方法,但有 __iter__()
的对象,可以认为是一个容器,其中有N个元素,可以迭代。
在Python中可以简单的认为,能够使用for循环遍历的,都是可迭代对象。常见的类型由list、tuple、range对象、str、bytes、bytearra、set、dict等。
判断一个对象是否可迭代就看这个对象里面有没有实现 __iter__()
或 __getitem__()
方法,前者的优先级更高。
1.2 迭代器(Iterator)
迭代器创建方法1:
使用 iter() 方法将可迭代对象转为迭代器。
示例一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import sys
mylist1 = [1, 3, 4, 6, 7, 8, 'osaiduhgfoiuydsfhguy', 'adsuygfydshgf', '654856465', 88, 90, 344]
print('==========mylist1=======')
print('type:',type(mylist1))
print(len(mylist1))
print('size:',sys.getsizeof(mylist1)) # 输出list占用内存空间
myiter1 = iter(mylist1) # 将list转为迭代器
print('==========mylist1=======')
print('type:',type(myiter1))
print('size:',sys.getsizeof(myiter1)) # 输出list占用内存空间
# 从迭代器取值
print('next取值---: ')
print(next(myiter1))
print(next(myiter1))
print('for 取值---:')
for i in myiter1:
print(i)
|
示例二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
from collections.abc import Iterable, Iterator
s = 'jaye'
l = [7, 3, 5, 7]
isinstance(s, Iterable) # True, 是可迭代对象
isinstance(l, Iterable) # True, 是可迭代对象
isinstance(s, Iterator) # False, 不是迭代器
isinstance(l, Iterator) # False, 不是迭代器
ll = iter(l) # iter() 返回迭代对象的迭代器
isinstance(ll, Iterator) # True, 是迭代器
# a是可迭代对象
a = [i for i in range(20)]
# iter(a)才是迭代器
it_dir = dir(iter(a))
# 检查iter(a)迭代器是否含有__iter__和__next__
print("__iter__" in it_dir and "__next__" in it_dir)
|
迭代器创建方法2:
任何实现了__iter__() 和 next()方法的对象都是迭代器。
迭代器是一种对象,它允许你逐一遍历某个数据集合。一个对象是迭代器,必须实现两个方法:__iter__()
和 __next__()
。
__iter__()
: 返回迭代器对象本身,使得对象可以用于循环遍历。
__next__()
: 决定迭代规则;返回容器中的下一个值,如果容器中没有更多元素了,则抛出StopIteration异常。而且,迭代器不会一次性吧所有元素都加载到内存,而是需要的时候才返回结果。
迭代器每次调用next()方法的时候做两件事:
- 为下一次调用next()方法修改状态。
- 生成当前调用的返回结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
str1 = 'laohu'
>>> data = iter(str1)
>>>
>>> next(data)
'l'
>>> next(data)
'a'
>>> next(data)
'o'
>>> next(data)
'h'
>>> next(data)
'u'
>>> next(data)
>>> next(data)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
|
实现迭代器的两个魔法函数:
__iter__()
:返回自身以获取该类的迭代器
__next__()
:返回容器中的下一个元素,当数据取完时,要引发一个StopIteration异常。
自定义迭代器,通过传入最小值和最大值,返回该范围内所有数值的平方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class Number():
def __init__(self, min, max):
self.min = min
self.max = max
def __iter__(self):
return self
def __next__(self):
num = self.min
if self.min <= self.max:
self.min += 1
return num ** 2
else:
raise StopIteration
for i in Number(1,7):
print(i,end='--')
# 1--4--9--16--25--36--49--
|
示例二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class Myiterator():
# 迭代器求斐波那契数列
def __init__(self,max):
self.prev = 0
self.curr = 1
self.max = max
def __iter__(self):
return self
def __next__(self):
if self.max == 0:
raise StopAsyncIteration
value = self.curr
self.curr += self.prev
self.prev = value
self.max = self.max-1
return value
mm = Myiterator(20)
for i in mm:
print(i)
|
为什么list、dict、str等数据类型不是Iterator?
这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
迭代器的缺点:
- 取值不够灵活。next方法只能往后取值,不能往前。取值不如按照索引的方式灵活,不能取指定的某一个值
- 无法预测迭代器的长度。迭代器通过next()方法取值,并不能提前知道要迭代出的个数
- 用完一次就失效
二、Python 中的生成器
2.1 生成器(Generator)
生成器(Generator) 是一个特殊类型的迭代器,特殊在可以通过send()方法像生成器中传入数据,而迭代器只能将数据输出。生成器是通过函数来定义的,但与普通函数不同,它使用 yield 关键字来返回值。
当生成器函数被调用时,它并不立即执行函数体,而是返回一个生成器对象。这个生成器对象在每次调用其 __next__()
方法时,执行生成器函数体中的代码,直到遇到 yield,然后暂停执行并返回 yield 后面的值。
生成器可以保存函数的局部状态,因此当函数被再次调用时,它从上一次暂停的地方继续执行。
python中函数如果包含了yield关键字,那么这个函数就不再是一个普通函数,而是一个生成器(generator)。生成器就是一个迭代器,在调用生成器的过程中,每次遇到 yield 时函数会暂停并保存运行信息,并在下次执行 next() 方法时从当前位置继续运行。
其主要的特点有:
- 拥有yield关键字的函数就是生成器函数,生成器拥有迭代器的迭代传出数据的功能,但用关键字yield来替代迭代器中的__next__()方法来实现。
- 生成器可以传入数据(使用send())进行计算(不同于迭代器),并根据变量内容计算结果后返回。
- 生成器不会一次把所有的元素加载到内存,而是调用的时候才生成返回结果(这点相同于迭代器)
- 可以通过for循环进行迭代(因为生成器是迭代器)
从生成器取值方法:
for i in myiter
:形式迭代取值 —- 同迭代器
next(myiter)
:迭代器next函数取值 —- 同迭代器
myiter.send(x)
:生成器send函数发送参数,同时取到值(生成器函数中未对接收参数进行任何处理时,也能使用send进行传参)
Tips: 注:不能在第一次取值时使用send(x);第一次取值使用next() 或者send(None)或for中迭代取值,否则会出现报错:TypeError: can't send non-None value to a just-started generator-python
生成器创建后并不会执行任何代码逻辑,要触发运行需要先激活,激活生成器的两种方法:
- 使用 next(generator) 函数
- 使用 generator.send(None)
1
2
3
4
5
6
7
8
9
10
11
12
|
def test_gen():
yield 'j'
yield 'a'
yield 'y'
yield 'e'
if __name__ == '__main__':
gen = test_gen()
print(gen.send(None))
print(next(gen))
print(gen.send(None))
print(next(gen))
|
python 中有两种方式提供生成器:1. yield 关键字实现的生成器函数;2. 生成器推导式。
2.2 yield 关键字实现生成器函数
在函数中使用 yield 语句而不是 return 语句返回结果,yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次重新在挂起的地方继续执行。函数中可以有多个yield,每个yield都会挂起一次,在for 循环中迭代,或使用next(iterobject,defalt)函数。
示例:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
|
def fib(max):
n, a = 0, 0
while n < max:
print('in while start')
yield a #每次返回值则停在此处等待下次取值再往下执行
a = a+1
n = n + 1
print('in while end')
dd = fib(10)
i = 0
while i<10:
print(next(dd))
print('完成一次取值')
i = i +1
'''
Output:
in while start
0
完成一次取值
in while end
in while start
1
完成一次取值
in while end
in while start
2
完成一次取值
in while end
in while start
3
完成一次取值
in while end
in while start
4
完成一次取值
'''
|
yield 与 return 的区别:
使用生成器的 Send() 方法,可以向生成器函数中传入数据,从而实现对生成器函数的计算。实际上next()和send()在一定意义上作用是相似的,区别是send()可以传递yield表达式的值进去,而next()不能传递特定的值,只能传递None进去。因此,可以看做 c.next() 和 c.send(None) 作用是一样的。
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
26
27
|
1:def test():
2: i = 1
3: while i < 5:
4: temp = yield i**2 #python程序碰到 “=” 从右往左执行
5: print("in iter:",temp)
6: i += 1
7:aa = test()
8:for j in range(4):
9: if j == 0:
10: print(aa.send(None))
11: else:
12: print(aa.send(j))
13: print('==========')
'''
Output:
1 #第一次执行到第4行等号右侧
==========
in iter: 1 #第二次执行 从第4行等号左侧接收send参数 往下执行
4
==========
in iter: 2
9
==========
in iter: 3
16
==========
'''
|
解析:
- 第一次执行只执行到yield右侧 并返回yield右侧的值
- send() 把参数传递给temp,并让生成器执行一次到yield右侧,接收返回值
2.23 生成器推导式
生成器推导式 类似于列表推导式,但是,生成器返回按需要产生结果的一个对象,而不是一次构建整个结果列表。
列表推导式: squares = [x**2 for x in range(5)]
,squres = [0,1,4,9,16] 是列表对象
换成圆括号–生成器:
1
2
3
4
|
squares = (x**2 for x in range(5)) # squares 是生成器对象
next(squares) # 0
next(squares) # 1
|
2.4 生成器中的方法:
- send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个 yield 位置
1
2
3
4
5
6
7
8
9
10
11
12
13
|
def gen_func():
url = yield "http://www.jayeblog.cn"
print(url)
yield 1
yield 2
if __name__ == "__main__":
gen = gen_func()
url = gen.send(None)
print(url)
url = "http://www.zhihu.com"
print(gen.send(url)) # send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个 yield 位置
|
- close() 关闭生成器,已经关闭的生成器再次调用 next 方法报错: StopIteration
1
2
3
4
5
6
7
8
9
10
|
def gen_func():
yield "http://www.jayeblog.cn"
yield "1"
yield "2"
if __name__ == "__main__":
gen = gen_func()
print(next(gen)) # 启动生成器,并输出了第一个 yield 返回信息
gen.close() # 调用 close 方法关闭了生成器
print(next(gen)) # 再次调用 next 方法报错: StopIteration
|
我们来试着捕获一下异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
def gen_func():
try:
yield "http://www.jayeblog.cn"
except GeneratorExit:
pass
yield "1"
yield "2"
if __name__ == "__main__":
gen = gen_func()
print(next(gen))
gen.close()
print(next(gen))
|
代码执行到第一个 print(next(gen)) 的时候,其实就停在了 yield “http://www.jayeblog.cn"处,因此会输出 yield "http://www.jayeblog.cn"
。然后当调用 gen.close()
时,就捕获了 GeneratorExit 异常,我们直接 pass ,按理说第二个 print(next(gen)) 应该打印输出 “1”,但是直接在 gen.close() 的地方报错:RuntimeError: generator ignored GeneratorExit,这是因为生成器已经关闭了,执行生成器相关的代码逻辑(含有 yield 的语句)就会报错,而其他语句不会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
def gen_func():
try:
yield "http://www.jayeblog.cn"
except Exception as e:
print(f'--{e}--')
print('0')
yield 1
yield 2
if __name__ == "__main__":
gen = gen_func()
print(next(gen))
gen.throw(Exception, "throw method test")
print(next(gen))
|
解释一下为什么这里第二个 print(next(gen)) 打印的值是 2 而不是 1,当我们第一次调用 next(gen)之后就会 print 出第一个 yield
的值,然后挂起,当调用 throw 方法传入一个异常之后,这个异常被捕捉到,但是 throw 方法会继续执行生成器里后面的语句,
它会驱动生成器继续执行到下一个 yield 才返回,也就是 yield 1 这里,所以当再次执行 next(gen),就到了yield 2
2.5 生成器的状态
生成器的生命周期中,会有以下四个状态:
- GEN_CREATED: 已创建,还未被激活
- GEN_RUNNING: 正在执行,只有在多线程任务中才能看到
- GEN_SUSPENDED: 在 yield 表达式处暂停,可以理解为挂起中
- GEN_CLOSED: 执行结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
def test_gen():
yield 'j'
yield 'a'
yield 'y'
yield 'e'
if __name__ == '__main__':
from inspect import getgeneratorstate
gen = test_gen()
print(getgeneratorstate(gen)) # GEN_CREATED
gen.send(None) # j
print(getgeneratorstate(gen)) # GEN_SUSPENDED
next(gen) # a
print(getgeneratorstate(gen)) # GEN_SUSPENDED
gen.close()
print(getgeneratorstate(gen)) # GEN_CLOSED
|
2.6 生成器 与 迭代器 的区别
- 定义方式:迭代器是通过类来定义并实现
__iter__()
和 __next__()
方法;生成器是通过使用 yield 关键字的函数来定义的。不过生成器实际上是一种特殊的迭代器。
- 使用方式:迭代器需要通过实现类来定义和管理数据迭代,而生成器通过函数的代码执行和 yield 表达式自动生成数据。可以传参数(send()),迭代器不行。
- 内存效率:生成器通常更节省内存,因为它们会根据需要产生值,而不是一次性生成所有值。
可迭代对象和迭代器,是将所有的值都生成存放在内存中。而生成器由于有这个 yield 关键字,它在每次取值的时候都会在这里将新的值返回,并阻塞等待下一次调用,它是在需要元素的时候才临时生成,所以更节省时间和空间。
迭代器和生成器在 Python 中都有重要的作用,主要用于数据的迭代、处理和生成。它们在编写可读性和内存效率更高的代码方面特别有用。
三、Python 中 生成器 的应用
3.1 大数据处理/惰性计算
在处理大量数据时,使用生成器可以避免将整个数据集加载到内存中,有些数据需要惰性计算(延迟计算),从而节省内存。例如,逐行读取大文件或者逐个处理大型数据集。
比如统计大文件有多少行:
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
26
27
|
def read_large_file(file_path):
"""使用生成器逐行读取大型文件。"""
try:
with open(file_path, 'r') as file:
for line in file:
# 在这里使用yield暂停并返回每一行
yield line.strip() # 去除每行的换行符
except FileNotFoundError:
print(f"文件 {file_path} 未找到。")
return
def process_large_file(file_path):
"""示例函数,用于演示如何使用生成器处理大型文件。"""
line_count = 0 # 行计数器
for line in read_large_file(file_path):
# 对每一行进行处理
# 这里可以添加你自己的处理逻辑
print(f"Processing line {line_count}: {line}")
# 示例:简单地统计行数
line_count += 1
print(f"Total lines processed: {line_count}")
if __name__ == "__main__":
# 在这里指定要读取的文件路径
file_path = "large_file.txt"
# 使用process_large_file函数处理大文件
process_large_file(file_path)
|
3.2 流式数据处理
生成器适用于流式数据处理,在数据流源源不断地产生的应用场景下,可以通过生成器逐一处理它们。例如,读取数据流(如网络流或数据库流)并逐一处理。
比如下面这段源源不断从redis队列拉取数据:
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
26
27
28
29
30
31
32
33
34
35
36
37
|
import time
import redis
def redis_stream_generator(redis_client, queue_name):
"""生成器函数,用于从 Redis 队列中流式获取数据。"""
while True:
# 尝试从 Redis 队列中弹出一个数据项
data = redis_client.blpop(queue_name, timeout=2)
if data:
# `data` 是一个元组 (queue_name, data)
_, value = data
# 使用 yield 返回数据
yield value
else:
# 如果队列为空,睡眠2秒
time.sleep(2)
def process_redis_stream(data_stream):
"""处理 Redis 数据流的示例函数。"""
for data in data_stream:
# 对数据进行处理
print(f"Processing data: {data}")
# 在这里添加你的数据处理逻辑
# 例如:对数据进行统计、计算等
# 如果要在特定条件下停止处理,可以在这里添加逻辑
if __name__ == "__main__":
# 连接到 Redis 服务器
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)
# 定义 Redis 队列的名称
queue_name = 'my_queue'
# 创建 Redis 数据流生成器
data_stream = redis_stream_generator(redis_client, queue_name)
# 处理 Redis 数据流
process_redis_stream(data_stream)
|
3.3 无限序列
生成器可以用来生成无限序列,例如斐波那契数列、素数序列等。这些序列往往无法通过列表来表示,因为它们是无限的,但可以通过生成器逐 一生成和处理。
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
26
|
def fibonacci_generator():
"""生成器函数,用于生成斐波那契数列。"""
a, b = 0, 1
while True:
# 使用 yield 返回当前的斐波那契数
yield a
# 计算下一个斐波那契数
a, b = b, a + b
def process_fibonacci_sequence(n):
"""示例函数,用于演示如何处理生成的斐波那契数列。"""
# 创建 Fibonacci 数列的生成器
fibonacci_gen = fibonacci_generator()
print(f"First {n} numbers in the Fibonacci sequence:")
# 使用生成器逐个获取前 n 个斐波那契数
for _ in range(n):
fibonacci_number = next(fibonacci_gen)
print(fibonacci_number)
if __name__ == "__main__":
# 指定要生成的斐波那契数的个数
n = 10 # 例如,获取前 10 个斐波那契数
# 处理并打印前 n 个斐波那契数
process_fibonacci_sequence(n)
|
3.4 组合生成器
通过生成器的组合,可以创建复杂的数据流水线。例如,将多个生成器组合在一起,进行数据的提取、转换和加载(ETL)操作。
3.5 实现协程
协程是一种比线程更加轻量级的执行体,它可以被暂停并在之后恢复执行。协程的使用场景主要是为了实现异步编程。