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

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

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

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

for (int i = 0; i<n;i++)
printf("d\n",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()会发生什么呢?生成器可以遍历多次么?

posted @ 2019-11-15 13:03  银色的音色  阅读(259)  评论(0编辑  收藏  举报