Python 07_Python迭代器与生成器

一、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

生成器创建后并不会执行任何代码逻辑,要触发运行需要先激活,激活生成器的两种方法:

  1. 使用 next(generator) 函数
  2. 使用 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)函数。

  • yield 关键字实现不带参数的生成器函数

示例:

 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 的区别:

  • return 语句执行后,会跳出当前函数,yield 不会。

  • yield 会保存当前的执行状态,再次调用会从上次暂停的位置继续执行。

  • yield 关键字实现带参数的生成器函数

使用生成器的 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
1def test():
2    i = 1
3    while i < 5:
4        temp = yield i**2       #python程序碰到 “=” 从右往左执行
5        print("in iter:",temp)
6        i += 1
7aa = test()
8for 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 的语句)就会报错,而其他语句不会报错

  • throw方法就是往生成器里面扔一个异常
 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 实现协程

协程是一种比线程更加轻量级的执行体,它可以被暂停并在之后恢复执行。协程的使用场景主要是为了实现异步编程。

Licensed under CC BY-NC-SA 4.0