赞
踩
Python yield与实现:http://www.cnblogs.com/coder2012/p/4990834.html
官网文档:https://docs.python.org/zh-cn/3.11/glossary.html#term-iterator
为了搞清楚 yield 是用来做什么的,首先得知道 Python 中 generator(生成器) 的相关概念,要理解 generator(生成器) ,的先从 迭代(iteration) 与 迭代器(iterator) 讲起。
迭代是重复反馈过程的活动,其目的通常是为了接近并到达所需的目标或结果。每一次对过程的重复被称为一次“迭代”,而每一次迭代得到的结果会被用来作为下一次迭代的初始值。———— 以上是 维基百科 对迭代的定义。
在 Python中,迭代 通常是通过 for ... in ...
来完成的,而且只要是 可迭代对象(iterable)
,都能进行迭代。
示例:创建一个列表,你可以逐个遍历列表中的元素,而这个过程便叫做 迭代 。
可迭代对象(iterable)
是实现了iterator.__iter__()
方法的对象。
可迭代对象特点:一次只能返回一个成员的对象。
所有能够接受 for...in...
操作的对象都是 可迭代对象,如:列表、字符串、文件等。
如果 可迭代对象 实现了
iterator.__iter__()
方法后,又实现了iterator.__next__()
方法,那么iterator.__iter__()
方法返回值 就叫做 迭代器(iterator),也叫iterator
对象,根据官方的说法,正是这个方法,实现了for ... in ...
语句。
对一个iterable
用for ... in ...
进行迭代时,实际是先通过调用iter()
方法得到一个iterator
,假设叫做X。然后循环地调用X的next()
方法取得每一次的值,直到iterator为空,返回的StopIteration
作为循环结束的标志。for ... in ...
会自动处理StopIteration
异常,从而避免了抛出异常而使程序中断。如图所示
iterator.__next__()可以
显式 地获取一个元素。当调用 next()
方法时,实际上产生了2个操作:
如果你学过 C++
,它其实跟指针的概念很像(如果你还学过链表的话,或许能更好地理解)。
正是 __next__()
,使得iterator
能在每次被调用时,一次只能返回一个值,从而极大的节省了内存资源。另一点需要格外注意的是,iterator
是消耗型的,即每一个值被使用过后,就消失了。因此,你可以将以上的操作2理解成pop
。
可以把
iterator
理解成保存数据的容器,但是这个容器只能遍历一次,遍历之后就变成了一个空的容器了,但不等于None
。若要重复使用容器里面的数据,可以利用list()
方法保存结果。
示例:
from collections.abc import Iterable, Iterator a = [1, 2, 3] # list 是一个 iterable b = iter(a) # 通过 iter()方法, 得到 iterator,iter() 实际上调用了__iter__() print(isinstance(a, Iterable)) # True print(isinstance(a, Iterator)) # False print(isinstance(b, Iterable)) # True print(isinstance(b, Iterator)) # True # 可见, iterable是iterator,但iterator不一定是iterable # iterator 是消耗型的,用一次少一次。对 iterator 进行遍历,iterator就空了! # 通过 list 保存了 迭代器b数据,相当于遍历了迭代器b, 所以list(b)后,迭代器b里面数据就空了 c = list(b) print(c) # [1, 2, 3] # c 已经遍历过了,所以 d 就为 空 了 d = list(b) print(d) # [] if b: # list(b)后,迭代器b里面数据就空了, 但是只是数据为空,b仍然是迭代器对象 print(f"b 不是 None, type(b) ---> {type(b)}") if b is None: print("b 是 None")
再来感受一下 next()
- >>> e = iter(a)
- >>> next(e) #next()实际调用了__next__()方法,此后不再多说
- 1
- >>> next(e)
- 2
itertools
模块包含了许多用来操作可迭代对象的函数。
想复制一个生成器?想连接两个生成器?想把多个值组合到一个嵌套列表里面?使用 map/zip
而不用重新创建一个列表?那么就:import itertools
吧。
让我们来看看四匹马赛跑可能的到达结果:
- >>> horses = [1, 2, 3, 4]
- >>> races = itertools.permutations(horses)
- >>> print(races)
- <itertools.permutations object at 0xb754f1dc>
- >>> print(list(itertools.permutations(horses)))
- [(1, 2, 3, 4),
- (1, 2, 4, 3),
- (1, 3, 2, 4),
- (1, 3, 4, 2),
- (1, 4, 2, 3),
- (1, 4, 3, 2),
- (2, 1, 3, 4),
- (2, 1, 4, 3),
- (2, 3, 1, 4),
- (2, 3, 4, 1),
- (2, 4, 1, 3),
- (2, 4, 3, 1),
- (3, 1, 2, 4),
- (3, 1, 4, 2),
- (3, 2, 1, 4),
- (3, 2, 4, 1),
- (3, 4, 1, 2),
- (3, 4, 2, 1),
- (4, 1, 2, 3),
- (4, 1, 3, 2),
- (4, 2, 1, 3),
- (4, 2, 3, 1),
- (4, 3, 1, 2),
- (4, 3, 2, 1)]
迭代的内部机理:
迭代是一个依赖于可迭代对象(需要实现__iter__()方法)和迭代器(需要实现__next__()方法)的过程。
可迭代对象是任意你可以从中得到一个迭代器的对象。
迭代器是让你可以对可迭代对象进行迭代的对象。
常说的 "生成器",就是 "带有 yield 的函数"。
带yield的函数是一个生成器,而不在是一个函数,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。
生成器 是 这样一个函数,它记住上一次返回时在函数体中的位置。
对生成器函数的第二次(或第 n 次)调用跳转至该函数中间,而上次调用的所有局部变量都保持不变。生成器不仅 “记住” 了它的数据状态;生成器还 “记住” 了它的流控制构造。
生成器的特点:
- 1. 生成器是一个函数,而且函数的参数都会保留。
- 2. 迭代到下一次的调用时,所使用的参数都是第一次所保留下的,即是说,在整个所有函数调用的参数都是第一次所调用时保留的,而不是新创建的
- 3. 节约内存
- 一个生成器函数的定义很像一个普通的函数,除了当它要生成一个值的时候,使用 yield 关键字而不是 return。如果一个 def 的主体包含 yield,这个函数会自动变成一个生成器(即使它包含一个 return)。创建一个生成器就这么简单。。。
生成器 也是 迭代器,也只能对它们进行一次迭代,原因在于它们并没有将所有数据存储在内存中,而是即时生成这些数据。
生成器表达式
my_generator = (x*x for x in range(3))
for i in my_generator:
print(i)
这一段代码和上面 迭代 那段很相似,唯一不同的地方是使用了()
代替 []
。但是,这样的后果是你无法对 my_generator 进行第二次遍历,因为生成器只能被使用一次:它首先计算出结果0,然后忘记它再计算出1,最后是4,一个接一个。
- a = (elem for elem in [1, 2, 3])
- print(f"a ---> {a}")
-
-
- def fib():
- a, b = 0, 1
- while True:
- yield b
- a, b = b, a + b
-
-
- print(f"fib ---> {fib}")
-
- # fib 是 一个函数,但是fib中有yield关键字,所以函数返回值是一个生成器
- b = fib()
- print(f"b ---> {b}")
其实说白了,generator
就是 iterator
的一种,以更优雅的方式实现的 iterator
。
官方的说法是:Python’s generators provide a convenient way to implement the iterator protocol.
你完全可以像使用 iterator
一样使用 generator
,但是请记住他们两个的定义不一样:
iterator
,你需要分别实现 __iter__()
方法和 __next__()
方法,generator
只需要一个小小的 yield
( 好吧,generator expression
的使用比较简单,就不展开讲了。)Python 中 生成器 是使用 yield 关键字 来实现的。
示例:
- def func_test():
- for i in range(5):
- yield i #
- print(i + 100)
-
-
- t = func_test()
- for i in t:
- print(i)
- pass
可以 单步调试 上面这个代码,就可以 验证 上面 两个 特点。
前文讲到 iterator
通过 __next__()
方法实现了每次调用,返回一个单一值的功能。而 yield
就是实现 generator
的 __next__()
方法的关键!先来看一个最简单的例子:
- >>> def g():
- ... print("1 is")
- ... yield 1
- ... print("2 is")
- ... yield 2
- ... print("3 is")
- ... yield 3
- ...
- >>> z = g()
- >>> z
- <generator object g at 0x7f0d2387c8b8>
- >>> next(z)
- 1 is
- 1
- >>> next(z)
- 2 is
- 2
- >>> next(z)
- 3 is
- 3
- >>> next(z)
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- StopIteration
解释:
next()
方法时,函数似乎执行到 yield 1
,就暂停了。next()
方法时,函数从 yield 1
之后开始执行的,并再次暂停。next()
方法时,从第二次暂停的地方开始执行。next()
方法时,抛出StopIteration
异常。事实上,generator
确实在遇到 yield
之后暂停了,确切点说,是先返回了 yield
表达式的值,再暂停的。当再次调用 next()
时,从先前暂停的地方开始执行,直到遇到下一个 yield
。这与上文介绍的对iterator
调用next()
方法,执行原理一般无二。
有些教程里说
generator
保存的是算法,而我觉得用中断服务子程序
来描述generator
或许能更好理解,这样你就能将yield
理解成一个中断服务子程序的断点
,没错,是中断服务子程序的断点。我们每次对一个generator
对象调用next()
时,函数内部代码执行到 "断点"yield
,然后返回这一部分的结果,并保存上下文环境,"中断" 返回。
怎么样,是不是瞬间就明白了yield
的用法,
再来看另一段代码。
- >>> def gen():
- ... while True:
- ... s = yield
- ... print(s)
- ...
- >>> g = gen()
- >>> g.send("111")
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- TypeError: can't send non-None value to a just-started generator
- >>> next(g)
- >>> g.send("222")
- 222
generator
其实有第二种调用方法(恢复执行),即通过 send(value)
方法将 value
作为 yield
表达式的当前值,你可以用该值再对其他变量进行赋值,
这一段代码就很好理解了。
- g = gen() 定义一个变量,用来保存生成器
- g.send("111") 时,因为生成器没有执行,所以报错
- netxt(g) ,生成器执行到 yield 处并暂停,然后返回。
- g.send("222") ,由于yield的缘故被暂停了。此时,send(value) 方法传入的值作为 yield 表达式的值,函数中又将该值赋给了变量 s,然后 print 函数打印 s,循环再遇到 yield,暂停返回。如此循环,直到结束
注意:
调用
send(value)
时要注意,要确保generator
是在yield
处被暂停了,如此才能向yield
表达式传值,否则将会报错(如上所示),可通过next()
方法或send(None)
使generator
执行到yield
。
再来看一段 yield
更复杂的用法,或许能加深你对 generator
的 next()
与 send(value)
的理解。
- def echo(value=None):
- while 1:
- value = (yield value)
- print("The value is", value)
- if value:
- value += 1
- print('add +1 value', value)
- print('**************************************')
-
-
- # 调用send(value)时要注意,要确保generator是在yield处被暂停了,
- # 如此才能向yield表达式传值,否则将会报错
- # 可通过next()方法或send(None)使generator执行到yield。
- # 生成器(generator) 有两种方法 恢复执行:1. send() 方法。2. next() 方法
-
- g = echo(1) # 返回一个 生成器
- print(next(g)) # 通过 next() 方法 使 生成器 执行到 yield 处暂停
- g.send(2) # send(value)方法传入的值作为yield表达式的值
- g.send(5)
- next(g)
- next(g)
- next(g)
- """
- 执行结果:
- 1
- The value is 2
- add +1 value 3
- **************************************
- The value is 5
- add +1 value 6
- **************************************
- The value is None
- **************************************
- The value is None
- **************************************
- The value is None
- **************************************
- """
上述代码既有 yield value 的形式,又有 value = yield 形式,看起来有点复杂。但以 yield 分离代码进行解读,就不太难了。
但是,这里就引出了另一个问题,yield 作为一个暂停恢复的点,代码从 yield 处恢复,又在下一个 yield 处暂停。可见,在一次 next()(非首次) 或 send(value)调用过程中,实际上存在 2 个 yield
因此,也就有 2 个 yield 表达式。send(value)方法是将值传给恢复点yield。调用next()表达式的值时,其恢复点yield的值总是为None,而将暂停点的yield表达式的值返回。为方便记忆,你可以将此处的恢复点记作当前的(current),而将暂停点记作下一次的(next),这样就与next()方法匹配起来啦。
generator
还实现了另外两个方法throw(type[, value[, traceback]])
与close()
。前者用于抛出异常,后者用于关闭generator
.不过这2个方法似乎很少被直接用到,本文就不再多说了,有兴趣的同学请看这里。
示例解析:
- # generation.py
- def gen():
- for x in range(4):
- tmp = yield x
- if tmp == "hello":
- print("world")
- else:
- print(f"12345abcd_{str(tmp)}")
-
-
- c = gen()
- next(c)
- next(c)
- c.send("python")
-
- """
- 12345abcd_None
- 12345abcd_python
- """
执行到 yield 时,gen 函数暂时停止并保存,返回 x 的值,同时 tmp 接收 send 的值(ps:yield x 相当于 return x ,所以第一次c.next()结果是0。第二次 next(c) 时,继续在原来暂停的地方执行,因为没有send 值,所以 tmp 为 None。next(c) 等价 c.send(None))。下次c.send(“python”),send发送过来的值,next(c) 等价 c.send(None)
了解了next()如何让包含yield的函数执行后,我们再来看另外一个非常重要的函数send(msg)。其实next()和send()在一定意义上作用是相似的,区别是send()可以传递yield表达式的值进去,而next()不能传递特定的值,只能传递None进去。因此,我们可以看做c.next() 和 c.send(None) 作用是一样的。
需要提醒的是,第一次调用时,请使用next()语句或是send(None),不能使用send发送一个非None的值,否则会出错的,因为没有Python yield语句来接收这个值。
理解了这些,我们就可以向协同程序发起攻击了,所谓协同程序也就是是可以挂起,恢复,有多个进入点。其实说白了,也就是说多个函数可以同时进行,可以相互之间发送消息等。
非标准模块 下载:https://github.com/dongjiawei316/multitask
使用 multitask 的简单代码:
- def tt():
- for x in range(4):
- print('tt' + str(x))
- yield
-
-
- def gg():
- for x in range(4):
- print('xx' + str(x))
- yield
-
-
- t = multitask.TaskManager()
- t.add(tt())
- t.add(gg())
- t.run()
下载地址:https://pypi.org/search/?q=multitask,pypi 地址:https://pypi.org/project/python-multitasking/
安装:pip install python-multitasking 。使用示例:
- import multitasking
- import time
- import random
- import requests
- import signal
- import urllib3
-
- urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
- # kill all tasks on ctrl-c
- signal.signal(signal.SIGINT, multitasking.killall)
-
-
- # or, wait for task to finish on ctrl-c:
- # signal.signal(signal.SIGINT, multitasking.wait_for_tasks)
-
-
- @multitasking.task # <== this is all it takes :-)
- def hello(index):
- global global_list_flag
- url = 'https://www.baidu.com/'
- print(f'{index} : {url}')
- req = requests.get(url, verify=False)
- print(f'{index} : {req.status_code}')
- global_list_flag[index-1] = 1
-
-
- if __name__ == "__main__":
- count = 10
- global_list_flag = [0 for _ in range(count)]
- for i in range(0, count):
- hello(i + 1)
- # https://www.yuanrenxue.com/python/python-asyncio-demo.html
- multitasking.wait_for_tasks()
如果不是使用生成器,那么要实现上面现象,即函数交错输出,那么只能使用线程了,所以生成器给我们提供了更广阔的前景。
如果仅仅是实现上面的效果,其实很简单,我们可以自己写一个。主要思路就是将生成器对象放入队列,执行send(None)后,如果没有抛出StopIteration,将该生成器对象再加入队列。
- # python 2.X 叫 Queue
- # python 3.X 叫 queue
- import queue
- import multitask
-
-
- def tt():
- for x in range(4):
- print('tt' + str(x))
- yield
-
-
- def gg():
- for x in range(4):
- print('xx' + str(x))
- yield
-
-
- class Task(object):
- def __init__(self):
- self._queue = queue.Queue()
-
- def add(self, gen):
- self._queue.put(gen)
-
- def run(self):
- while not self._queue.empty():
- for i in range(self._queue.qsize()):
- try:
- gen = self._queue.get()
- gen.send(None)
- except StopIteration:
- pass
- else:
- self._queue.put(gen)
-
-
- t = Task()
- t.add(tt())
- t.add(gg())
- t.run()
当然,multitask 实现的肯定不止这个功能,有兴趣的童鞋可以看下源码,还是比较简单易懂的。
有这么一道题目,模拟多线程交替输出:
- def thread1():
- for x in range(4):
- yield x
-
-
- def thread2():
- for x in range(4, 8):
- yield x
-
-
- threads = []
- threads.append(thread1())
- threads.append(thread2())
-
-
- def run(threads): # 写这个函数,模拟线程并发
- pass
-
-
- run(threads)
如果上面 class Task 看懂了,那么这题很简单,其实就是考你用yield模拟线程调度,解决如下:
- def thread1():
- for x in range(4):
- yield x
-
-
- def thread2():
- for x in range(4, 8):
- yield x
-
-
- td_list = list()
- td_list.append(thread1())
- td_list.append(thread2())
-
-
- def run(thread_list):
- for td in thread_list:
- try:
- print(next(td))
- except StopIteration:
- pass
- else:
- thread_list.append(td)
-
-
- run(td_list)
可迭代对象(Iterable)是实现了
__iter__()
方法的对象,通过调用iter()
方法可以获得一个迭代器(Iterator)。迭代器(Iterator)是实现了
__iter__()
和__next__()
的对象。
for ... in ...
的迭代,实际是将可迭代对象转换成迭代器,再重复调用next()
方法实现的。生成器(generator)是一个特殊的迭代器,它的实现更简单优雅
yield
是生成器实现__next__()
方法的关键。它作为生成器执行的暂停恢复点,可以对yield
表达式进行赋值,也可以将yield
表达式的值返回。
yield
语句将你的函数转化成一个能够生成一种能够包装你原函数体的名叫生成器 的特殊对象的工厂。
当生成器被迭代时,它将会从起始位置开始执行函数一直到达下一个yield
,然后挂起执行,计算返回传递给yield
的值,它将会在每次迭代的时候重复这个过程直到函数执行到达函数的尾部,举例来说:
- def simple_generator():
- yield 'one'
- yield 'two'
- yield 'three'
- for i in simple_generator():
- print i
-
- 输出结果为:
- one
- two
- three
这种效果的产生是由于在循环中使用了可以产生序列的生成器,生成器在每次循环时执行代码到下一个yield
,并计算返回结果,这样生成器即时生成了一个列表,这对于特别是大型计算来说内存节省十分有效。
假设你想实现自己的可以产生一个可迭代一定范围数的range
函数(特指Python 2.x中的range
),你可以这样做和使用:
- def myRangeNaive(i):
- n = 0
- range = []
- while n < i:
- range.append(n)
- n = n + 1
- return range
- for i in myRangeNaive(10):
- print i
但是这样并不高效,原因1:你创建了一个你只会使用一次的列表;原因2:这段代码实际上循环了两次。
由于Guido和他的团队很慷慨地开发了生成器因此我们可以这样做:
- def myRangeSmart(i):
- n = 0
- while n < i:
- yield n
- n = n + 1
- return
- for i in myRangeSmart(10):
- print i
现在,每次对生成器迭代将会调用next()
来执行函数体直到到达yield
语句,然后停止执行,并计算返回结果,或者是到达函数体尾部。在这种情况下,第一次的调用next()
将会执行到yield n
并返回n
,下一次的next()
将会执行自增操作,然后回到while
的判断,如果满足条件,则再一次停止并返回n
,它将会以这种方式执行一直到不满足while
条件,使得生成器到达函数体尾部。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。