Python生成器

生成器

一、 迭代器

1、 迭代

什么是迭代器?它是一个带状态的对象,在你调用next()方法的时候返回容器中的下一个值,任何实现了__iter____next__()(python2中实现next())方法的对象都是迭代器,__iter__返回迭代器自身,__next__返回容器中的下一个值,如果容器中没有更多元素了,则抛出StopIteration异常。可迭代对象实现了__iter__方法,该方法返回一个迭代器对象

判断是否可迭代的方法:

from collections.abc import Iterable

print(isinstance("abc", Iterable))

可迭代对象取值,可以通过for ... in ...来遍历取值。只要是可迭代对象,无论是否有下标,都是可迭代的


那么,如果要对list进行遍历取值,如何同时返回索引和值呢?

  1. 通过引入外部变量(谁都会)

  2. 使用enumerate()枚举值

    lis = ['a', 'b', 'c']
    for i, j in enumerate(lis):
        print(i, j)
    

2、 迭代器原理

2.1 组成

从技术上讲,在Python中,迭代器是实现迭代器协议的对象,该协议由方法__iter__()__next__()组成

如果迭代器里面没有元素,会抛出StopIteration异常

列表,元组,字典和集合都是可迭代的对象。 它们是可迭代的容器,可以从中获得迭代器,所有这些对象都有一个iter()方法,该方法用于获取迭代器:

lis = ['a', 'b', 'c']
lis_iter = iter(lis)  # 也可以通过lis.__iter__() 来创建可迭代对象
print(lis_iter.__next__())  # 也可以使用next(lis_iter)来取值
print(next(lis_iter))

故,for循环的工作原理可以大概这么认为:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# @author: A.L.Kun
# @file : demo02.py
# @time : 2022/5/1 10:22
lis = ['a', 'b', 'c']
it = iter(lis)
# for 循环遍历,相当于
while True:
    try:
        print(next(it))  # 对迭代器里面的元素进行操作
    except StopIteration:
        # 如果是迭代错误,说明迭代器里面没有值,退出死循环
        break

2.2 定义迭代器

要创建一个对象/类作为迭代器,必须实现__iter__()__next__()方法

正如,所有类都有一个名为__init__()的函数,该函数可以在创建对象时进行一些初始化

__iter__()方法的行为类似,可以执行操作(初始化等),但必须始终返回迭代器对象本身

__next__()方法还允许你进行操作,并且必须返回序列中的下一项

同时,限制迭代次数,最多迭代20次

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# @author: A.L.Kun
# @file : demo02.py
# @time : 2022/5/1 10:22
class MyIter:
    def __iter__(self):
        self.a = 0
        return self

    def __next__(self):
        if self.a < 20:
            self.a += 1
            return self.a  # 返回值
        # 如果迭代次数超过20
        raise StopIteration


mi = MyIter()
it = iter(mi)
for i in range(22):
    print(next(it))

3、 列表生成式

lis = [i * i for i in range(20)]

通过列表生成式,我们可以直接创建一个列表

二、 生成器

1、 引入背景

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator

也就是说,对于可以用某种算法推算得到的多个数据,生成器并不会一次性生成它们,而是什么时候需要,才什么时候生成

生成器的创建方式也比迭代器简单很多,大体分为以下 2 步:

  1. 定义一个以 yield 关键字标识返回值的函数
  2. 调用刚刚创建的函数,即可创建一个生成器

生成器和迭代器的区别:

  • 生成器函数包含一个或多个yield语句
  • 调用时,它返回一个对象(迭代器),但不会立即开始执行
  • 像__iter__()和__next__()这样的方法会自动实现。因此,我们可以使用next()来遍历项目
  • 一旦函数产生了结果,函数就会暂停,控制就会转移给调用者
  • 局部变量及其状态在连续调用之间被记住
  • 最后,当函数终止时,在进一步调用时会自动引发StopIteration

2、生成器使用

2.1 创建

创建一个生成器

def gen():
    a = 1
    yield a  
    a += 2
    yield a  

g = gen()  # 其为生成器对象
print(type(g))
print(g.__next__())  # 使用next取值
print(next(g))

可以通过next取值,说明就就可以使用for循环来取值,当生成器里面没有数据时,会抛出StopIteration异常

2.2 send

send(val):外部传入一个值,到生成器内部,改变yield返回的值

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# @author: A.L.Kun
# @file : demo02.py
# @time : 2022/5/1 10:22
def gen():
    i = 1
    while True:
        j = yield i
        i *= 2
        if j == -1:
            print("停止")
            break


# 这里,我们发现,i的值一直为正数,那么,如何使得j的值为-1呢?
# 这里就要使用send方法
g = gen()
print(g.__next__())
print(next(g))
g.send(-1)  # 抛出迭代异常而停止

2.3 throw

外部除了可以向生成器里面传入值以外,还可以传入异常

throw(type[, value[, traceback]]):外部向生成器抛出一个异常

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# @author: A.L.Kun
# @file : demo02.py
# @time : 2022/5/1 10:22
def gen():
    i = 1
    while True:
        try:
            j = yield i
            i *= 2
        except StopIteration:
            print("迭代器传入异常")
            yield StopIteration  # 返回异常信息
            break


g = gen()
print(g.__next__())
print(next(g))
print(g.throw(StopIteration))  # 抛出迭代异常而停止

2.4 close

生成器的 close() 方法也比较简单,就是手动关闭这个生成器,关闭后的生成器无法再进行操作

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# @author: A.L.Kun
# @file : demo02.py
# @time : 2022/5/1 10:22
def gen():
    i = 1
    while True:
        j = yield i
        i *= 2


g = gen()
print(g.__next__())
g.close()  # 关闭生成器
print(next(g))  # 生成器关闭以后,就无法取值了

2.5 yield from

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# @author: A.L.Kun
# @file : demo02.py
# @time : 2022/5/1 10:22
# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        count += 1
        total += new_num
        average = total / count


# 委托生成器
def proxy_gen():
    while True:
        yield from average_gen()


# 调用方
def main():
    calc_average = proxy_gen()
    next(calc_average)  # 预激生成器
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0


if __name__ == '__main__':
    main()

委托生成器:

  • 在调用方与子生成器之间建立一个双向通道
  • 双向通道
    • 调用方可以通过send()直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方
  • 同时,其不会对yield返回的值进行拦截

2.6 生成器生成式

# 和列表生成式类似,只是把 [] 改成了 ()
lis = [i for i in range(20)]  # 列表生成式
print(lis)
gen = (i for i in range(20))  # 生成器生成式
print(gen)

3、 综合案例

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# @author: A.L.Kun
# @file : demo02.py
# @time : 2022/5/1 10:22

from collections import namedtuple

Result = namedtuple('Result', 'count average')  # 创建一个命名元组


# 子生成器
# 这个例子和上边示例中的 average 协程一样,只不过这里是作为字生成器使用
def average():
    total = 0.0
    count = 0
    average = None
    while True:
        # main 函数发送数据到这里
        term = yield  # 接收数据
        if term is None:  # 终止条件
            break
        total += term  # 求和
        count += 1  # 求有多少组
        average = total / count  # 求平均值
    return Result(count, average)  # 返回的Result 会成为grouper函数中yield from表达式的值


# 委派生成器
def grouper(results, key):
    # 这个循环每次都会新建一个average 实例,每个实例都是作为协程使用的生成器对象
    while True:
        # grouper 发送的每个值都会经由yield from 处理,通过管道传给average 实例。
        # grouper会在yield from表达式处暂停,等待average实例处理客户端发来的值。
        # average实例运行完毕后,返回的值绑定到results[key] 上。while 循环会不断创建average实例,处理更多的值。
        x = average()  # 其为生成器对象
        results[key] = yield from x


# 调用方
def main(data):
    results = {}
    for key, values in data.items():
        # group 是调用grouper函数得到的生成器对象,传给grouper 函数的第一个参数是results,用于收集结果;第二个是某个键
        group = grouper(results, key)
        next(group)  # 预激活生成器
        for value in values:
            # 把各个value传给grouper 传入的值最终到达average函数中;
            # grouper并不知道传入的是什么,同时grouper实例在yield from处暂停
            group.send(value)
        # 把None传入grouper,传入的值最终到达average函数中,导致当前实例终止。然后继续创建下一个实例。
        # 如果没有group.send(None),那么average子生成器永远不会终止,委派生成器也永远不会在此激活,也就不会为result[key]赋值
        group.send(None)
    report(results)


# 输出报告
def report(results):
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit))


data = {
    'girls;kg': [40, 41, 42, 43, 44, 54],
    'girls;m': [1.5, 1.6, 1.8, 1.5, 1.45, 1.6],
    'boys;kg': [50, 51, 62, 53, 54, 54],
    'boys;m': [1.6, 1.8, 1.8, 1.7, 1.55, 1.6],
}

if __name__ == '__main__':
    main(data)

4、 生成器作用

  1. 易于实施

    实现幂序列

    # 使用迭代器来实现
    class PowTowIter:
        def __init__(self, max_):
            self.max_ = max_
    
        def __iter__(self):
            self.n = 0
            return self
    
        def __next__(self):
            if self.n > self.max_:
                raise StopIteration
    
            ret = 2 ** self.n
            self.n += 1
            return ret
    
    
    it = iter(PowTowIter(20))
    for i in it:
        print(i)
    
    
    # 使用生成器来实现
    def powTwoGen(max_):
        n = 0
        while n < max_:
            yield 2 ** n
            n += 1
    
    
    for i in powTwoGen(20):
        print(i)
    
  2. 节省内存

    一个普通的返回序列的函数会在返回结果之前在内存中创建整个序列。如果序列中的项目数量很大,会影响效率

    而这种序列的生成器实现对内存友好,因此是首选的,因为它一次只能生成一项

  3. 表示无限流

    生成器是表示无限数据流的绝佳媒介。无限流无法存储在内存中,并且由于生成器一次只生成一项,因此它可以表示无限数据流

    def create_num():
        init = 0
        while True:
            yield init
            init += 2
    
    
    gen = create_num()
    while True:
        print(gen.__next__())
    
posted @ 2022-05-01 13:57  Kenny_LZK  阅读(71)  评论(0编辑  收藏  举报