Python高级第七天

一、可迭代对象

可迭代的对象(Iterable)是指使用iter()内置函数可以获取迭代器(Iterator)的对象

1. 判断是否是可迭代对象:

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

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

2. iter()函数有两种用法:

  • iter(iterable) -> iterator: 传入可迭代的对象,返回迭代器
  • iter(callable, sentinel) -> iterator: 传入两个参数,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符
注:
  1. Python迭代协议要求iter()必须返回特殊的迭代器对象。
  2. 如果类里有__iter__方法,且__iter__能返回一个迭代器,其对象就是可迭代对象
  3. 检查对象x是否为可迭代对象最准确的方法,就是调用iter(x)方法
  4. iter(callable,sentinel)两个参数(可调用对象,哨符) , 重复调用callable,产出各个值,直到返回的值为哨符就会停止迭代,抛出异常StopIteration,而不产出哨符

3. 实用的示例:

逐行读取文件,直到遇到空行或者到达文件末尾为止

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

二、迭代器

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

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

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

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

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. __next__()__iter__()

标准的迭代器接口:

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

说明:只要实现了能返回迭代器的__iter__()方法的对象就是可迭代的对象,所以,迭代器都是可迭代的对象!

示例:

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  # 返回迭代器本身
注:
  1. 可以用next()对迭代器进行迭代,如:hello,调用一次next()就可以取到h,在调用一次取到e
  2. next()只能对迭代器使用
  3. 如果实现了__iter__()__next__()就是一个迭代器
  4. with open('') as f: f也是一个迭代器,可以使用next(f)
  5. next(迭代器,默认值)可以两个参数,迭代完会返回默认值,不会抛出异常

3. 可迭代的对象与迭代器的对比

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

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

In [1]: s = 'ABC'

In [2]: for char in s:
        :     print(char)
       
A
B
C
总结:
  • 迭代器要实现__next__()方法,返回迭代器中的下一个元素
  • 迭代器还要实现__iter__()方法,返回迭代器本身,因此,迭代器可以迭代。迭代器都是可迭代的对象
  • 可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现__iter__()方法,但不能实现__next__()方法

三、生成器

在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

1. 生成器函数

只要 Python 函数的定义体中有 yield关键字,该函数就是生成器函数。调用生成器函数时,会返回一个生成器(generator)对象。也就是说,生成器函数是生成器工厂

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异常

注:
  1. 普通函数返回值,调用生成器函数返回生成器,生成器产出或生成值
  2. 调用生成器函数后,会构建一个实现了迭代器接口的生成器对象,即,生成器一定是迭代器!
  3. 迭代器和生成器都是为了惰性求值(lazy evaluation),避免浪费内存空间。
  4. yield(产出)关键字 只要定义的函数里有yield就是一个生成器函数,可以生成一个生成器对象
  5. 函数执行时,遇到yield会阻塞一下,返回yield的值,类似return,但是yield返回值后可以继续执行函数

2. 生成器表达式

生成器表达式,与列表推导式类似,不过是(),最后产出一个生成器

如果列表推导是制造列表的工厂,那么生成器表达式就是制造生成器的工厂

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. 嵌套的生成器

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

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]

4. 增强生成器

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

查看生成器对象的状态:
  • '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'

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

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

5. yield from

yield from 后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。
他可以把可迭代对象里的每个元素一个一个的yield出来,对比yield来说代码更加简洁,结构更加清晰。
yield from它可以帮我们处理异常

小结

  1. Python中内置的序列,如list、tuple、str、bytes、dict、set、collections.deque等都是可迭代对象,但它们不是迭代器。
  2. 迭代器可以被 next() 函数调用,并不断返回下一个值。
  3. Python从可迭代的对象中获取迭代器。
  4. 迭代器和生成器都是为了惰性求值(lazy evaluation),避免浪费内存空间,实现高效处理大量数据。
  5. 在Python 3中,生成器有广泛的用途,所有生成器都是迭代器,因为生成器完全实现了迭代器接口。
  6. 迭代器用于从集合中取出元素,而生成器用于"凭空"生成元素 。
  7. PEP 342 给生成器增加了 send() 方法,实现了"基于生成器的协程"。
  8. PEP 380允许生成器中可以return返回值,并新增了 yield from 语法结构,打开了调用方和子生成器的双向通道
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • 本节课纲 可迭代对象 迭代器 生成器Python中内置的序列,如list、tuple、str、bytes、dict...
    郭_扬阅读 1,152评论 0 0
  • 迭代、迭代器、生成器、协程、yield、greenlet、gevent、进程线程协程对比、gevent多任务图片下...
    Cestine阅读 466评论 0 0
  • 一、总体内容 1.1、协程的介绍 1.2、迭代器以及迭代器的应用 1.3、生成器(生成器与迭代器保存的都是生成数据...
    IIronMan阅读 848评论 0 1
  • 你不知道JS:异步 第四章:生成器(Generators) 在第二章,我们明确了采用回调表示异步流的两个关键缺点:...
    purple_force阅读 916评论 0 2
  • 暗恋就像一场无疾而终的事故,悄悄的来,伤痛的死去,真正看到你秀的恩爱,我就知道该放弃了,我的初恋
    舟儿萤萤阅读 241评论 0 1