python高级_day6

python可迭代对象、迭代器、生成器

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

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))
    
    
# from test import Sentence  # 导入刚创建的类
s = Sentence('I love Python')  # 传入字符串,创建一个Sentence实例

print(s)  # 自动调用__repr__()函数
print(s[0]) # 自动调用__getitem__()函数,和下面效果一样

print(s.__getitem__(0))

for word in s:  # Sentence实例可以迭代
     print(word)


print(list(s))  # 因为可以迭代,所以Sentence对象可以用于构建列表和其它可迭代的类型

from collections import abc

print(isinstance(s, abc.Iterable))  # 不能正确判断Sentence类的对象s是可迭代的对象

print(iter(s))  # 没有抛出异常,返回迭代器,说明Sentence类的对象s是可迭代的

Sentence('I love Python')
I
I
I
love
Python
['I', 'love', 'Python']
False
<iterator object at 0x0000000004D22128>

1.3 __iter__()

   如果实现了__iter__()方法,但该方法没有返回迭代器时,这时调用该方法python的解释器会抛出一个异常

class Foo:
    def __iter__(self):
        pass    

from collections import abc

f = Foo()

print(isinstance(f, abc.Iterable))  # 错误地判断Foo类的对象f是可迭代的对象

print(iter(f))  # 使用iter()方法会抛出异常,即对象f不可迭代,不能用for循环迭代它

True
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-ccfd08854f37> in <module>
9 print(isinstance(f, abc.Iterable)) # 错误地判断Foo类的对象f是可迭代的对象
10

---> 11 print(iter(f)) # 使用iter()方法会抛出异常,即对象f不可迭代,不能用for循环迭代它

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

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

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

from collections import abc

f = Foo()

print(isinstance(f, abc.Iterable))

print(iter(f))

for i in f:
     print(i)

True
<list_iterator object at 0x0000000004FD7470>
1
2
3

1.4 iter()函数的补充

iter()函数有两种用法:

  • iter(iterable) -> iterator: 传入可迭代的对象,返回迭代器
  • iter(callable, sentinel) -> iterator: 传入两个参数,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符
# 使用iter()的第二种方法来掷骰子,知道掷出1点为止
from random import randint

def d6():
     return randint(1, 6)

d6_iter = iter(d6, 1)  # 第一个参数是d6函数,第二个参数是哨符

print(d6_iter)  # 这里的 iter 函数返回一个 callable_iterator 对象

for roll in d6_iter:  # for 循环可能运行特别长的时间,不过肯定不会打印 1,因为 1 是哨符
    print(roll)

<callable_iterator object at 0x0000000004FFAB70>
4
2
4
5
6
5
4
4
4
4
2
5

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

lj

abc

111

xixixi

hhahahah

2 迭代器

   迭代是数据处理的基石。当扫描内存中放不下的数据集时,我们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式(Iterator pattern)
   迭代器是这样的对象:实现了无参数的__next__()方法,返回序列中的下一个元素,如果没有元素了,就抛出StopIteration异常。即,迭代器可以被next()函数调用,并不断返回下一个值。
在 python 语言内部,迭代器用于支持:

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

2.1 判断对象是否为迭代器

   检查对象x是否为迭代器最好的方式是调用 isinstance(x, abc.Iterator);注意区分可迭代对象和迭代器的区别,判断可迭代对象使用的是iter()函数

from collections import abc

print(isinstance([1,3,5], abc.Iterator))
print(isinstance((2,4,6), abc.Iterator))
print(isinstance({'name': 'wangy', 'age': 18}, abc.Iterator))
print(isinstance({1, 2, 3}, abc.Iterator))
print(isinstance('abc', abc.Iterator))
print(isinstance(100, abc.Iterator))

print(isinstance((x*2 for x in range(5)), abc.Iterator))  # 生成器表达式,后续会介绍

False
False
False
False
False
False
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__()方法的对象就是可迭代的对象,所以,迭代器都是可迭代的对象!
import re
import reprlib

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

# Sentence是可迭代对象
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__返回一个迭代器


# SentenceIterator是迭代器
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异常是用来通知我们迭代结束的

  • next()函数抛出异常
with open('mydata.txt') as fd:
    try:
        while True:
            line = next(fd)
            print(line, end='')
    except StopIteration:
        pass

lj
abc
111
xixixi
hhahahah

wuwuwuw

  • next()函数指定默认值
with open('mydata.txt') as fd:
    while True:
        line = next(fd, None)
        if line is None://用==也可以判断
            break
        print(line, end='')

laijie
abc
111
xixixi
hhahahah
wuwuwuw

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

   首先,我们要明确可迭代的对象和迭代器之间的关系:python从可迭代的对象中获取迭代器。
   比如,用for循环迭代一个字符串'ABC',字符串是可迭代的对象。for循环的背后会先调用iter(s)将字符串转换成迭代器,只不过我们看不到

s = 'ABC'

for char in s:
    print(char)

A
B
C

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

it = iter(s)  # 使用可迭代的对象s构建迭代器it

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

<str_iterator object at 0x0000000004E23048>
A
B
C
<str_iterator object at 0x0000000004E23048>

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

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  # 返回迭代器本身
    
# from test import Sentence

s = Sentence('Pig and Pepper')
it = iter(s)  # 获取迭代器
print(it)

print(next(it))  # 使用next()方法获取下一个单词
print(it.__next__())  # __next__()方法也能达到效果,但我们应该避免直接调用特殊方法
print(next(it))
try:
    print(next(it))  # 没有单词了,因此迭代器抛出StopIteration异常
except:
    print("所有单词都迭代完了")

print(list(it))  # 到头后,迭代器就没用了
print(list(iter(s)))  # 如果想再次迭代,要重新构建迭代器

<__main__.SentenceIterator object at 0x0000000004E3A8D0>
Pig
and
Pepper
所有单词都迭代完了
[]
['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

0
1
1
2
3
5
8
13
21
34

3.1 生成器函数

   只要 Python 函数的定义体中有 yield 关键字,该函数就是生成器函数。调用生成器函数时,会返回一个生成器(generator)对象。也就是说,生成器函数是生成器工厂
普通的函数与生成器函数在语法上唯一的区别是,在后者的定义体中有 yield 关键字

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

print(gen_AB)  # 生成器函数


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


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

<function gen_AB at 0x0000000004E58D08>
<generator object gen_AB at 0x0000000004DE1408>
start
A
continue
B
end


StopIteration Traceback (most recent call last)
<ipython-input-50-cf1e26265094> in <module>
15 print(next(g)) # 生成器都是迭代器,执行next(g)时生成器函数会向前,前进到函数定义体中的下一个 yield 语句,生成 yield 关键字后面的表达式的值,在函数定义体的当前位置暂停,并返回生成的值
16 print(next(g))
---> 17 print(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异常
  • 注意用词: 普通函数返回值,调用生成器函数返回生成器,生成器产出或生成值
  • 调用生成器函数后,会构建一个实现了迭代器接口的生成器对象,即,生成器一定是迭代器!
def gen_AB():  # 定义生成器函数的方式与普通的函数无异,只不过要使用 yield 关键字
    print('start')
    yield 'A'
    print('continue')
    yield 'B'
    print('end')

for c in gen_AB():
    print('-->', c)

start
--> A
continue
--> B
end

所以,可以使用生成器函数改写前面章节中的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关键字,将列表推导中的[]替换为()即可),让代码变得更简短
   生成器表达式可以理解为列表推导的惰性版本:不会迫切地构建列表,而是返回一个生成器,按需惰性生成元素。也就是说,如果列表推导是制造列表的工厂,那么生成器表达式就是制造生成器的工厂:

def gen_AB():
    print('start')
    yield 'A'
    print('continue')
    yield 'B'
    print('end')

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

for i in res1:  # 这个 for 循环迭代列表推导生成的 res1 列表
    print('-->', i)

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

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

start
continue
end
['AAA', 'BBB']
--> AAA
--> BBB
<generator object <genexpr> at 0x0000000004DE1570>
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))
  • 何时使用生成器表达式?
    • 生成器表达式是创建生成器的简洁语法,这样无需先定义函数再调用。不过,生成器函数灵活得多,可以使用多个语句实现复杂的逻辑,也可以作为协程使用(后续博文介绍)
    • 遇到简单的情况时,可以使用生成器表达式,因为这样扫一眼就知道代码的作用。如果生成器表达式要分成多行写,我倾向于定义生成器函数,以便提高可读性。此外,生成器函数有名称,因此可以重用

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

print(list((x*2 for x in range(5))))

print(list(x*2 for x in range(5)))  # 可以省略生成器表达式外面的()

[0, 2, 4, 6, 8]
[0, 2, 4, 6, 8]

3.3 嵌套生成器

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

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

chain1 = integers()

print(list(chain1))


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

chain2 = squared(integers())

print(list(chain2))

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

chain3 = negated(squared(integers()))  # 链式生成器,更高效

print(list(chain3))

[1, 2, 3, 4, 5, 6, 7, 8]
[1, 4, 9, 16, 25, 36, 49, 64]
[-1, -4, -9, -16, -25, -36, -49, -64]

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

integers = range(1, 9)
squared = (i * i for i in integers)
negated = (-i for i in squared)

print(negated)

print(list(negated))

<generator object <genexpr> at 0x0000000004DE1660>
[-1, -4, -9, -16, -25, -36, -49, -64]

3.4 增强生成器

   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: 执行结束
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.")
        
g=echo(1)  # 返回生成器对象,此时value=1

import inspect

print(inspect.getgeneratorstate(g))

print(next(g))  # 第一次要调用next()方法,让生成器前进到第一个yield处,后续才能在调用send()方法时,在该yield表达式位置接收客户发送的数据

print(inspect.getgeneratorstate(g))

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

print(inspect.getgeneratorstate(g))

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

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

g.close()  # 调用close()方法,关闭生成器

print(inspect.getgeneratorstate(g))

GEN_CREATED
Execution starts when 'next()' is called for the first time.
1
GEN_SUSPENDED
None
GEN_SUSPENDED
2
spam
Don't forget to clean up when 'close()' is called.
GEN_CLOSED

这是一项重要的 "改进",甚至改变了生成器的本性:像这样使用的话,生成器就变身为基于生成器的协程。

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

3.5 yield from

  • yield from 是在python3.3才出现的语法。所以这个特性在Python2中是没有的。
  • yield from 后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。

3.5.1 简单应用:拼接可迭代对象

使用yield和使用yield from对比:

  • 使用yield
# 字符串
astr='ABC'
# 列表
alist=[1,2,3]
# 字典
adict={"name":"wangbm","age":18}
# 生成器
agen=(i for i in range(4,8))

def gen(*args):
    for item in args:
        for i in item:
            yield i

new_list=gen(astr, alist, adict,agen)
print(list(new_list))

['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]

  • 使用yield from
# 字符串
astr='ABC'
# 列表
alist=[1,2,3]
# 字典
adict={"name":"wangbm","age":18}
# 生成器
agen=(i for i in range(4,8))

def gen(*args):
    for item in args:
        yield from item

new_list=gen(astr, alist, adict, agen)
print(list(new_list))

['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]

由上面两种方式对比,可以看出,yield from后面加上可迭代对象,他可以把可迭代对象里的每个元素一个一个的yield出来,对比yield来说代码更加简洁,结构更加清晰。

3.5.2 复杂应用:生成器的嵌套

   当 yield from 后面加上一个生成器后,就实现了生成的嵌套。
   当然实现生成器的嵌套,并不是一定必须要使用yield from,而是使用yield from可以让我们避免自己处理各种料想不到的异常,而让我们专注于业务代码的实现。

讲解之前,首先要知道几个概念:

  1. 调用方:调用委派生成器的客户端(调用方)代码
  2. 委托生成器:包含yield from表达式的生成器函数
  3. 子生成器:yield from后面加的生成器函数

你可能不知道他们都是什么意思,没关系,来看下这个例子。

#这个例子,是实现实时计算平均值的。 比如,第一次传入10,那返回平均数自然是10. 第二次传入20,那返回平均数是(10+20)/2=15 第三次传入30,那返回平均数(10+20+30)/3=20
# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        count += 1
        total += new_num
        average = total/count

# 委托生成器
def proxy_gen():
    while True:
        yield from average_gen()

# 调用方
def main():
    calc_average = proxy_gen()
    next(calc_average)            # 预激下生成器
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0

if __name__ == '__main__':
    main()

0
10.0
15.0
20.0

  • 委托生成器的作用是:在调用方与子生成器之间建立一个双向通道。
    所谓的双向通道是什么意思呢? 调用方可以通过send()直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方。

  • 你可能会经常看到有些代码,还可以在yield from前面看到可以赋值。这是什么用法?
    你可能会以为,子生成器yield回来的值,被委托生成器给拦截了。你可以亲自写个demo运行试验一下,并不是你想的那样。 因为我们之前说了,委托生成器,只起一个桥梁作用,它建立的是一个双向通道,它并没有权利也没有办法,对子生成器yield回来的内容做拦截。
    为了解释这个用法,还是用上述的例子,并对其进行了一些改造。

# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        if new_num is None:
            break
        count += 1
        total += new_num
        average = total/count

    # 每一次return,都意味着当前协程结束。
    return total,count,average

# 委托生成器
def proxy_gen():
    while True:
        # 只有子生成器要结束(return)了,yield from左边的变量才会被赋值,后面的代码才会执行。
        total, count, average = yield from average_gen()
        print("计算完毕!!\n总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))

# 调用方
def main():
    calc_average = proxy_gen()
    print(next(calc_average))            # 预激协程
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0
    calc_average.send(None)      # 结束协程
    # 如果此处再调用calc_average.send(10),由于上一协程已经结束,将重开一协程

if __name__ == '__main__':
    main()

0
10.0
15.0
20.0
计算完毕!!
总共传入 3 个数值, 总和:60,平均数:20.0

  • 为什么要使用yield from
    既然委托生成器,起到的只是一个双向通道的作用,还需要委托生成器做什么?调用方直接调用子生成器不就好啦?

下面我们来一起探讨一下,到底yield from 有什么过人之处,让我们非要用它不可。

  • 因为它可以帮我们处理异常
    如果我们去掉委托生成器,而直接调用子生成器。那我们就需要把代码改成像下面这样,我们需要自己捕获异常并处理。而不像使yield from那样省心。
# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        if new_num is None:
            break
        count += 1
        total += new_num
        average = total/count
    return total,count,average

# 调用方
def main():
    calc_average = average_gen()
    print(next(calc_average))            # 预激协程
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0

    # ----------------注意-----------------
    try:
        calc_average.send(None)
    except StopIteration as e:
        total, count, average = e.value
        print("计算完毕!!\n总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))
    # ----------------注意-----------------

if __name__ == '__main__':
    main()

0
10.0
15.0
20.0

计算完毕!!
总共传入 3 个数值, 总和:60,平均数:20.0

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 本节课纲 可迭代对象 迭代器 生成器Python中内置的序列,如list、tuple、str、bytes、dict...
    郭_扬阅读 1,214评论 0 0
  • 一、可迭代对象 可迭代的对象(Iterable)是指使用iter()内置函数可以获取迭代器(Iterator)的对...
    code与有荣焉阅读 313评论 0 2
  • 迭代、迭代器、生成器、协程、yield、greenlet、gevent、进程线程协程对比、gevent多任务图片下...
    Cestine阅读 484评论 0 0
  • Python可迭代对象/迭代器/生成器 概述 迭代是数据处理的基石. 扫描内存中放不下的数据集时, 需要找到一种惰...
    老苏GO阅读 659评论 0 0
  • 一、总体内容 1.1、协程的介绍 1.2、迭代器以及迭代器的应用 1.3、生成器(生成器与迭代器保存的都是生成数据...
    IIronMan阅读 861评论 0 1