• Python核心技术与实战——十五|深入了解迭代器和生成器


    我们在前面应该写过类似的代码

    for i in [1,2,3,4,5]:
        print(i)

    for in 语句看起来很直观,很便于理解,比起C++或Java早起的

    for (int i = 0; i<n;i++)
    printf("d
    ",a[i])

    是不是简洁清晰的多。但是我们有没有想过Python在处理for in语句的时候,具体发生了什么吗?什么样的对象可以被for in用来枚举呢?

    所以,这一节我们就深入到Python的容器类型实现底层看一看,了解一下迭代器和生成器

    前面用过的容器、可迭代对象和迭代器

    容器这个概念还是比较容易理解的,我们说过,Python中一切皆为对象,对象的抽象就是类,而对象的集合就是容器。

    l = [0,1,2]
    t = (0,1,2)
    d = {0:0,1:1,2:2}
    s = set([0,1,2])

    上面的列表,元组、字典和集合都是容器。对于容器,我们可以很直观的想象成很多个元素在一起的单元;而不同容器的区别,正式在于内部数据结构的实现方法。然后我们就可以针对不同的场景,选择不同时间复杂度和空间复杂度的容器。

    所有的容器都是可迭代的(iterable)。这里的迭代,和枚举是完全不同的,这里的迭代可以想象成去买个苹果,卖家并声明他有多少库存,这样,每次去买一个苹果的时候卖家采取的行为不外乎给你拿一个苹果,要么就是告诉你苹果已经卖完了,所以你并不需要知道卖家是怎么在仓库内存放苹果的。

    严谨 都说,迭代器(iterator)提供了一个next的方法,调用这个方法后,要么叨叨容器的下一个对象,要么得到一个StopIteration的错误。我们并不需要想列表一样指定元素的索引,因为字典和集合是没有索引的说法的(字典采用哈希表实现)。我们只需要知道,next函数可以不重复不遗漏的拿到所有的元素就可以了。

    而可迭代对象,是通过iter()函数返回一个迭代器,在通过next()函数就可以实现遍历。for in语句将这个过程隐式化,我们只需要大概知道他怎么做就行了。

    我们再看看下面的代码,主要展示了如何判断一个对象是否可迭代。当然还有一种用法,是isinstance(obj,Iterable)。

    def is_iterable(param):
        try:
            iter(param)
            return True
        except TypeError:
            return False
    
    params = [
        '1234',
        1234,
        [1,2,3,4],
        set([1,2,3,4]),
        {1:1,2:2,3:3},
        (1,2,3,4)
    ]
    
    for param in params:
        print('{} is iterable?{}.'.format(param,is_iterable(param)))
    1234 is iterable?True.
    1234 is iterable?False.
    [1, 2, 3, 4] is iterable?True.
    {1, 2, 3, 4} is iterable?True.
    {1: 1, 2: 2, 3: 3} is iterable?True.
    (1, 2, 3, 4) is iterable?True.
    输出

    通过上面的代码可以发现,在给出的类型中,只有数字1234是不可迭代的,其余数据类型都可以迭代。

    what is 生成器?

    那么生成器又是什么呢?在很多语言中,生成器都没有相对应的模型,所以这里只需要记住一点:生成器就是懒人版的迭代器。

    如果想要在迭代器中枚举他的元素,这些元素要事先生成。这里,我们看看下面的例子

    import os
    import psutil
    
    def show_memory_info(hint):
        pid = os.getpid()
        p = psutil.Process(pid)
    
        info = p.memory_full_info()
        memory = info.uss / 1024. /1024
        print('{} memory used:{}MB'.format(hint,memory))
    
    def test_iterator():
        show_memory_info('initing iterator')
        list_1 = [i for i in range(100000000)]
        show_memory_info('after iterator initiated')
        print(sum(list_1))
    
    def test_generator():
        show_memory_info('intiting generator')
        list_2 = (i for i in range(100000000))
        show_memory_info('after generator initiated')
        print(sum(list_2))
        show_memory_info('after sum called')
    
    test_iterator()
    test_generator()
    initing iterator memory used:7.21875MB
    after iterator initiated memory used:1848.28515625MB
    4999999950000000
    intiting generator memory used:1.7109375MB
    after generator initiated memory used:1.7421875MB
    4999999950000000
    after sum called memory used:2.109375MB
    输出
    
    

    我们用[i for i in range(100000000)]声明了一个包含一个亿元素的列表(声明了一个迭代器),每个元素在生成以后都保存在内存里,通过代码可以发现他们占用了巨大的内存空间,如果内存不够的话就直接OOM错误了。

    不过,我们并不需要在内存中同事保存这么多东西,比方元素求和,我们只需要知道每个元素相加的那一刻是多少就可以了,用完扔掉即可。

    于是,生成器在这里就体现出作用了。在我们调用next()函数的时候,才会生成下一个变量,生成器在Python中的写法是用小括号括起来:

    l = (i for i in range(100000000))

    这样一来,可以清晰的看到生成器是不会像迭代器一样占用大量内存的,只有在被使用的时候才会调用,而且生成器在初始化的时候,并不需要一次生成操作,相比于例子中的第一个测试函数,第二个函数节省了一次生成一亿个元素的过程,因此耗时明显变短。

    此外,生成器并不是单单节省了时间和计算机资源,我们可以看看下面的例子。

    生成器还有什么作用?

    数学中有一个恒等式:

    (1+2+3+...+n)^2 = 1^3+2^3+3^3+...+n^3

    如果我们想验证一下他,要怎么码代码呢?

    def generator(k):
        i = 1
        while True:
            yield i**k
            i += 1
    
    gen_1 = generator(1)
    gen_3 = generator(3)
    
    print(gen_1)
    print(gen_3)
    
    def get_sum(n):
        sum_1 ,sum_3 = 0,0
        for i in range(n):
            next_1 = next(gen_1)
            next_3 = next(gen_3)
            print('next_1 = {},next_3 = {}'.format(next_1,next_3))
            sum_1 += next_1
            sum_3 += next_3
        print(sum_1*sum_1,sum_3)
    
    get_sum(10)

    首先,可以注意一下generator()这个函数,他返回了一个生成器。

    下面的yield可以说是程序的关键,因为函数运行到这里的时候,是会暂停的,然后跳出到next()函数处。而i**k则成了next()函数的返回值。

    这样,每次next(gen)函数被调用的时候,暂停的程序就重新复活,从yield处向下继续执行,同时要注意的是,局部变量i并没有被清除掉,而是会继续累加,所以next_1和next_3是不停变化的。

    所以说,这个生成器是可以无限制的一直进行下去。迭代器是一个有限的集合,而生成器则可以视为一个无限集合,我们只管调用next()就可以,生成器根据运算会自动生成新的元素返回,非常方便。

    我们看看下面这段代码

    def index_normal(L,target):
        result = []
        for i,num in enumerate(L):
            if num == target:
                result.append(i)
        return result
    
    print(index_normal([1,2,3,4,5,6,7,8,2],2))

    就是获取列表中和指定元素相同的索引值组成的列表,那我们用下面的方法是不是简单的多?

    def index_generator(L,target):
        for i,num in enumerate(L):
            if num == target:
                yield i
    
    print(list(index_generator([1,2,3,4,5,6,7,8,2],2)))

    到这里就不用多做解释了,唯一需要强调的是index_generator会返回一个Generator对象,需要用list转换为列表后才能打印。

    这里有个事情要强调

    在Python的语言规范中,用更少,更加清晰的代码实现相同的功能一直是被推崇的办法。因为这样能够很有效的提高代码的可读性,减少出错概率,也能方便他人快速准确的理解作者的意图。但是,这里的“更少”的前提是清晰,而非使用更多的魔术操作,即便减少了代码反而增加了阅读的难度。

    回归正题,我们看一看这样的问题,给定两个序列,判定第一个序列是不是第二个序列的子序列。(子序列,一个列表的元素在第二个列表中按顺序出现,注意按顺序,比方[1,3,5]是[1,2,3,4,5]的子序列,而[1,5,3]就不是)

    a = ['a','c','d']
    b = ['a','b','c','d','e']
    
    def is_subsequence(list_1,list_2):
        if not list_1:
            return True
        else:
            for x in list_1:
                if not (x in list_2):
                    return False
                else:
                    if is_subsequence(list_1[list_1.index(x)+1:],list_2[list_2.index(x)+1:]):
                        return 'list_1 is subsequence of list_2'
                    else:
                        return 'list_1 is not subsequence of list_2'
    print(is_subsequence(a,b))

    上面的代码就是用了叫做‘贪心算法’的常规算法,我们维护两个指针指向两个列表的最开始,然后对第二个列表一路扫过去,如果某个数字和第一个指针指的一样,那么就把第一个指针前进一步。直到一个指针移出第一个序列。

    那么我们要用生成器和迭代器的方法该怎么实现呢?

    我们先看一个极简版的

    a = ['a','c','d']
    b = ['a','b','c','d','e']
    a1 = ['a','d','c']
    
    def is_subsequence(a,b):
        b = iter(b)
        return all(i in b for i in a)
    print(is_subsequence(a,b))
    print(is_subsequence(a1,b))

    看完是不是感觉一脸蒙逼?没关系,我们把他复杂化,一步一步看

    a = ['a','c','d']
    b = ['a','b','c','d','e']        
    def is_subsequence(a,b):
        b = iter(b)
        print(b)
    
        gen = (i for i in a)
        print(gen)
        
        for i in gen:
            print(i)
    
        gen = ((i in b)for i in a)
        print(gen)
    
        return all(((i in b)for i in a))
    print(is_subsequence(a,b))

    在函数开始,先把列表b转换成了迭代器,用途后面再讲

    接下来的gen=。。。比较好理解,就是产生一个生成器,这个生成器用来变量列表a,所以可以输出a里的数据。而i in b就需要好好理解了,这里是不是能联想到 for in 语句?

    没错,这里的i in b大概可以等价于下面的代码

    while True:
        val = next(b)
        if val == i:
            yield True

    所以这里就非常巧妙的利用都了生成器的特性,next()函数运行的时候,保存了当前的指针,再看看下面的示例:

    b = (i for i in range(5))
    print(2 in b)
    print(6 in b)
    
    ####输出####
    True
    False

    最后的all()函数就很简单了,他用来判断一个迭代器的元素是否全为True,如果是则返回True,否则就返回False。

    总结

    容器是可迭代对象,可迭代对象调用iter()函数,可以得到一个迭代器,迭代器可以通过next()获得下一个元素从而支持遍历。

    生成器是一种特殊的迭代器(注意反向逻辑是不成立的)。使用生成器,可以写出来更加清晰的代码,合理使用生成器可以有效降低内存使用、优化程序结构、提高程序速度。

    生成器在Python2的版本上是协程的一种重要实现方式,而在3.5引入asyncawait语法糖后,生成器实现协程的方式就已经落后了。

    课后思考

    对于一个有限元素的生成器,如果迭代完成后,继续调用next()会发生什么呢?生成器可以遍历多次么?

  • 相关阅读:
    调试一个 Bus error 错误
    推荐10个我在用的Chrome浏览器插件
    六个让你的Ubuntu系统提速的方法
    6个可以隐藏运行bat,浏览器等程序的方法
    Windows下5个简单快速加密文件的方法
    如何获取与安装你需要的android 应用
    一款免费的.NET智能持续测试运行工具——MightyMoose
    给 mysql 系统表加上 trigger
    2011百度数据挖掘研发工程师实习生笔试面试题
    Linux常用操作命令整理
  • 原文地址:https://www.cnblogs.com/yinsedeyinse/p/11848287.html
Copyright © 2020-2023  润新知