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]
 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 
    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 
    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 
    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这个关键字的理解不是很透彻,这里主要是记录一下最近的学习成果。大家如有异议,欢迎指正。

你可能感兴趣的:(Python3学习笔记:清晰理解迭代器、生成器以及yield表达式)