• 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()会发生什么呢?生成器可以遍历多次么?

  • 相关阅读:
    手机号码正则表达式
    POJ 3233 Matrix Power Series 矩阵快速幂
    UVA 11468
    UVA 1449
    HDU 2896 病毒侵袭 AC自动机
    HDU 3065 病毒侵袭持续中 AC自动机
    HDU 2222 Keywords Search AC自动机
    POJ 3461 Oulipo KMP模板题
    POJ 1226 Substrings KMP
    UVA 1455 Kingdom 线段树+并查集
  • 原文地址:https://www.cnblogs.com/yinsedeyinse/p/11848287.html
Copyright © 2020-2023  润新知