测试开发之Python核心笔记(15):迭代器与生成器

15.1 可迭代对象Iterable

还记得for循环吗?for循环可以循环迭代处理字符串以及列表、元组、字典、集合这些容器类型,知道为什么这些数据类型可以被for迭代吗?因为这些对象都是可迭代对象。

判断是否是可迭代对象,可以isinstance(obj, Iterable)判断,输出True表示obj对象是可迭代的(iterable)。

15.2 迭代器iterator

通过迭代器,程序员可以迭代非序列类型,就是除了列表、元素、字典和集合之外类型。

迭代器通过next()方法获取对象中的下一个元素,可以把它看做一个数据流,我们不知道他的长度,一边使用一边计算下一个数据,是一种惰性计算。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。

迭代器的主要优点是节约内存(循环过程中,数据不用一次读入,在处理文件对象时特别有用,因为文件也是迭代器对象)、不依赖索引取值、实现惰性计算(需要时再取值计算);

with open('java.txt') as f:
    for line in f:
        print(line)

这样每次读取一行处理一行,而不是一次性将整个文件读入,节约内存。

迭代器有个缺点,就是只能遍历一遍,不能遍历第二遍。因为遍历访问时,只能向前一个一个访问数据,直到访问到没有数据了,不能回头。也就是说迭代器只能往前不能后退。

l = ['a', 'n', 's', 'd']
l_iterator = iter(l)  # 通过iter函数得到一个迭代器
for i in l_iterator:
    print(i)

for i in l_iterator:  # 经过第一遍迭代后,迭代器中已经没有数据了,或者说已经访问到尾部了。
    print(i)

15.2.1 通过 iter() 函数得到迭代器

tuple、list、dict、str虽然是Iterable,却不是迭代器Iterator。可以通过 iter() 函数返回一个迭代器。通过 next() 方法就可以实现遍历,调用next方法,要么得到这个容器的下一个对象,要么得到一个 StopIteration 的错误。Python的for循环迭代tuple、list本质上是先将他们转成迭代器Iterator,然后通过不断调用next()函数实现的。

l_iterator=iter([1,2,3,4,5])

15.2.1 通过类实现迭代器

在Python中有很多通过类实现的迭代器,比如reversed(),enumerate(),通过查看源码可以发现,这些类都实现了__next__方法 和 __iter__ 方法。

如果你想让一个类对象可以被迭代,那么把一个类实现成一个迭代器,就是让类继承于Iterable,然后重写两个方法__next__方法 和 __iter__ 方法。

__iter__ 方法返回一个特殊的迭代器对象。

__next__ 会返回下一个迭代器对象。

例如,实现一个递减迭代器,对某个正整数,依次递减 1,直到 0。

from collections.abc import Iterable


class Decrease(Iterable):
    def __init__(self, init):
        self.init = init

    def __iter__(self):  # 返回对象本身
        return self

    def __next__(self):
        while 0 < self.init:
            self.init -= 1
            return self.init # 返回下一个
        raise StopIteration  #  通过 raise 终断next


for i in Decrease(6):  # 可以用for循环迭代这个类对象了
    print(i)

15.3 itertools 模块

这个模块实现了一系列快速、高效的迭代器。这些迭代器本身或组合都很有用。这一小节就来介绍一些非常好用的迭代器。

Help(itertools)可以看到内置的迭代器。如果使用Pycharm编辑器,按两下Shift,输入itertools,勾选上Include non-project items,在弹出的页面上选择itertools文件,就可以进入itertools.py文件。你会看到这样内容,这里列出来了itertools模块提供的所有迭代器:

"""
Functional tools for creating and using iterators.

Infinite iterators:
count(start=0, step=1) --> start, start+step, start+2*step, ...
cycle(p) --> p0, p1, ... plast, p0, p1, ...
repeat(elem [,n]) --> elem, elem, elem, ... endlessly or up to n times

Iterators terminating on the shortest input sequence:
accumulate(p[, func]) --> p0, p0+p1, p0+p1+p2
chain(p, q, ...) --> p0, p1, ... plast, q0, q1, ... 
chain.from_iterable([p, q, ...]) --> p0, p1, ... plast, q0, q1, ... 
compress(data, selectors) --> (d[0] if s[0]), (d[1] if s[1]), ...
dropwhile(pred, seq) --> seq[n], seq[n+1], starting when pred fails
groupby(iterable[, keyfunc]) --> sub-iterators grouped by value of keyfunc(v)
filterfalse(pred, seq) --> elements of seq where pred(elem) is False
islice(seq, [start,] stop [, step]) --> elements from
       seq[start:stop:step]
starmap(fun, seq) --> fun(*seq[0]), fun(*seq[1]), ...
tee(it, n=2) --> (it1, it2 , ... itn) splits one iterator into n
takewhile(pred, seq) --> seq[0], seq[1], until pred fails
zip_longest(p, q, ...) --> (p[0], q[0]), (p[1], q[1]), ... 

Combinatoric generators:
product(p, q, ... [repeat=1]) --> cartesian product
permutations(p[, r])
combinations(p, r)
combinations_with_replacement(p, r)
"""

下面我们重点学习其中的几个迭代器。

15.3.1 拼接迭代器chain

可将多个可迭代对象变成为单个迭代器。在Pycharm的导航栏中,选择Navigate——>File Structure,就可以列出itertools.py模块中所有的类。

在这里插入图片描述

点击chain类,可以看到chain的说明:

class chain(object):
    """
    chain(*iterables) --> chain object
    
    Return a chain object whose .__next__() method returns elements from the
    first iterable until it is exhausted, then elements from the next
    iterable, until all of the iterables are exhausted.
    """

文档的意思就是说,创建一个迭代器,它首先返回第一个可迭代对象中所有元素,接着返回下一个可迭代对象中所有元素,直到耗尽所有可迭代对象中的元素。可将多个可迭代对象变成为单个迭代器。举几个例子,在Pycharm中编辑:

from itertools import chain

chain_iterator = chain("ABCDef", "1234")  # 两个可迭代对象:"ABCDef"和"1234"
print(list(chain_iterator))  # ['A', 'B', 'C', 'D', 'e', 'f', '1', '2', '3', '4']

使用 chain() 的一个常见场景是,当你想对不同的集合中所有元素执行某些操作的时候。比如,你想对两个列表的元素都计算平方和并形成一个新列表。可以这样做:

from itertools import chain


def square_multi_iterables(*iterables):  # 定义一个生成器
    chain_iterator = chain(*iterables)  # 利用chain生成迭代器
    for l in chain_iterator:  # 对迭代器里面的元素求进行平方
        yield l * l  # yield 返回


if __name__ == '__main__':
    lst1 = [1, 2, 3]
    lst2 = [4, 5, 6]
    for i in square_multi_iterables(lst1, lst2):
        print(i)

    print(list(square_multi_iterables(lst2, lst1)))

这种解决方案要比使用两个单独的循环更加优雅!

15.3.2 累积迭代器accumulate

先看看源码中怎么说:

class accumulate(object):
    """
    accumulate(iterable[, func]) --> accumulate object
    
    Return series of accumulated sums (or other binary function results).
    """

将可迭代对象应用到func函数上,返回可迭代对象的累积迭代器。如果func没有提供,则返回可迭代对象累计和组成的迭代器。

看个例子说明一下:

from itertools import accumulate

lst = [1, 2, 3, 4, 5, 6]
for i in accumulate(lst):
    print(i)
print(list(accumulate(lst)))  # 返回 [1, 3, 6, 10, 15, 21]

这个例子没有提供func函数,默认是对原序列累计求和。再来看看提供func函数的用法。

from itertools import accumulate

lst = [1, 2, 3, 4, 5, 6]
for i in accumulate(lst, lambda x, y: x * y):
    print(i)
print(list(accumulate(lst, lambda x, y: x * y)))  # 返回 [1, 2, 6, 24, 120, 720]

15.3.3 排列组合迭代器

依然是先看看代码怎么说:

class combinations(object):
    """
    combinations(iterable, r) --> combinations object
    
    Return successive r-length combinations of elements in the iterable.
    
    combinations(range(4), 3) --> (0,1,2), (0,1,3), (0,2,3), (1,2,3)
    """

返回由输入iterable可迭代对象中元素组成长度为 r 的子序列。组合按照字典序返回。所以如果输入 iterable 是有序的,生成的组合元组也是有序的。

即使元素的值相同,不同位置的元素也被认为是不同的。如果元素各自不同,那么每个组合中没有重复元素。

举个例子看看,如何根据 “ABCD”, 输出 [‘AB’, ‘AC’, ‘AD’, ‘BC’, ‘BD’, ‘CD’]

def my_combinations(iterables, length):
    for i in combinations(iterables, length):  
        yield "".join(i)


if __name__ == '__main__':
    lst=[]
    for element in my_combinations("ABCD", 2):  # 两个元素组成的排列组合
        lst.append(element)

    print(lst)  # ['AB', 'AC', 'AD', 'BC', 'BD', 'CD']

15.3.3 压缩迭代器compress

看看源码怎么说:

class compress(object):
    """
    compress(data, selectors) --> iterator over selected data
    
    Return data elements corresponding to true selector elements.
    Forms a shorter iterator from selected data elements using the
    selectors to choose the data elements.
    """

创建一个迭代器,它返回 data 中经 selectors 真值测试为 True 的元素。迭代器在两者较短的长度处停止。

print(list(compress('ABCDEF', [1, 0, 1, 0, 1, 1])))  # 输出['A', 'C', 'E', 'F']

相当于是下面的这段代码:

def compress(data, selectors):
    return (d for d, s in zip(data, selectors) if s)

15.3.4 丢弃迭代器dropwhile

看看源码的描述:

class dropwhile(object):
    """
    dropwhile(predicate, iterable) --> dropwhile object
    
    Drop items from the iterable while predicate(item) is true.
    Afterwards, return every element until the iterable is exhausted.
    """

创建一个迭代器,如果 predicate 为true,迭代器丢弃这些元素,然后返回其他元素。迭代器在 predicate 首次为false之前不会产生任何输出,所以可能需要一定长度的启动时间。

举个例子:

from itertools import dropwhile

print(list(dropwhile(lambda x: x < 5, [1, 4, 6, 4, 1])))  # 输出 [6, 4, 1]

大致相当于:

def my_dropwhile(predicate, iterable):
    iterable = iter(iterable)
    for x in iterable:
        if not predicate(x):
            yield x  # 返回第一个不满足predicate的值后退出这个循环
            break
    for x in iterable:  # 接着循环剩下的元素
        yield x


print(list(my_dropwhile(lambda x: x < 5, [1, 4, 6, 4, 1])))  # 输出 [6, 4, 1]

更多的迭代器,可以参考itertools.py源码。

15.3 生成器generator(特殊的迭代器)

带 yield 的函数是生成器,而生成器也是一种迭代器。所以,生成器也有上面那些迭代器的特点。

通俗理解 yield,可结合函数的返回值关键字 return,yield 是一种特殊的 return。说是特殊的 return,是因为执行遇到 yield 时,立即返回,这是与 return 的相似之处。

不同之处在于:下次进入函数时直接到 yield 的下一个语句,而 return 后再进入函数,还是从函数体的第一行代码开始执行。

声明一个迭代器很简单,通过列表解析式[i for i in range(100000000)]就可以生成一个包含一亿个元素的列表。每个元素在生成后都会保存到内存中(这个过程很慢),而且占用了巨量的内存,内存不够的话就会出现 OOM 错误。不过,有的时候,我们并不需要提前在内存中保存这么多东西,比如对元素累加求和,我们只需要知道每个元素在相加的那一刻是多少就行了,用完就可以扔掉了。

生成器generator不会提前生成好所有的值放到内存中,只有调用next方法(生成器的方法)时,才会生成下一个变量,从而加快代码执行速度并节省内存

得到生成器的方式有两种:

15.3.1 小括号定义生成器

将[]换成(),就可以简单地把列表解析式改成生成器generator,就得到一个生成器(i for i in range(100000000))。列表解析式最常用,但是尽量把列表解析式变成生成器,因为这样节省内存,节省内存的思想应该处处体现在代码里,这样才能体现水平。

15.3.2 通过yield关键字定义生成器

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

在每次调用next()的时候执行,遇到yield语句暂停,返回yeild表达式后面的值,再次调用next时从yield语句处继续执行。

15.3.3 生成器的价值

生成器除了可以利用惰性计算来节约内存,还能提高代码的可读性。例如,求一段文本中,每个单词的的起始下标。不用生成器的方案:

def index_words(text):
    result = []  # 存放下标
    if text:
        result.append(0)
    for index, letter in enumerate(text, 1):  # 第二个参数是1,如果不写是什么情况
        if letter == " ":
            result.append(index)
    return result


if __name__ == '__main__':
    enumerate_desc = "The enumerate object yields pairs containing a count (from start, which defaults to zero) and a value yielded by the iterable argument."
    print(index_words(enumerate_desc))

如果使用生成器:

def index_words(text):
    if text:
        yield 0  # 第一次返回
    for index, letter in enumerate(text, 1): 
        if letter == " ":
            yield index  # 每次调用返回


if __name__ == '__main__':
    enumerate_desc = "The enumerate object yields pairs containing a count (from start, which defaults to zero) and a value yielded by the iterable argument."
    for index in index_words(enumerate_desc):
        print(index)
    print(list(index_words(enumerate_desc)))

可以使用生成器的代码更加清晰,代码行数更少。在不使用生成器时,对于每次结果,首先要执行一个append操作,最终才返回result。而使用生成器时候,直接yield index,少了列表操作带来的干扰,一眼就能看出,代码是要返回index。

kafka消费数据可以用yield生成器。还有pytest的fixture函数中也用到了,还有启动webdrvier也是用yield。

工作中一定要多多使用生成器。但是要注意,因为生成器也是迭代器,因此生成器只能遍历一次

15.4 练习题

  1. 求列表中某一个元素的所有下标 index组成的列表
def index_generator(L, target):
    for i, num in enumerate(L):
        if num == target:
            yield i

print(list(index_generator([1, 6, 2, 4, 5, 2, 8, 6, 3, 2], 2)))

########## 输出 ##########

[2, 5, 9]

index_generator 会返回一个 Generator 对象,需要使用 list 转换为列表后,才能用 print 输出。

  1. 给定两个序列,判定第一个是不是第二个的子序列。

LeetCode 链接如下:https://leetcode.com/problems/is-subsequence/ 。序列就是列表,子序列则指的是,一个列表的元素在第二个列表中都按顺序出现,但是并不必挨在一起。举个例子,[1, 3, 5] 是 [1, 2, 3, 4, 5] 的子序列,[1, 4, 3] 则不是。

要解决这个问题,常规算法是贪心算法。维护两个指针指向两个列表的最开始,然后对第二个序列一路扫过去,如果某个数字和第一个指针指的一样,那么就把第一个指针前进一步。第一个指针移出第一个序列最后一个元素的时候,返回 True,否则返回 False。

不过本小节,使用生成器来实现。

def is_subsequence(a, b):
    b = iter(b)
    return all(i in b for i in a)

print(is_subsequence([1, 3, 5], [1, 2, 3, 4, 5]))
print(is_subsequence([1, 4, 3], [1, 2, 3, 4, 5]))

########## 输出 ##########

True
False

猜你喜欢

转载自blog.csdn.net/liuchunming033/article/details/107896326