Python基础之迭代器与生成器

迭代器

从【for】说起

在 Python 中,我们学过的数据类型有可以使用 for 循环的,例如:str,list,tuple,dict,set 等; 也有不可以使用 for 循环的,例如:bool, int 等。

我们将一个 int 类型的数据放入 for 循环中:

for i in 123:
    print(i)

会得到这样一行报错:
报错啦!!!
翻译成中文就是int对象是不可以迭代的

那么是不是 for 循环只能用于可迭代对象呢?

是的!


可迭代

从 for 循环我们谈到了 可迭代 的概念。因为 123 是不可迭代的,所以它不能被 for 循环遍历。 而 字符串,列表,元祖,字典,集合 都是可以被遍历的,那么这些数据类型都是可迭代的。

我们可以借助 isinstance() 函数来验证我们的猜想。

isinstance(object, classinfo)
· object ——实例对象。
· classinfo —— 可以是直接或间接类名、基本类型或者由它们组成的元组。
如果参数object是classinfo的实例,或者object是classinfo类的子类的一个实例, 返回True。如果object不是一个给定类型的的对象, 则返回结果总是False。

from collections import Iterable
print(isinstance(True, Iterable))  # False
print(isinstance(123, Iterable))  # False
print(isinstance('123', Iterable))  # True
print(isinstance([1, 2, 3], Iterable))  # True
print(isinstance((1, 2, 3), Iterable))  # True
print(isinstance({1: 'A', 2: 'B', 3: 'C'}, Iterable))  # True
print(isinstance({1, 2, 3}, Iterable))  # True

结合上面 for 循环的取值现象,我们可以认定,可迭代就是可以将数据集中的元素一个挨一个的取出来


可迭代协议

从上面知道能被 for 循环的数据类型是 “可迭代的”,那么 for 循环是怎么判定数据类型是否是可迭代的呢?

在创造一个数据类型的时候,要想这个数据类型能被 for 一个一个的取值,那么就需要满足使用 for 的某种要求。这种要求我们可以看做一种 “协议”。 可以满足被迭代要求的协议称作 “可迭代协议”

可迭代协议数据对象内部实现了一个__iter__的方法

下面我们来验证上面的说法。 Python 中的 dir() 函数返回一个包含 参数对象的属性、方法的列表。

num_dir = dir(123)
str_dir = dir('123')
list_dir = dir([1, 2, 3])
tuple_dir = dir((1, 2, 3))
dict_dir = dir({1: 'A', 2: 'B', 3: 'C'})
set_dir = dir({1, 2, 3})

print('Yes') if '__iter__' in num_dir else print('No')  # No
print('Yes') if '__iter__' in str_dir else print('No')  # Yes
print('Yes') if '__iter__' in list_dir else print('No')  # Yes
print('Yes') if '__iter__' in tuple_dir else print('No')  # Yes
print('Yes') if '__iter__' in dict_dir else print('No')  # Yes
print('Yes') if '__iter__' in set_dir else print('No')  # Yes

在上面我们可以看到: 所有能被 for 循环的数据类型内部都有一个__iter__方法。

那么我们来调用这个双下iter方法,看看会出现什么结果。

list_obj = [1, 2, 3]
print(list_obj.__iter__())
# <list_iterator object at 0x00000140F0CB4978>

对于 list 类型的对象执行了__iter__方法后, 产生了一个 list_iterator。 iterator : 迭代器。


迭代器协议

刚刚我们得到了一个 list_iterator,就是列表迭代器。 那么是什么是迭代器呢? 列表迭代器又和列表有什么区别呢?

print(dir([1, 2, 3]))
print(dir([1, 2, 3].__iter__()))

分别打印 列表 和 列表迭代器 中包含的方法。 为了更好地看出其中的区别,可以:

print(set(dir([1, 2, 3].__iter__())) - set(dir([1, 2, 3])))

得到: {'__next__', '__setstate__', '__length_hint__'}

list_iter = [1, 2, 3, 6324, '国机二院徐嘉浩', 'gkd'].__iter__()
# 获取迭代器的长度
print(list_iter.__length_hint__())  # 6
# 指定开始迭代的位置
print(list_iter.__setstate__(3))  # None
# 按照索引顺序从迭代器中取值,一次取一个
print(list_iter.__next__())  # 6324
print(list_iter.__next__())  # 国机二院徐嘉浩
print(list_iter.__next__())  # gkd
print(list_iter.__next__())  # StopIteration 抛出异常

迭代器中的三个方法就实现了 for 循环的效果。__ next __ 一个一个取值的过程就是 for 的取值过程,但是不同与 for 的是,在无法从迭代器中取到值的时候,__ next __()方法会抛出一个异常StopIteration。

迭代器遵循迭代器协议必须拥有__iter__和__next__方法。

好,下面我们用异常处理机制和 while 来模拟 for 的功能。

list_iter = [1, 2, 3, 6324, '国机二院徐嘉浩', 'gkd'].__iter__()

while True:
    try:
        item = list_iter.__next__()
        print(item)
    except StopIteration:
        break

range()究竟是什么

借用上面的知识,一起来判定一下 range() 究竟是可迭代对象还是迭代器?

from collections import Iterable

print(isinstance(range(10), Iterable))  # True
print('__iter__' in dir(range(10)))  # True
print('__next__' in dir(range(10)))  # False

所以 range() 是一个可迭代对象,并不是迭代器



生成器

【for】存在的意义

从上面的讨论中我们知道 for 是基于迭代器协议,调用__iter__()方法将数据容器转化为迭代器,并用__next__()一个接着一个的取出数据来实现对数据容器的循环遍历的。

但是我们好像使用 while 就可以轻松实现 for 的功能。例如对于一个列表:

li = [1, 2, 3, 6324, '国机二院徐嘉浩', 'gkd']
index = 0
while index < len(li):
    print(li[index])
    index += 1

是不是很轻松? 那 for 还有存在的意义吗?

我们在 while 中实现的循环遍历是基于列表的下标的。 对于序列类型的数据容器我们都可以用 while 去实现遍历。 那么 像 字典,集合, 文件对象这类非序列类型数据容器呢?
for 存在的意义就是对于非序列类型的数据容器也可以实现遍历。


认识生成器

为什么要使用迭代器呢? 除了可以实现对非序列类型的数据容器的取值,还有什么好处呢?
迭代器是一个接一个取值的,可以做到需要多少就取多少。好处就是可以节省内存
在实际开发过程中,内存问题是我们必须要考虑的问题。 很多时候为了节省内存,我们需要自己写能实现迭代器功能的东西,这种东西就叫做生成器

生成器Generator
  本质:迭代器(所以自带了__iter__方法和__next__方法,不需要我们去实现)
  特点:惰性运算,开发者自定义

Python 中提供的两种生成器:

  1. 生成器函数:常规函数定义。但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次重它离开的地方继续执行。
  2. 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表。

生成器函数

初次见面,生成器函数你好!

包含关键字 yield 的函数就是生成器函数。yield 也可以像 return 一样为我们从函数中返回值。 但不同于 return 的是, 函数执行到 yield 这一步时不会终止程序,而是将程序挂起,在下一次调用时会紧接着上次断开的位置继续执行。 所以一个 生成器函数中可以存在多次 yield 函数。 直接调用生成器函数不会返回一个具体的值,而是得到一个迭代器。 正是这个迭代器一次一次的获取值,才推动生成器函数的执行。

import time
from collections import Iterable


def generator_fuc():
    a = 1
    print('here is a')
    yield a
    print('o'*30)  # 停顿3秒后才执行这句输出,说明第一次调用g1时,在yield a执行结束后,程序就挂起了
    b = 2
    print('here is b')
    yield b


g1 = generator_fuc()  # 调用生成器函数
print(g1)  # 打印g1,得到<generator object generator_fuc at 0x000001F8076351A8>,说明g1是一个生成器

# 验证生成器g1就是个迭代器
print(isinstance(g1, Iterable))
print('__iter__' in dir(g1))
print('__next__' in dir(g1))


print('\n' + '-'*30 + '\n')  # 我是分割线
print(next(g1))
time.sleep(3)
print(next(g1))

那么生成器有什么好处呢? 这样的取值方式比一次性取完保存到内存中有哪些优点呢?

显而易见,生成器可以避免一次性在内存中产生太多的数据。 基于生成器的这种特性,可以做到即用即取

例如,公司需要生产一批零部件用于组装产品。 计划组装产品10万个,那么也就需要10万个零部件。 在实际的生产过程中,不可能说公司一次性生产完10万个零部件,才开始组装产品。 这样做可能早就错过了产品的抢占市场的时机或者是最佳销售期。 所以要按照批次生产零部件,组装产品。

假设一个批次生产1000个零部件:

def produce():
    """生产零部件"""
    for i in range(100000):
        yield '这是生产的第%s件零部件' % i


# 检测是否能顺利生产
product_g = produce()
print(product_g.__next__())
print(product_g.__next__())
print(product_g.__next__())

# 生产第一个批次
product_g1 = produce()
num = 0
for item in product_g1:
    print(item)
    num += 1
    if num == 1000:
        break

生成器监听文件输入

def detector(filename):
    f = open(filename, mode='r', encoding='utf-8')
    f.seek(0, 2)  # 将光标移动到文件末尾
    while True:
        content = f.readline()
        if content.strip():
            yield content.strip()
            
detector_g = detector('A.txt')
for line in detector_g:
    print(line)

利用生成器监测文件中的输入内容。运行生成器程序时,在文件中输入的内容会输出到Python输出的控制台中。
注意: 因为不同电脑本地运行的区别,监测不一定是及时的,可能会出现较长时间的停滞反应。


send的用法

关于 send 怎么用,我们直接来看一段代码

def generator():
    print('冲冲冲')
    content = yield 4396  # 注意这里!! yield 4396 不仅是一个表达式,也变成了一个值,并赋给了content
    print('content =', content)  # content == 7777777
    print('冲冲冲')
    yield 2800

g = generator()
ret1 = g.__next__()
print('ret1 =', ret1)
ret2 = g.send(7777777)
print('ret2 =', ret2)

运行一下,我们发现 content 的值是 7777777。

仔细阅读这段代码,我们发现了和前面写的生成器函数不同的地方。 yield 不仅仅是用来为生成器返回值了,它也可以为生成器函数中的变量赋值。send() 和__next__()在接收生成器中的值的作用上并没有什么差别,不同的是 send() 在接收当前 yield 返回的值之外,还可以从外部往上一个 yield 的位置传递一个值。

所以在使用 send() 的时候,我们需要注意

1. 第一次调用生成器的时候,必须使用__next__(),或者是send(None)
2. 生成器函数中的最后一个 yield 是不能接收到外部的值的。

程序实例: 实时计算平均数。 计算当前从键盘输入的所有数字的平均值。

def average_num():
    total = 0.0
    cnt = 0
    avg = None
    while True:
        term = yield avg
        total += term
        cnt += 1
        avg = total/cnt


g_avg = average_num()
next(g_avg)
while True:
    A = input()
    if A.isdigit():  # 如果A是数字组成的
        print(g_avg.send(int(A)))
    if A == 'Break':
        break

带预激协程的生成器

预激协程就是用来预先激活生成器的协助程序。在一些场景中,使用生成器的时候侧重 send() 的功能,那么第一次的yield返回值就不重要了。我们每一次碰到这样的场景都需要先写一个 next( g ) 来激活生成器。 为了更方便,更规范(代码尽量模块化);我们借助装饰器来帮我们完成这个步骤。

以上面的 “实时计算平均数” 的程序为例:

from functools import wraps

def wrapper(fun):
    @wraps(fun)
    def inner(*args, **kwargs):
        g = fun(*args, **kwargs)
        next(g)
        return g
    return inner

@wrapper
def average_num():
    total = 0.0
    cnt = 0
    avg = None
    while True:
        term = yield avg
        total += term
        cnt += 1
        avg = total/cnt

g_avg = average_num()
# next(g_avg)  装饰器函数已经提前完成了这一步 
while True:
    A = input()
    if A.isdigit():  # 如果A是数字组成的
        print(g_avg.send(int(A)))
    if A == 'Break':
        break

yield from

yield from 后面接可迭代对象。 可以是普通的可迭代对象,迭代器;也可以是一个生成器。
yield from可以把可迭代对象里的每个元素一个一个的 yield 出来,对比 yield 来说代码更加简洁,结构更加清晰。

def gen1():
    for s in 'abc':
        yield s
    for i in range(3):
        yield i

print(list(gen1()))


def gen2():
    yield from 'abc'
    yield from range(3)

print(list(gen2()))


生成器表达式

前面我们学过了 列表推导式, 可以用一句话表示出列表的构造。 生成器表达式也具有这个功能。也可以用一句话表示出生成器。而且它与列表推导式的区别仅仅是将 [] 换成了 () 。但是生成器表达式更加的节省内存。

例如:

list_even_num = [i for i in range(20) if i % 2 == 0]
print(list_even_num)  # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

gen_even_num = (i for i in range(20) if i % 2 == 0)
print(gen_even_num)  # <generator object <genexpr> at 0x0000025F37E61D00>
print(next(gen_even_num))  # 0
for i in gen_even_num:
    print(i, ' ', end='')  # 2  4  6  8  10  12  14  16  18  

Python不但使用迭代器协议,让for循环变得更加通用。大部分内置函数,也是使用迭代器协议访问对象的。例如,sum函数是Python的内置函数,该函数使用迭代器协议访问对象,而生成器实现了迭代器协议.。

所以,我们可以直接这样计算一系列值的和:

sum(i for i in range(20) if i % 2 == 0)

而不用多此一举地构造一个列表:

sum([i for i in range(20) if i % 2 == 0])

文章参考于http://www.cnblogs.com/Eva-J/articles/7213953.html

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页