赞
踩
目录
1.2.1 __iter__ 和 __next__(python3)
3.1.7 清单 7. 使用 isgeneratorfunction 判断是否是一个特殊的 generator 函数
python里面有很多的以__
开始和结尾的函数,利用它们可以完成很多复杂的逻辑代码,而且提高了代码的简洁性。
迭代是Python最强大的功能之一,是访问集合元素的一种方式。
迭代器是一个可以记住遍历的位置的对象。
迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
迭代器有两个基本的方法:iter() 和 next()。
字符串,列表或元组对象都可用于创建迭代器:
- >>> a=[1,2,3,4]
- >>> it = iter(a) # 创建迭代器对象
- >>> print (next(it)) # 输出迭代器的下一个元素
- 1
- >>> print (next(it))
- 2
迭代器对象可以使用常规for语句进行遍历:
- a=[1,2,3,4]
- it = iter(a) # 创建迭代器对象
- for x in it:
- print (x, end=" ")
-
- # 输出: 1 2 3 4
也可以使用 next() 函数:
- import sys # 引入 sys 模块
-
- a=[1,2,3,4]
- it = iter(a) # 创建迭代器对象
-
- while True:
- try:
- print (next(it))
- except StopIteration:
- sys.exit()
-
- # 输出
- # 1
- # 2
- # 3
- # 4
__iter__
和 __next__
(python3) 其实这里需要引入一个概念,叫迭代器,常见的就是我们在使用for
语句的时候,python内部其实是把for
后面的对象上使用了内建函数iter
,比如:
- a = [1, 2, 3]
- for i in a:
- do_something()
其实在python内部进行了类似如下的转换:
- a = [1, 2, 3]
- for i in iter(a):
- do_something()
那么iter
返回的是什么呢,就是一个迭代对象,它主要映射到了类里面的__iter__
函数,此函数返回的是一个实现了__next__
的对象。注意理解这句话,比如:
- class B(object):
- def __next__(self):
- raise StopIteration
-
- class A(object):
- def __iter__(self):
- return B()
我们可以看见,A
这个类实现了一个__iter__
函数,返回的是B()
的实例对象,其中B
里面实现了__next__
这个函数。
下面引入几个概念:
Iterable
: 有迭代能力的对象,一个类,实现了__iter__
,那么就认为它有迭代能力,通常此函数必须返回一个实现了__next__
的对象,如果自己实现了,你可以返回self
,当然这个返回值不是必须的;Iterator
: 迭代器(当然也是Iterable
),同时实现了__iter__
和__next__
的对象,缺少任何一个都不算是Iterator
,比如上面例子中,A()
可以是一个Iterable
,但是A()
和B()
都不能算是和Iterator
,因为A
只实现了__iter__
,而B
只实现了__next__
()。我们可以使用collections
里面的类型来进行验证:
- class B(object):
- def __next__(self):
- raise StopIteration
-
- class A(object):
- def __iter__(self):
- return B()
-
-
- from collections.abc import *
-
- a = A()
- b = B()
- print(isinstance(a, Iterable))
- print(isinstance(a, Iterator))
-
- print(isinstance(b, Iterable))
- print(isinstance(b, Iterator))
结果:
- True
- False
- False
- False
让我们稍微对B
这个类做一点修改:
- class B(object):
- def __next__(self):
- raise StopIteration
-
- def __iter__(self):
- return None
-
- class A(object):
- def __iter__(self):
- return B()
-
-
- from collections.abc import *
-
- a = A()
- b = B()
- print(isinstance(a, Iterable))
- print(isinstance(a, Iterator))
-
- print(isinstance(b, Iterable))
- print(isinstance(b, Iterator))
结果:
- True
- False
- True
- True
python3 中range生成的是一个可迭代对象,不是迭代器。
上面只是做了几个演示,这里具体说明一下:
当调用iter
函数的时候,生成了一个迭代对象,要求__iter__
必须返回一个实现了__next__
的对象,我们就可以通过next
函数访问这个对象的下一个元素了,并且在你不想继续有迭代的情况下抛出一个StopIteration
的异常(for
语句会捕获这个异常,并且自动结束for
),下面实现了一个自己的类似range
函数的功能。
- class MyRange(object):
- def __init__(self, end):
- self.start = 0
- self.end = end
-
- def __iter__(self):
- return self
-
- def __next__(self):
- if self.start < self.end:
- ret = self.start
- self.start += 1
- return ret
- else:
- raise StopIteration
-
- from collections.abc import *
-
- a = MyRange(5)
- print(isinstance(a, Iterable))
- print(isinstance(a, Iterator))
-
- for i in a:
- print(i)
结果:
- True
- True
- 0
- 1
- 2
- 3
- 4
接下来我们使用next
函数模拟一次:
- class MyRange(object):
- def __init__(self, end):
- self.start = 0
- self.end = end
-
- def __iter__(self):
- return self
-
- def __next__(self):
- if self.start < self.end:
- ret = self.start
- self.start += 1
- return ret
- else:
- raise StopIteration
-
- a = MyRange(5)
- print(next(a))
- print(next(a))
- print(next(a))
- print(next(a))
- print(next(a))
- print(next(a)) # 其实到这里已经完成了,我们在运行一次查看异常
结果:
可以看见一个很明显的好处是,每次产生的数据,是产生一个用一个,什么意思呢,比如我要遍历[0, 1, 2, 3.....]
一直到10亿,如果使用列表的方式,那么是会全部载入内存的,但是如果使用迭代器,可以看见,当用到了(也就是在调用了next
)才会产生对应的数字,这样就可以节约内存了,这是一种懒惰的加载方式。
因此:把一个类作为一个迭代器使用需要在类中实现两个方法 __iter__() 与 __next__() (Python 2 里是 next())。
如果你已经了解的面向对象编程,就知道类都有一个构造函数,Python 的构造函数为 __init__(), 它会在对象初始化的时候执行。
创建一个返回数字的迭代器,初始值为 1,逐步递增 1:
- class MyNumbers:
- def __iter__(self):
- self.a = 1
- return self
-
- def __next__(self):
- x = self.a
- self.a += 1
- return x
-
- myclass = MyNumbers()
- myiter = iter(myclass)
-
- print(next(myiter))
- print(next(myiter))
- print(next(myiter))
- print(next(myiter))
- print(next(myiter))
-
- # 输出
- # 1
- # 2
- # 3
- # 4
- # 5
StopIteration 异常用于标识迭代的完成,防止出现无限循环的情况,该异常是在循环对象穷尽所有元素时的报错。在 __next__() 方法中我们可以设置在完成指定循环次数后触发 StopIteration 异常来结束迭代。
在 20 次迭代后停止执行:
- class MyNumbers:
- def __iter__(self):
- self.a = 1
- return self
-
- def __next__(self):
- if self.a <= 20:
- x = self.a
- self.a += 1
- return x
- else:
- raise StopIteration
-
- myclass = MyNumbers()
- myiter = iter(myclass)
-
- for x in myiter:
- print(x)
-
- # 输出:
- # 1
- # 2
- # 3
- # 4
- # 5
- # 6
- # 7
- # 8
- # 9
- # 10
- # 11
- # 12
- # 13
- # 14
- # 15
- # 16
- # 17
- # 18
- # 19
- # 20
collection.abs
里面的Iterator
和Iterable
配合isinstance
函数来判断一个对象是否是可迭代的,是否是迭代器对象iter
实际是映射到了__iter__
函数__iter__
的对象就是可迭代对象(Iterable
),正常情况下,应该返回一个实现了__next__
的对象(虽然这个要求不强制),如果自己实现了__next__
,当然也可以返回自己__iter__
和__next__
的是迭代器(Iterator
),当然也是一个可迭代对象了,其中__next__
应该在迭代完成后,抛出一个StopIteration
异常for
语句会自动处理这个StopIteration
异常以便结束for
循环在 Python 中,使用了 yield 的函数被称为生成器(generator)。
跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。
在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。
调用一个生成器函数,返回的是一个迭代器对象。
以下实例使用 yield 实现斐波那契数列:
- import sys
-
- def fibonacci(n): # 生成器函数 - 斐波那契
- a, b, counter = 0, 1, 0
- while True:
- if (counter > n):
- return
- yield a
- a, b = b, a + b
- counter += 1
- f = fibonacci(10) # f 是一个迭代器,由生成器返回生成
-
- while True:
- try:
- print (next(f), end=" ")
- except StopIteration:
- sys.exit()
-
- # 输出
- # 0 1 1 2 3 5 8 13 21 34 55
- # An exception has occurred, use %tb to see the full traceback.
- # SystemExit
斐波那契(Fibonacci)数列是一个非常简单的递归数列,除第一个和第二个数外,任意一个数都可由前两个数相加得到。用计算机程序输出斐波那契數列的前 N 个数是一个非常简单的问题,许多初学者都可以轻易写出如下函数:
- def fab(max):
- n, a, b = 0, 0, 1
- while n < max:
- print b
- a, b = b, a + b
- n = n + 1
- fab(5)
-
- # 输出
- # 1
- # 1
- # 2
- # 3
- # 5
结果没有问题,但有经验的开发者会指出,直接在 fab 函数中用 print 打印数字会导致该函数可复用性较差,因为 fab 函数返回 None,其他函数无法获得该函数生成的数列。
要提高 fab 函数的可复用性,最好不要直接打印出数列,而是返回一个 List。以下是 fab 函数改写后的第二个版本:
- def fab(max):
- n, a, b = 0, 0, 1
- L = []
- while n < max:
- L.append(b)
- a, b = b, a + b
- n = n + 1
- return L
-
- for n in fab(5):
- print n
-
- # 输出
- # 1
- # 1
- # 2
- # 3
- # 5
改写后的 fab 函数通过返回 List 能满足复用性的要求,但是更有经验的开发者会指出,该函数在运行中占用的内存会随着参数 max 的增大而增大,如果要控制内存占用,最好不要用 List来保存中间结果,而是通过 iterable 对象来迭代。
在 Python2.x 中,代码:
for i in range(1000): pass
会导致生成一个 1000 个元素的 List,而代码:
for i in xrange(1000): pass
则不会生成一个 1000 个元素的 List,而是在每次迭代中返回下一个数值,内存空间占用很小。因为 xrange 不返回 List,而是返回一个 iterable 对象。
利用 iterable 我们可以把 fab 函数改写为一个支持 iterable 的 class,以下是第三个版本的 Fab:
- # python2
- class Fab(object):
-
- def __init__(self, max):
- self.max = max
- self.n, self.a, self.b = 0, 0, 1
-
- def __iter__(self):
- return self
-
- def next(self): # python2
- # def __next__(self): # python3
- if self.n < self.max:
- r = self.b
- self.a, self.b = self.b, self.a + self.b
- self.n = self.n + 1
- return r
- raise StopIteration()
-
- for n in Fab(5):
- print n
-
- # 输出
- # 1
- # 1
- # 2
- # 3
- # 5
-
然而,使用 class 改写的这个版本,代码远远没有第一版的 fab 函数来得简洁。如果我们想要保持第一版 fab 函数的简洁性,同时又要获得 iterable 的效果,yield 就派上用场了:
- def fab(max):
- n, a, b = 0, 0, 1
- while n < max:
- yield b # 使用 yield
- # print b
- a, b = b, a + b
- n = n + 1
-
- for n in fab(5):
- print n
-
- # 输出
- # 1
- # 1
- # 2
- # 3
- # 5
-
-
- print(list(Fab(5)) # list(Fab(5))的作用是将生成器中的结果呈现出来
- # 输出
- # [1, 2, 3, 4, 5]
简单地讲,yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,调用 fab(5) 不会执行 fab 函数,而是返回一个 iterable 对象!在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。
可以用list获取生成器中的所有结果。
也可以手动调用 fab(5) 的 next() 方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程:
- >>>f = fab(5)
- >>> f.next()
- 1
- >>> f.next()
- 1
- >>> f.next()
- 2
- >>> f.next()
- 3
- >>> f.next()
- 5
- >>> f.next()
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- StopIteration
当函数执行结束时,generator 自动抛出 StopIteration 异常,表示迭代完成。在 for 循环里,无需处理 StopIteration 异常,循环会正常结束。
我们可以得出以下结论:
一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。
yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。
如何判断一个函数是否是一个特殊的 generator 函数?可以利用 isgeneratorfunction 判断:
- >>>from inspect import isgeneratorfunction
- >>> isgeneratorfunction(fab)
- True
要注意区分 fab 和 fab(5),fab 是一个 generator function,而 fab(5) 是调用 fab 返回的一个 generator,好比类的定义和类的实例的区别:
- >>>import types
- >>> isinstance(fab, types.GeneratorType)
- False
- >>> isinstance(fab(5), types.GeneratorType)
- True
fab 是无法迭代的,而 fab(5) 是可迭代的:
- >>>from collections import Iterable
- >>> isinstance(fab, Iterable)
- False
- >>> isinstance(fab(5), Iterable)
- True
每次调用 fab 函数都会生成一个新的 generator 实例,各实例互不影响:
- >>>f1 = fab(3)
- >>> f2 = fab(5)
- >>> print 'f1:', f1.next()
- f1: 1
- >>> print 'f2:', f2.next()
- f2: 1
- >>> print 'f1:', f1.next()
- f1: 1
- >>> print 'f2:', f2.next()
- f2: 1
- >>> print 'f1:', f1.next()
- f1: 2
- >>> print 'f2:', f2.next()
- f2: 2
- >>> print 'f2:', f2.next()
- f2: 3
- >>> print 'f2:', f2.next()
- f2: 5
return 的作用:在一个 generator function 中,如果没有 return,则默认执行至函数完毕,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代。
另一个 yield 的例子来源于文件读取。如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取:
- def read_file(fpath):
- BLOCK_SIZE = 1024
- with open(fpath, 'rb') as f:
- while True:
- block = f.read(BLOCK_SIZE)
- if block:
- yield block
- else:
- return
在一个 generator function 中,如果没有 return,则默认执行至函数完毕,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代。
- def foo():
- print("starting...")
- while True:
- res = yield 4
- print("res:",res)
- g = foo()
- print(next(g))
- print("*"*20)
- print(next(g))
代码的输出这个:
- starting...
- 4
- ********************
- res: None
- 4
解释代码运行顺序,相当于代码单步调试:
- g = foo()
- # 1.程序开始执行以后,因为foo函数中有yield关键字,所以foo函数并不会真的执行,
- # 而是先得到一个生成器g(相当于一个对象)
-
- print(next(g))
- # 2.直到调用next方法,foo函数正式开始执行,先执行foo函数中的print方法,然后进入while循环。。
- # 3.程序遇到yield关键字,然后把yield想想成return,return了一个4之后,程序停止,
- # 并没有执行赋值给res操作,此时next(g)语句执行完成,所以输出的前两行
- # (第一个是while上面的print的结果,第二个是return出的结果)是执行print(next(g))的结果,
-
- print("*"*20)
- # 4.程序执行print("*"*20),输出20个*
-
- print(next(g))
- # 5.又开始执行下面的print(next(g)),这个时候和上面那个差不多,不过不同的是,
- # 这个时候是从刚才那个next程序停止的地方开始执行的,也就是要执行res的赋值操作,
- # 这时候要注意,这个时候赋值操作的右边是没有值的(因为刚才那个是return出去了,
- # 并没有给赋值操作的左边传参数),所以这个时候res赋值是None,
- # 所以接着下面的输出就是res:None,
- # 6.程序会继续在while里执行,又一次碰到yield,这个时候同样return 出4,然后程序停止,
- # print函数输出的4就是这次return出的4.
1.程序开始执行以后,因为foo函数中有yield关键字,所以foo函数并不会真的执行,而是先得到一个生成器g(相当于一个对象)
2.直到调用next方法,foo函数正式开始执行,先执行foo函数中的print方法,然后进入while循环
3.程序遇到yield关键字,然后把yield想想成return,return了一个4之后,程序停止,并没有执行赋值给res操作,此时next(g)语句执行完成,所以输出的前两行(第一个是while上面的print的结果,第二个是return出的结果)是执行print(next(g))的结果,
4.程序执行print("*"*20),输出20个*
5.又开始执行下面的print(next(g)),这个时候和上面那个差不多,不过不同的是,这个时候是从刚才那个next程序停止的地方开始执行的,也就是要执行res的赋值操作,这时候要注意,这个时候赋值操作的右边是没有值的(因为刚才那个是return出去了,并没有给赋值操作的左边传参数),所以这个时候res赋值是None,所以接着下面的输出就是res:None,
6.程序会继续在while里执行,又一次碰到yield,这个时候同样return 出4,然后程序停止,print函数输出的4就是这次return出的4.
带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从foo函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。
- def foo():
- print("starting...")
- while True:
- res = yield 4
- print("res:",res)
- g = foo()
- print(next(g))
- print("*"*20)
- print(g.send(7))
再看一个这个生成器的send函数的例子,这个例子就把上面那个例子的最后一行换掉了,输出结果:
- starting...
- 4
- ********************
- res: 7
- 4
上面那个res的值是None,这个变成了7,到底为什么?这是因为,send是发送一个参数给res的,因为上面讲到,return的时候,并没有把4赋值给res,下次执行的时候只好继续执行赋值操作,只好赋值为None了,而如果用send的话,开始执行的时候,先接着上一次(return 4之后)执行,先把7赋值给了res,然后执行next的作用,遇见下一回的yield,return出结果后结束。
5.程序执行g.send(7),程序会从yield关键字那一行继续向下运行,send会把7这个值赋值给res变量
6.由于send方法中包含next()方法,所以程序会继续向下运行执行print方法,然后再次进入while循环
7.程序执行再次遇到yield关键字,yield会返回后面的值后,程序再次暂停,直到再次调用next方法或send方法。
Python3 迭代器与生成器:https://www.runoob.com/python3/python3-iterator-generator.html
Python yield 使用浅析:https://www.runoob.com/w3cnote/python-yield-used-analysis.html
【Python魔术方法】迭代器(__iter__和__next__): https://www.jianshu.com/p/1b0686bc166d
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。