Python3爬虫系列06 (理论) - 可迭代对象、迭代器、生成器

  • 原创
  • Madman
  • /
  • 2018-10-06 16:30
  • /
  • 0
  • 353 次阅读

spider 06-min.jpg

Synopsis: Python中内置的序列,如list、tuple、str、bytes、dict、set、collections.deque等都是可迭代的,但它们不是迭代器。迭代器可以被 next() 函数调用,并不断返回下一个值。Python从可迭代的对象中获取迭代器。迭代器和生成器都是为了惰性求值(lazy evaluation),避免浪费内存空间,实现高效处理大量数据。在Python 3中,生成器有广泛的用途,所有生成器都是迭代器,因为生成器完全实现了迭代器接口。迭代器用于从集合中取出元素,而生成器用于"凭空"生成元素 。PEP 342 给生成器增加了 send() 方法,实现了"基于生成器的协程"。PEP 380允许生成器中可以return返回值,并新增了 yield from 语法结构,打开了调用方和子生成器的双向通道

代码已上传到 https://github.com/wangy8961/python3-concurrency ,欢迎star

1. 可迭代的对象

可迭代的对象(Iterable)是指使用iter()内置函数可以获取迭代器(Iterator)的对象。Python解释器需要迭代对象x时,会自动调用iter(x),内置的iter()函数有以下作用:

  1. 检查对象x是否实现了__iter__()方法,如果实现了该方法就调用它,并尝试获取一个迭代器
  2. 如果没有实现__iter__()方法,但是实现了__getitem__(index)方法,尝试按顺序(从索引0开始)获取元素,即参数index是从0开始的整数(int)。之所以会检查是否实现__getitem(index)__方法,为了向后兼容
  3. 如果前面都尝试失败,Python会抛出TypeError异常,通常会提示'X' object is not iterable(X类型的对象不可迭代),其中X是目标对象所属的类

具体来说,哪些是可迭代对象呢?

  • 如果对象实现了能返回迭代器__iter__()方法,那么对象就是可迭代的
  • 如果对象实现了__getitem__(index)方法,而且index参数是从0开始的整数(索引),这种对象也可以迭代的。Python中内置的序列类型,如list、tuple、str、bytes、dict、set、collections.deque等都可以迭代,原因是它们都实现了__getitem__()方法(注意: 其实标准的序列还都实现了__iter__()方法)

1.1 判断对象是否可迭代

从Python 3.4开始,检查对象x能否迭代,最准确的方法是:调用iter(x)函数,如果不可迭代,会抛出TypeError异常。这比使用isinstance(x, abc.Iterable)更准确,因为iter(x)函数会考虑到遗留的__getitem__(index)方法,而abc.Iterable类则不会考虑

1.2 __getitem__()

下面构造一个类,它实现了__getitem__()方法。可以给类的构造方法传入包含一些文本的字符串,然后可以逐个单词进行迭代:

'''创建test.py模块'''
import re
import reprlib

RE_WORD = re.compile('\w+')

class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __getitem__(self, index):
        return self.words[index]

    def __len__(self):  # 为了让对象可以迭代没必要实现这个方法,这里是为了完善序列协议,即可以用len(s)获取单词个数
        return len(self.words)

    def __repr__(self):
        return 'Sentence({})'.format(reprlib.repr(self.text))

测试Sentence实例能否迭代:

In [1]: from test import Sentence  # 导入刚创建的类

In [2]: s = Sentence('I love Python')  # 传入字符串,创建一个Sentence实例

In [3]: s
Out[3]: Sentence('I love Python')

In [4]: s[0]
Out[4]: 'I'

In [5]: s.__getitem__(0)
Out[5]: 'I'

In [6]: for word in s:  # Sentence实例可以迭代
   ...:     print(word)
   ...:     
I
love
Python

In [7]: list(s)  # 因为可以迭代,所以Sentence对象可以用于构建列表和其它可迭代的类型
Out[7]: ['I', 'love', 'Python']

In [8]: from collections import abc

In [9]: isinstance(s, abc.Iterable)  # 不能正确判断Sentence类的对象s是可迭代的对象
Out[9]: False

In [10]: iter(s)  # 没有抛出异常,返回迭代器,说明Sentence类的对象s是可迭代的
Out[10]: <iterator at 0x7f82a761e5f8>

1.3 __iter__()

如果实现了__iter__()方法,但该方法没有返回迭代器时:

In [1]: class Foo:
   ...:     def __iter__(self):
   ...:         pass
   ...:     

In [2]: from collections import abc

In [3]: f = Foo()

In [4]: isinstance(f, abc.Iterable)  # 错误地判断Foo类的对象f是可迭代的对象
Out[4]: True

In [5]: iter(f)  # 使用iter()方法会抛出异常,即对象f不可迭代,不能用for循环迭代它
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-a2fd621ca1d7> in <module>()
----> 1 iter(f)

TypeError: iter() returned non-iterator of type 'NoneType'

Python迭代协议要求__iter__()必须返回特殊的迭代器对象。下一节会讲迭代器,迭代器对象必须实现__next__()方法,并使用StopIteration异常来通知迭代结束

In [1]: class Foo:
   ...:     def __iter__(self):  # 其实是将迭代请求委托给了列表
   ...:         return iter([1, 2, 3])  # iter()函数从列表创建迭代器,等价于[1, 2, 3].__iter__()
   ...:     

In [2]: from collections import abc

In [3]: f = Foo()

In [4]: isinstance(f, abc.Iterable)
Out[4]: True

In [5]: iter(f)
Out[5]: <list_iterator at 0x7fbe0e4f2d30>

In [6]: for i in f:
   ...:     print(i)
   ...:     
1
2
3

1.4 iter()函数的补充

iter()函数有两种用法:

  • iter(iterable) -> iterator: 传入可迭代的对象,返回迭代器
  • iter(callable, sentinel) -> iterator: 传入两个参数,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符

下述示例展示如何使用iter()函数的第2种用法来掷骰子,直到掷出 1 点为止:

In [1]: from random import randint

In [2]: def d6():
   ...:     return randint(1, 6)
   ...:

In [3]: d6_iter = iter(d6, 1)  # 第一个参数是d6函数,第二个参数是哨符

In [4]: d6_iter  # 这里的 iter 函数返回一个 callable_iterator 对象
Out[4]: <callable_iterator at 0x473c5d0>

In [5]: for roll in d6_iter:  # for 循环可能运行特别长的时间,不过肯定不会打印 1,因为 1 是哨符
   ...:     print(roll)
   ...:
6
3
5
2
4
4

实用的示例: 逐行读取文件,直到遇到空行或者到达文件末尾为止

with open('mydata.txt') as fp:
    for line in iter(fp.readline, '\n'):  # fp.readline每次返回一行
        print(line)

1.5 Iterable reducing functions

有一些函数接受一个可迭代的对象,然后返回单个结果。下表中列出的每个内置函数都可以使用functools.reduce函数实现,内置是因为使用它们便于解决常见的问题。此外,对allany函数来说,有一项重要的优化措施是reduce函数做不到的:这两个函数会短路(即一旦确定了结果就立即停止使用迭代器):

模块 函数 说明
functools reduce(function, sequence[, initial]) Apply a function of two arguments cumulatively to the items of a sequence, from left to right, so as to reduce the sequence to a single value. For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates ((((1+2)+3)+4)+5)
(内置) all(iterable, /) Return True if bool(x) is True for all values x in the iterable. If the iterable is empty, return True.
(内置) any(iterable, /) Return True if bool(x) is True for any x in the iterable. If the iterable is empty, return False.
(内置) min(iterable, *[, default=obj, key=func]) With a single iterable argument, return its smallest item. If the provided iterable is empty, return the default obj.
(内置) max(iterable, *[, default=obj, key=func]) With a single iterable argument, return its biggest item. If the provided iterable is empty, return the default obj.
(内置) sum(iterable, start=0, /) Return the sum of a 'start' value (default: 0) plus an iterable of numbers. When the iterable is empty, return the start value.

2. 迭代器

迭代是数据处理的基石。当扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式(Iterator pattern)

迭代器是这样的对象:实现了无参数的__next__()方法,返回序列中的下一个元素,如果没有元素了,就抛出StopIteration异常。即,迭代器可以被next()函数调用,并不断返回下一个值。

在 Python 语言内部,迭代器用于支持:

  • for 循环
  • 构建和扩展集合类型
  • 逐行遍历文本文件
  • 列表推导、字典推导和集合推导
  • 元组拆包
  • 调用函数时,使用 * 拆包实参

2.1 判断对象是否为迭代器

检查对象x是否为迭代器最好的方式是调用 isinstance(x, abc.Iterator)

In [1]: from collections import abc

In [2]: isinstance([1,3,5], abc.Iterator)
Out[2]: False

In [3]: isinstance((2,4,6), abc.Iterator)
Out[3]: False

In [4]: isinstance({'name': 'wangy', 'age': 18}, abc.Iterator)
Out[4]: False

In [5]: isinstance({1, 2, 3}, abc.Iterator)
Out[5]: False

In [6]: isinstance('abc', abc.Iterator)
Out[6]: False

In [7]: isinstance(100, abc.Iterator)
Out[7]: False

In [8]: isinstance((x*2 for x in range(5)), abc.Iterator)  # 生成器表达式,后续会介绍
Out[8]: True

Python中内置的序列类型,如list、tuple、str、bytes、dict、set、collections.deque等都是可迭代的对象,但不是迭代器生成器一定是迭代器

2.2 __next__()__iter__()

标准的迭代器接口:

  • __next__(): 返回下一个可用的元素,如果没有元素了,抛出StopIteration异常。调用next(x)相当于调用x.__next__()
  • __iter__(): 返回迭代器本身(self),以便在应该使用可迭代的对象的地方能够使用迭代器,比如在for循环、list(iterable)函数、sum(iterable, start=0, /)函数等应该使用可迭代的对象地方可以使用迭代器说明: 如章节1所述,只要实现了能返回迭代器__iter__()方法的对象就是可迭代的对象,所以,迭代器都是可迭代的对象

下面的示例中,Sentence类的对象是可迭代的对象,而SentenceIterator类实现了典型的迭代器设计模式:

import re
import reprlib

RE_WORD = re.compile('\w+')

class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        return SentenceIterator(self.words)  # 迭代协议要求__iter__返回一个迭代器


class SentenceIterator:
    def __init__(self, words):
        self.words = words
        self.index = 0

    def __next__(self):
        try:
            word = self.words[self.index]  # 获取 self.index 索引位(从0开始)上的单词。
        except IndexError:
            raise StopIteration()  # 如果 self.index 索引位上没有单词,那么抛出 StopIteration 异常
        self.index += 1
        return word

    def __iter__(self):
        return self  # 返回迭代器本身

2.3 next()函数获取迭代器中下一个元素

除了可以使用for循环处理迭代器中的元素以外,还可以使用next()函数,它实际上是调用iterator.__next__(),每调用一次该函数,就返回迭代器的下一个元素。如果已经是最后一个元素了,再继续调用next()就会抛出StopIteration异常。一般来说,StopIteration异常是用来通知我们迭代结束的:

with open('/etc/passwd') as fd:
    try:
        while True:
            line = next(fd)
            print(line, end='')
    except StopIteration:
        pass

或者,为next()函数指定第二个参数(默认值),当执行到迭代器末尾后,返回默认值,而不是抛出异常:

with open('/etc/passwd') as fd:
    while True:
        line = next(fd, None)
        if line is None:
            break
        print(line, end='')

2.4 可迭代的对象与迭代器的对比

首先,我们要明确可迭代的对象迭代器之间的关系:Python从可迭代的对象中获取迭代器

比如,用for循环迭代一个字符串'ABC',字符串是可迭代的对象for循环的背后会先调用iter(s)将字符串转换成迭代器,只不过我们看不到:

In [1]: s = 'ABC'

In [2]: for char in s:
   ...:     print(char)
   ...:
A
B
C

如果没有for循环,就不得不使用while循环来模拟:

In [3]: it = iter(s)  # 使用可迭代的对象s构建迭代器it

In [4]: while True:
   ...:     try:
   ...:         print(next(it))  # 不断在迭代器上调用next函数,获取下一个字符
   ...:     except StopIteration:  # 如果没有字符了,迭代器会抛出StopIteration异常
   ...:         del it
   ...:         break
   ...:
A
B
C

StopIteration异常表明迭代器到头了,Python语言内部会处理for循环和其它迭代上下文(如列表推导、元组拆包等)中的StopIteration异常

使用章节2.2中定义的Sentence类,演示如何使用iter()函数来构建迭代器,并使用next()函数依次获取迭代器中的元素:

In [1]: from test import Sentence

In [2]: s = Sentence('Pig and Pepper')

In [3]: it = iter(s)  # 获取迭代器

In [4]: it
Out[4]: <iterator at 0x4148650>

In [5]: next(it)  # 使用next()方法获取下一个单词
Out[5]: 'Pig'

In [6]: it.__next__()  # __next__()方法也能达到效果,但我们应该避免直接调用特殊方法
Out[6]: 'and'

In [7]: next(it)
Out[7]: 'Pepper'

In [8]: next(it)  # 没有单词了,因此迭代器抛出StopIteration异常
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-8-bc1ab118995a> in <module>()
----> 1 next(it)

StopIteration:

In [9]: list(it)  # 到头后,迭代器就没用了
Out[9]: []

In [10]: list(iter(s))  # 如果想再次迭代,要重新构建迭代器
Out[10]: ['Pig', 'and', 'Pepper']

总结:

  • 迭代器要实现__next__()方法,返回迭代器中的下一个元素
  • 迭代器还要实现__iter__()方法,返回迭代器本身,因此,迭代器可以迭代。迭代器都是可迭代的对象
  • 可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现__iter__()方法,但不能实现__next__()方法

3. 生成器

在Python中,可以使用生成器让我们在迭代的过程中不断计算后续的值,而不必将它们全部存储在内存中:

'''斐波那契数列由0和1开始,之后的费波那契系数就是由之前的两数相加而得出,它是一个无穷数列'''
def fib():  # 生成器函数
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

g = fib()  # 调用生成器函数,返回一个实现了迭代器接口的生成器对象,生成器一定是迭代器
counter = 1
for i in g:  # 可以迭代生成器
    print(i)  # 每需要一个值时,才会去计算生成
    counter += 1
    if counter > 10:  # 只生成斐波那契数列前10个数值
        break

3.1 生成器函数

只要 Python 函数的定义体中有 yield 关键字,该函数就是生成器函数(2001年,Python 2.2 通过了 PEP 255 -- Simple Generators 引入了yield关键字)。调用生成器函数时,会返回一个生成器(generator)对象。也就是说,生成器函数是生成器工厂

普通的函数与生成器函数在语法上唯一的区别是,在后者的定义体中有 yield 关键字

In [1]: def gen_AB():  # 定义生成器函数的方式与普通的函数无异,只不过要使用 yield 关键字
   ...:     print('start')
   ...:     yield 'A'
   ...:     print('continue')
   ...:     yield 'B'
   ...:     print('end')
   ...:

In [2]: gen_AB  # 生成器函数
Out[2]: <function __main__.gen_AB()>

In [3]: g = gen_AB()  # 调用生成器函数,返回一个生成器对象,注意:此时并不会执行生成器函数定义体中的代码,所以看不到打印start

In [4]: g
Out[4]: <generator object gen_AB at 0x04CA74E0>

In [5]: next(g)  # 生成器都是迭代器,执行next(g)时生成器函数会向前,前进到函数定义体中的下一个 yield 语句,生成 yield 关键字后面的表达式的值,在函数定义体的当前位置暂停,并返回生成的值
start
Out[5]: 'A'

In [6]: next(g)
continue
Out[6]: 'B'

In [7]: next(g)
end
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-7-e734f8aca5ac> in <module>()
----> 1 next(g)

StopIteration:

调用生成器函数后会创建一个新的生成器对象,但是此时还不会执行函数体。

第一次执行next(g)时,会激活生成器生成器函数会向前 前进到 函数定义体中的 下一个 yield 语句生成 yield 关键字后面的表达式的值,在函数定义体的当前位置暂停,并返回生成的值。具体为:

  • 执行print('start')输出start
  • 执行yield 'A',此处yield关键字后面的表达式为'A',即表达式的值为A。所以整条语句会生成值A,在函数定义体的当前位置暂停,并返回值A,我们在控制台上看到输出A

第二次执行next(g)时,生成器函数定义体中的代码由 yield 'A' 前进到 yield 'B',所以会先输出continue,并生成值B,又在函数定义体的当前位置暂停,返回值B

第三次执行next(g)时,由于函数体中没有另一个 yield 语句,所以前进到生成器函数的末尾,会先输出end。到达生成器函数定义体的末尾时,生成器对象抛出StopIteration异常

注意用词: 普通函数返回值,调用生成器函数返回生成器,生成器产出生成

调用生成器函数后,会构建一个实现了迭代器接口的生成器对象,即,生成器一定是迭代器

In [8]: for c in gen_AB():
   ...:     print('-->', c)
   ...:
start
--> A
continue
-->
end

所以,可以使用生成器函数改写章节2.2中的Sentence类,此时不再需要SentenceIterator类:

import re
import reprlib

RE_WORD = re.compile('\w+')

class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        # 最简单是委托迭代给列表,这里仅演示生成器函数的用法
        # return iter(self.words)  # 等价于self.words.__iter__()
        for word in self.words:
            yield word  # 产出当前的word

迭代器生成器都是为了惰性求值(lazy evaluation),避免浪费内存空间。而上面的Sentence类却不具备惰性,因为RE_WORD.findall(text)会创建所有匹配项的列表,然后将其绑定到 self.words 属性上。如果我们传入一个非常大的文本,那么该列表使用的内存量可能与文本本身一样多,而假设我们只需要迭代前几个单词,那么将浪费大量的内存。

re.finditer 函数是 re.findall 函数的惰性版本,返回的不是列表,而是一个迭代器,按需生成 re.MatchObject 实例。如果有很多匹配, re.finditer 函数能节省大量内存。我们要使用这个函数让 Sentence 类变得懒惰,即只在需要时才生成下一个单词:

import re
import reprlib

RE_WORD = re.compile('\w+')

class Sentence:
    def __init__(self, text):
        self.text = text

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        # finditer()函数构建一个迭代器,包含 self.text 中匹配 RE_WORD 的单词,产出 MatchObject 实例
        for match in RE_WORD.finditer(self.text):
            yield match.group()  # match.group() 方法从 MatchObject 实例中提取匹配正则表达式的具体文本

3.2 生成器表达式

简单的生成器函数(有yield关键字),可以替换成生成器表达式(没有yield关键字,将列表推导中的[]替换为()即可),让代码变得更简短

生成器表达式可以理解为列表推导的惰性版本:不会迫切地构建列表,而是返回一个生成器,按需惰性生成元素。也就是说,如果列表推导是制造列表的工厂,那么生成器表达式就是制造生成器的工厂:

In [1]: def gen_AB():
   ...:     print('start')
   ...:     yield 'A'
   ...:     print('continue')
   ...:     yield 'B'
   ...:     print('end')
   ...:

In [2]: res1 = [x*3 for x in gen_AB()]  # 列表推导迫切地迭代 gen_AB() 函数生成的生成器对象产出的元素: 'A' 和 'B'。注意,下面的输出是 start、 continue 和 end
start
continue
end

In [3]: for i in res1:  # 这个 for 循环迭代列表推导生成的 res1 列表
   ...:     print('-->', i)
   ...:
--> AAA
--> BBB

In [4]: res2 = (x*3 for x in gen_AB())  # 把生成器表达式返回的值赋值给 res2。只需调用 gen_AB() 函数,虽然调用时会返回一个生成器,但是这里并不使用

In [5]: res2  # res2 是一个生成器对象
Out[5]: <generator object <genexpr> at 0x04599330>

In [6]: for i in res2:  # 只有 for 循环迭代 res2 时, gen_AB 函数的定义体才会真正执行。 for 循环每次迭代时会隐式调用 next(res2),前进到 gen_AB 函数中的下一个 yield 语句。注意, gen_AB 函数的输出与 for 循环中 print 函数的输出夹杂在一起
   ...:     print('-->', i)
   ...:
start
--> AAA
continue
--> BBB
end

可以看出,生成器表达式会产出生成器,因此可以使用生成器表达式进一步减少Sentence类的代码:

import re
import reprlib

RE_WORD = re.compile('\w+')

class Sentence:
    def __init__(self, text):
        self.text = text

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):  # 不再是生成器函数了(没有 yield),而是使用生成器表达式构建生成器
        return (match.group() for match in RE_WORD.finditer(self.text))

何时使用生成器表达式

生成器表达式是创建生成器的简洁语法,这样无需先定义函数再调用。不过,生成器函数灵活得多,可以使用多个语句实现复杂的逻辑,也可以作为协程使用(后续博文介绍)

遇到简单的情况时,可以使用生成器表达式,因为这样扫一眼就知道代码的作用。如果生成器表达式要分成多行写,我倾向于定义生成器函数,以便提高可读性。此外,生成器函数有名称,因此可以重用

如果将生成器表达式传入只有一个参数的函数时,可以省略生成器表达式外面的()

In [1]: list((x*2 for x in range(5)))
Out[1]: [0, 2, 4, 6, 8]

In [2]: list(x*2 for x in range(5))  # 可以省略生成器表达式外面的()
Out[2]: [0, 2, 4, 6, 8]

3.3 标准库中的生成器函数

Python标准库提供了很多生成器,有些是内置的,比如filterenumeratemapzipreversed等,有些在itertoolsfunctools模块中。实现生成器时要先知道标准库中有什么可用,否则很可能会重新发明轮子

(1) 过滤

用于过滤的生成器函数: 从输入的可迭代对象中产出元素的子集,而且不修改元素本身。它们大多数都接受一个断言参数(predicate),该参数是一个布尔函数,此布尔函数有一个参数,会应用到输入中的每个元素上,用于判断元素是否包含在输出中:

模块 函数 说明
(内置) filter(function or None, iterable) Return an iterator yielding those items of iterable for which function(item) is true. If function is None, return the items that are true.
itertools filterfalse(function or None, sequence) Return those items of sequence for which function(item) is false. If function is None, return the items that are false.
itertools takewhile(predicate, iterable) Return successive entries from an iterable as long as the predicate evaluates to true for each entry.
itertools dropwhile(predicate, iterable) Drop items from the iterable while predicate(item) is true. Afterwards, return every element until the iterable is exhausted.
itertools islice(iterable, stop)
islice(iterable, start, stop[, step])
Return an iterator whose next() method returns selected values from an iterable. Start defaults to zero. Step defaults to one.

(2) 映射

用于映射的生成器函数: 在输入的单个可迭代对象(map 和 starmap 函数处理 多个可迭代的对象)中的各个元素上做计算,然后返回结果。这里所说的 "映射" 与字典没有关系,而与内置的 map 函数有关。下面表格中的生成器函数会从输入的可迭代对象中的各个元素中产出一个元素,如果输入来自多个可迭代的对象, 第一个可迭代的对象到头后就停止输出:

模块 函数 说明
(内置) enumerate(iterable[, start]) Start defaults to zero. (0, seq[0]), (1, seq[1]), (2, seq[2]), ...
(内置) map(func, *iterables) Make an iterator that computes the function using arguments from each of the iterables. Stops when the shortest iterable is exhausted.

(3) 合并

用于合并的生成器函数: 从输入的多个可迭代对象中产出元素。zip并行处理输入的各个可迭代对象,而chain按顺序(一个接一个)处理输入的可迭代对象

模块 函数 说明
(内置) zip(iter1 [,iter2 [...]]) Return a zip object whose .__next__() method returns a tuple where the i-th element comes from the i-th iterable argument.
itertools chain(*iterables) 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.

(4) 扩展

用于把输入的各个元素扩展成多个输出元素的生成器函数: 有些生成器函数会从一个元素中产出多个值,扩展输入的可迭代对象

模块 函数 说明
itertools count(start=0, step=1) Return a count object whose .__next__() method returns consecutive values.
itertools cycle(iterable) Return elements from the iterable until it is exhausted. Then repeat the sequence indefinitely.
itertools combinations(iterable, r) 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)

(5) 排序

用于产出输入的可迭代对象中的全部元素,不过会以某种方式重新排列的生成器函数: 内置的reversed函数

模块 生成器函数 说明
(内置) reversed(sequence) Return a reverse iterator over values of the sequence
itertools groupby(iterable, key=None) Make an iterator that returns consecutive keys and groups from the iterable. If the key function is not specified or is None, the element itself is used for grouping.

3.4 嵌套的生成器

可以将多个生成器管道(pipeline)一样链接起来使用,更高效的处理数据:

generators as pipelines

In [1]: def integers():  # 1. 产出整数的生成器
   ...:     for i in range(1, 9):
   ...:         yield i
   ...:         

In [2]: chain = integers()

In [3]: list(chain)
Out[3]: [1, 2, 3, 4, 5, 6, 7, 8]

In [4]: def squared(seq):  # 2. 基于整数的生成器,产出平方数的生成器
   ...:     for i in seq:
   ...:         yield i * i
   ...:         

In [5]: chain = squared(integers())

In [6]: list(chain)
Out[6]: [1, 4, 9, 16, 25, 36, 49, 64]

In [7]: def negated(seq):  # 3. 基于平方数的生成器,产出负的平方数的生成器
   ...:     for i in seq:
   ...:         yield -i
   ...:         

In [8]: chain = negated(squared(integers()))  # 链式生成器,更高效

In [9]: list(chain)
Out[9]: [-1, -4, -9, -16, -25, -36, -49, -64]

由于上面各生成器函数的功能都非常简单,所以可以使用生成器表达式进一步优化链式生成器

In [1]: integers = range(1, 9)

In [2]: squared = (i * i for i in integers)

In [3]: negated = (-i for i in squared)

In [4]: negated
Out[4]: <generator object <genexpr> at 0x7f2a5c09be08>

In [5]: list(negated)
Out[5]: [-1, -4, -9, -16, -25, -36, -49, -64]

3.5 PEP 342: 增强生成器

2005年,Python 2.5 通过了 PEP 342 -- Coroutines via Enhanced Generators ,这个提案为生成器对象添加了额外的方法和功能,其中最值得关注的是.send()方法

.__next__()方法一样,.send()方法使生成器前进到下一个yield语句。不过,.send()方法还允许调用方把数据发送给生成器,即不管传给.send()方法什么参数,那个参数都会成为生成器函数定义体中对应的yield表达式的值。也就是说,.send()方法允许在调用方生成器之间双向交换数据,而.__next__()方法只允许调用方生成器中获取数据

查看生成器对象的状态:

可以使用 inspect.getgeneratorstate(...) 函数查看生成器对象的当前状态:

  • 'GEN_CREATED': 等待开始执行
  • 'GEN_RUNNING': 正在被解释器执行。只有在多线程应用中才能看到这个状态
  • 'GEN_SUSPENDED': 在yield表达式处暂停
  • 'GEN_CLOSED': 执行结束
In [1]: def echo(value=None):
   ...:     print("Execution starts when 'next()' is called for the first time.")
   ...:     try:
   ...:         while True:
   ...:             try:
   ...:                 value = (yield value)  # 调用send(x)方法后,等号左边的value将被赋值为x
   ...:             except Exception as e:
   ...:                 value = e
   ...:     finally:
   ...:         print("Don't forget to clean up when 'close()' is called.")
   ...:         

In [2]: g = echo(1)  # 返回生成器对象,此时value=1

In [3]: import inspect

In [4]: inspect.getgeneratorstate(g)
Out[4]: 'GEN_CREATED'

In [5]: print(next(g))  # 第一次要调用next()方法,让生成器前进到第一个yield处,后续才能在调用send()方法时,在该yield表达式位置接收客户发送的数据
Execution starts when 'next()' is called for the first time.
Out[5]: 1  # (yield value),产出value的值,因为此时value=1,所以打印1

In [6]: inspect.getgeneratorstate(g)
Out[6]: 'GEN_SUSPENDED'

In [7]: print(next(g))  # 第二次调用next()方法,相当于调用send(None),所以value = (yield value)中等号左边的value将被赋值为None。下一次While循环,又前进到(yield value)处,产出value的值,因为此时value=None,所以打印None
None

In [8]: inspect.getgeneratorstate(g)
Out[8]: 'GEN_SUSPENDED'

In [9]: print(g.send(2))  # 直接调用send(2)方法,所以value = (yield value)中等号左边的value将被赋值为2。下一次While循环,又前进到(yield value)处,产出value的值,因为此时value=2,所以打印2
2

In [10]: g.throw(TypeError, "spam")  # 调用throw()方法,将异常对象发送给生成器,所以except语句会捕获异常,即value=TypeError('spam')。下一次While循环,又前进到(yield value)处,产出value的值,因为此时value=TypeError('spam'),所以打印TypeError('spam')
Out[10]: TypeError('spam')

In [11]: g.close()  # 调用close()方法,关闭生成器
Don't forget to clean up when 'close()' is called.

In [12]: inspect.getgeneratorstate(g)
Out[12]: 'GEN_CLOSED'

这是一项重要的 "改进",甚至改变了生成器的本性:像这样使用的话,生成器就变身为基于生成器的协程,详情见 Python3爬虫系列07 (理论) - 协程

注意: 给已结束的生成器发送任何值,都将抛出StopIteration异常,且返回值(保存在异常对象的value属性上)是None

In [13]: g.send(3)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-35-494d69d54622> in <module>()
----> 1 g.send(3)

StopIteration:

3.6 PEP 380: 委托职责给子生成器

2009年,Python 3.3 通过了 PEP 380 -- Syntax for Delegating to a Subgenerator,引入了新的语法结构yield from <expr>,允许生成器将其部分操作委托给另一个生成器。就像一个庞大的函数可以拆分成多个功能独立的子函数一样,该提案允许将包含yield的一段代码分解出来并放在另一个生成器

(1) yield from <iterable>/<iterator>

yield from <expr>中的<expr>可以是一个简单的iterable对象,那么它实际上是for item in iterable: yield item的简写形式,因为用for循环去迭代iterable时,会先调用iter(iterable)从中获取迭代器,所以<expr>也可以是一个iterator对象

def gen():
    for i in 'ABC':
        yield i
    for j in range(1, 4):
        yield j

print(list(gen()))

# Output:
['A', 'B', 'C', 1, 2, 3]

如果换成yield from结构,则可以简写为:

def gen():
    yield from 'ABC'
    yield from range(1, 4)

print(list(gen()))

# Output:
['A', 'B', 'C', 1, 2, 3]

(2) yield from <subgenerator>

如果yield from <expr>中的<expr>是一个generator的话,那么允许该生成器中包含return value表达式,而且该返回值会成为yield from表达式的值

在Python 3.3 之前,如果生成器函数里面有return value表达式(即return后面有参数),解释器会抛出SyntaxError语法错误

PEP 380中的专门术语:

  • 委派生成器(delegating generator): 包含yield from <subgenerator>表达式的生成器函数
  • 子生成器(subgenerator)yield from <subgenerator>表达式中的<subgenerator>子生成器终止时的返回值如果有return语句,则返回return后面的表达式的值;否则,返回None)会成为yield from表达式的值
  • 调用方(caller): 使用调用方这个术语来指代调用委派生成器的客户端代码。也可以使用客户端代替调用方,以此与委派生成器(也是调用方,因为它调用了子生成器)区分开。注意: 调用方只使用委派生成器.send().throw().close()等方法

委派生成器result = yield from <subgenerator>表达式处暂停时:

  1. 调用方可以直接把数据发给子生成器(调用send()方法),子生成器再把产出的值发给调用方(通过yield关键字)
  2. 子生成器终止时,会抛出StopIteration异常给委派生成器,并且子生成器返回值如果有return语句,则返回return后面的表达式的值;否则,返回None)就附加在该异常对象的value属性上。此时委派生成器恢复执行yield from语法结构会自动捕获并处理该StopIteration异常,所以子生成器返回值会成为yield from <subgenerator>表达式的值,即result等于子生成器返回值
  3. 调用方也可以直接把异常发给子生成器(调用throw()方法)
  4. 调用方也可以直接关闭子生成器(调用close()方法)

所以,yield from的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样调用方可以直接发送值给子生成器,而子生成器直接产出值给调用方,同时调用方还可以直接传入异常子生成器,而不用在位于中间的委派生成器中添加大量接收调用方发送的值并传递给子生成器处理异常的样板代码(详情见下面的示例)

没有yield from语法结构之前,委派生成器的代码很复杂,既要接收调用方发送的值并传递给子生成器,也要捕获调用方发送的异常并继续抛出给子生成器

class SpamException(Exception):
    pass

def accumulate():  # 子生成器
    tally = 0
    while True:
        try:
            next = yield  # 产出None给委派生成器; 接收调用方发送来的值
        except SpamException as e:  # 测试: 如果调用方throw(SpamException)
            print('Subgenerator catches the SpamException, is the caller OK?')
        else:
            if next is None:
                return tally  # 返回累加的结果
            if isinstance(next, int):  # 接收到的值是int类型时,才累加; 否则会报TypeError异常,并向上冒泡给调用方
                tally += next  # 累加接收到的值

def gather_tallies(tallies):  # 委派生成器
    while True:
        # 每一次累加都是一次循环,因为累加结束时,给子生成器发送None,子生成器会返回累加的结果,此时子生成器已经结束了
        # 如果不重新创建子生成器,继续给已结束的子生成器send()值,都会抛出StopIteration异常,且value属性值为None
        try:
            subgen = accumulate()
            subgen.send(None)  # 启动子生成器,让它可以开始接收值
            while True:
                try:
                    x = yield  # 产出None给调用方; 委派生成器先代为接收调用方发送的值
                except SpamException as e:  # 委派生成器还得捕获调用方发送的任何异常并继续抛出给子生成器
                    subgen.throw(e)
                else:
                    res = subgen.send(x)  # 委派生成器转发调用方发送的值给子生成器
                # print('Recevied ({}) from subgenerator'.format(res))
        except StopIteration as e:  # 如果调用方发送None,子生成器会结束并抛出StopIteration异常,累加的结果为value属性值
            tally = e.value
            tallies.append(tally)  # 将累加的结果添加到列表中

def main():
    tallies = []  # 创建列表,用来保存每一次累加的结果
    acc = gather_tallies(tallies)  # 创建委派生成器
    next(acc)  # 启动委派生成器,它里面会创建一个子生成器用来累加,并会自动启动子生成器

    for i in [0, 'ABC', 1, 'spam', 2, 3]:  # 第一次累加,通过for循环依次发送累加的数值给委派生成器
        if i == 'spam':
            acc.throw(SpamException)  # 测试: 调用方抛出异常
        else:
            res = acc.send(i)
        # print('Recevied ({}) from delegating generator'.format(res))
    acc.send(None)  # 结束第一次累加,发送None给委派生成器,委派生成器转发给子生成器后,会结束子生成器,返回累加的结果并添加到列表中

    for i in range(5):  # 第二次累加
        res = acc.send(i)
        # print('Recevied ({}) from delegating generator'.format(res))
    acc.send(None)  # 结束第二次累加

    print(tallies)

if __name__ == '__main__':
    main()

# Output
Subgenerator catches the SpamException, is the caller OK?
[6, 10]

使用yield from后,就可以通过以前不可能的方式委托职责:

yield from

此时,委派生成器可以简写为:

def gather_tallies(tallies):  # 委派生成器
    while True:
        tally = yield from accumulate()
        tallies.append(tally)

yield from的真正作用:

  1. 子生成器产出的值都直接传给委派生成器调用方(即客户端代码)
  2. 调用方通过调用委派生成器send()方法所发送的值,都直接传给子生成器。如果发送的值是None,那么会调用子生成器__next__() 方法。如果发送的值不是 None,那么会调用子生成器send() 方法。如果调用子生成器对应的方法后抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器
  3. yield from表达式的值等于子生成器终止时的返回值,如果子生成器中没有return语句,将返回None
  4. 传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器throw() 方法。如果调用子生成器throw() 方法时抛出 StopIteration 异常,委派生成器恢复运行。 StopIteration 之外的异常会向上冒泡,传给委派生成器
  5. 如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么在子生成器上也调用 close() 方法,如果它有的话。如果调用子生成器close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常

3.7 PEP 525 异步生成器

在函数体中使用yield表达式会导致该函数成为生成器,并且在async def函数体(原生协程,参考 Python3爬虫系列07 (理论) - 协程 )中使用yield会导致该协程函数成为异步生成器

def gen():  # defines a generator function
    yield 123

async def agen(): # defines an asynchronous generator function (PEP 525)
    yield 123

代码已上传到 https://github.com/wangy8961/python3-concurrency ,欢迎star

未经允许不得转载: LIFE & SHARE - 王颜公子 » Python3爬虫系列06 (理论) - 可迭代对象、迭代器、生成器

分享

作者

作者头像

Madman

如果博文内容有误或其它任何问题,欢迎留言评论,我会尽快回复; 或者通过QQ、微信等联系我

0 条评论

暂时还没有评论.

发表评论前请先登录

专题