Python3学习笔记:清晰理解迭代器、生成器以及yield表达式

前言

迭代器、生成器与装饰器是python中非常重要的三个特性。对于迭代器,很多初学者理解起来不是很困难,但是学习生成器与装饰器时可能就会感觉云里雾里。本篇文章会用简洁清晰的方式讲解迭代器与生成器,同时记录一下最近的学习成果。

迭代器(Iterator)

在介绍迭代器之前,我们需要先简单了解一个概念:可迭代对象(iterable)。可以直接作用于for循环的对象,称之为可迭代对象,例如list、tuple、dict等类型对象,都是可迭代对象,它们有个共同特征,内部一定是实现了__iter__方法。

但是我们今天要了解的迭代器与可迭代对象并不是同一个东西。迭代器是指可以被next()函数调用并不断返回下一个值的对象。它与可迭代对象相比,同时实现了两个方法:__iter__(此方法用于返回迭代器自身)与__next__(此方法用于返回下一个值,如果没有下一个值了,则抛出StopIteration异常)。他们之间的关系为:

  • 可迭代对象不一定是迭代器
  • 迭代器一定是可迭代对象

为了方便理解,我们来看一个简单的自定义迭代器示例:

class Test:
    def __init__(self):
        self.curr = 1

    def __iter__(self):
        return self

    def __next__(self):
        value = self.curr
        self.curr += 1
        return value

t = Test()
print(next(t))
print(next(t)
print(next(t))

打印结果:

1
2
3

上述示例中,Test对象实现了__iter__方法,所以它是一个可迭代对象;因为同时又实现了__next__方法,所以它还是个迭代器。当每次调用next()方法时,它一共做了两件事:

  • 为下次返回的值做准备
  • 返回本次的值

迭代器是惰性返回的,只有当有人调用时才会生成值返回,否则就一直处于休眠状态,等待下次调用。

生成器(Generator)

生成器是特殊的迭代器,但是不用像迭代器那样自定义实现__iter____next__方法。其一共有两种实现方式,一种是直接将列表生成式的[]改为(),如下:

# 列表生成式
l = [x * x for x in range(10)]
# 生成器
g = (x * x for x in range(10))

print()
print(g)

打印结果:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
<generator object <genexpr> at 0x000001A504CF03B8>

此种实现是比较容易理解的,另一种是我们本文将要着重讲解的:在普通函数中使用yield关键字。下面我们来看一个简单的生成器示例:

def test():
    while True:
        rs = yield 1
        print("result: ", rs)

t = test()

test()函数与我们平时看到的普通函数,基本没有差别,只是在函数中使用到了yield关键字,此时我们称之为test()函数的对象 t 为生成器。现在我们已经大概知道了生成器是长什么样子了,那么下一步我们就要去理解这个yield表达式是干什么用的,毕竟它是一切的起因。

yield

还是上面的示例,我们先执行一遍

def test():
    print("开始执行")
    while True:
        rs = yield 1
        print("result: ", rs)


t = test()

打印结果:

Process finished with exit code 0

执行后,发现啥也没发生,print("开始执行")这句根本未执行。
所以,t = test()这句的作用只是返回一个生成器给 t,并不会执行test()函数。

然后,我们在t = test()下面加上如下代码,继续执行。

print(next(t))
print("=================")
print(next(t))

打印结果:

开始执行
1
=================
result:  None
1

其上执行流程如下:

  • 第一个next(t)表示启动生成器,开始执行test()函数
  • 执行test()函数中的print("开始执行")开始执行被打印出来
  • 进入while循环,执行语句rs = yield 1。此处我们可以暂时简单粗暴的把yield理解为return,所以此处可理解为 return 1。至此,返回结果1,并终止了执行,1被打印出来
  • 因为test()函数已经终止执行,所以往下执行,=================被打印出来
  • 最后一句,再次执行了next(t)方法。此时开始凸显yield的另一个特性,之前因为yield中断了循环,返回值,此时再次执行next(t)会接着上次中断的地方开始执行,即执行print("result: ", rs),而不是从函数头print("开始执行")开始执行。另外,yield在返回值后已经终止了函数执行,变量rs并没有被赋值,所以rs就为None。打印出来的的结果自然就是result: None。而后又开始新的while循环,重新执行到yield表达式语句,仍热是返回1,然后终止函数执行。打印出1

从以上流程可以看出,yield的两个特性:

  • 执行到yield所在的语句,会直接相当于return一样,返回一个值,停止函数执行
  • 再次使用next()方法调用生成器时,会从前面终止的地方继续往下执行,而不是从头开始

此外,yield还可以接收从生成器传过来的值,这需要用到生成器的 send() 方法来发送值,我们继续往下看。

send()方法

那如果想要变量rs被赋值,又该如何做呢?我们可以对上述代码做些稍稍改动,将最后一个next(t)改为t.send(2)如下:

print(next(t))
print("=================")
print(t.send(2))

打印结果:

开始执行
1
=================
result:  2
1

从打印结果中可以很容易的看出,result 不在是 None,而是2。毫无疑问,都是t.send(2)方法的原因,下面我们介绍一下send() 方法。

send() 方法是生成器的一个内部方法,调用此方法后,它会先发送一个参数,示例中是发送了一个 2。这个参数会被yield接收,如果此时存在指向yield表达式的变量,例如示例中的 rs ,则yield会将接收到的参数赋给此变量,在示例中就是将接收到的 2 赋给了 rs。然后从上次中断的地方继续往下执行,这部分的功能与next()的功能一致。

使用send()是需要注意,如果生成器还没有启动,不能调用send(有效参数)方法。因为如果生成器未启动,则代表还没有执行到第一处yield语句那边,调用了send(有效参数)方法就没有人去接收它。所以在调用send(有效参数)之前,必须先使用next(生成器)或者send(None)来启动生成器。(这里提一下,使用send(None)也可以启动生成器,等价于next(生成器),前面所说的有效参数指非None参数)

close()与throw()

生成器除了send()方法外,还有两个常用的方法close()与throw()方法。

  • close():用于关闭生成器,在生成器执行了close()后,还继续迭代的话,会报错,提示StopIteration,示例如下:
print(next(t))
t.close()
print(next(t))

执行结果:
Traceback (most recent call last):
  File "E:/PythonProjects/py3-webapp-blog/demo.py", line 17, in <module>
    print(next(t))
StopIteration
  • throw():用于主动抛出指定异常,生成器在执行了throw()后,会抛出指定的异常并停止迭代,示例如下:
def test():
    print("开始执行")
    try:
        while True:
           rs = yield 1
           print("result: ", rs)
    except ValueError:
        print("无效参数")


t = test()
print(next(t))
print(t.throw(ValueError))


执行结果:

开始执行
Traceback (most recent call last):
1
无效参数
  File "E:/PythonProjects/py3-webapp-blog/demo.py", line 14, in <module>
    print(t.throw(ValueError))
StopIteration

当生成器 t 执行了throw(ValueError)后,会抛出ValueError异常。因在test()函数中使用了try-except来捕获ValueError异常进行了处理,所以不会在控制台中直接抛出ValueError,而是去执行了except ValueError这块代码。如果不做try-except捕获ValueError处理异常,则会直接在控制台中抛出ValueError,改动后的示例如下:

def test():
    print("开始执行")
    while True:
        rs = yield 1
        print("result: ", rs)


t = test()
print(next(t))
print(t.throw(ValueError))


执行结果:

Traceback (most recent call last):
开始执行
  File "E:/PythonProjects/py3-webapp-blog/demo.py", line 11, in <module>
    print(t.throw(ValueError))
1
  File "E:/PythonProjects/py3-webapp-blog/demo.py", line 5, in test
    rs = yield 1
ValueError  <=== 直接抛出了ValueError异常

为什么要使用生成器

通过上文,我们知道生成器是特殊的迭代器,和迭代器一样,是惰性的,不会一下子将所有的值都生成,只有调用一次,才会生成一次值并返回。

如此一来,当我们有1000条数据时,如果我们将其存放在列表中,则会占据1000条数据的空间,而如果我们在一次访问时,只访问了前100条,后面的900条数据就没有啥作用,还浪费了大量的存储空间。

而如果我们使用生成器来实现,因为其惰性返回的特性,不会一下子将1000条数据全部生成一次,而是我们迭代一次,生成器才返回一次数据。这样一来,即使我们只是访问前100条数据,也不会生成后面900条数据占据空间,造成浪费。另一方面,我们也可以看出生成器所保存的其实就是生产数据的算法以及上次生产的状态。

结束感言

本人也是刚入坑Python3不久,之前对生成器的理解特别是yield这个关键字的理解不是很透彻,这里主要是记录一下最近的学习成果。大家如有异议,欢迎指正。

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