python2.7 协程,python中协程
Yyds干货库存
编程通常就是等待。等待函数,等待输入,等待计算,等待测试通过…
如果你的程序等你一次不是很好吗?这正是生成器和协程所做的!在过去的三篇文章中,我们一直在为此做准备,但我很高兴地宣布,等待结束了。
如果你没有读过《循环和迭代器》,《迭代工具》和《列表解析和生成器表达式》,那么你应该先读一下。
其他人,直接从下面开始吧。
了解发电机。你将如何生成任意长度的斐波那契数列?显然,您需要跟踪一些数据,并且需要以某种方式操纵这些数据来创建下一个元素。
你的第一直觉可能是创建一个迭代类,这是一个好方法。让我们从前面几节已经介绍过的内容开始:
斐波纳契类:
def __init__(self,limit):
self.n1=0
self.n2=1
self.n=1
self.i=1
自我限制=限制
def __iter__(self):
回归自我
def __next__(自己):
如果self.i自我限制:
提升停止迭代
if self.i 1:
self.n=self.n1 self.n2
self.n1,self.n2=self.n2,self.n
self.i=1
回归自我
fib=Fibonacci(10)
对于光纤中的I:
打印(一)
让我们把它变得更紧凑。
如果你一直跟随系列到现在,那么这里可能不会有什么惊喜。但是,对于像序列这样简单的东西,这种方法可能感觉有点过头了。
这种情况正是发电机的用途。
定义斐波那契(极限):
如果极限=1:
产量(n2 :=1)
n1=0
for _ in范围(1,限制):
产量(n :=n1 n2)
n1,n2=n2,n
对于斐波那契(10)中的I:
打印(一)
发电机看起来确实更紧凑。——只有9行,而类是——的22行,但也是可读的。
是key yield关键字,它在不退出函数的情况下返回值。Yield在功能上与我们类中的__next__()函数相同。生成器将运行到(并包含)它的yield语句,然后在执行任何操作之前等待另一个__next__()调用。一旦它接到这个电话,它将继续运行,直到达到另一个收益率。
注意:看起来很奇怪:=是Python 3.8中新增的“海象运算符”,赋值并返回值。如果您使用的是Python 3.7或更早版本,可以将这些语句分成两行(分别赋值和编写yield语句)。
您还会注意到缺少raise StopIteration语句。发电机不需要它们;事实上,从PEP 479开始,他们甚至不允许他们这样做。当生成器函数自然终止或使用return语句终止时,StopIteration会在后台自动触发。
发电机试修订日期:2019年11月29日
曾经规定yield不能出现在代码的try-finally的try子句中。PEP 255定义了生成器语法并解释了原因:
难点在于不能保证生成器会被恢复,所以不能保证finally块会被执行;这与finally的目的相反。
这在PEP 342PEP 342中有所改变,在Python 2.5中完成。
那么,为什么要讨论这样一个古老的变化呢?简单:时至今日,我的印象是yield不可能出现在try-finally里。一些关于这个话题的文章错误地引用了旧规则。
把生成器当作对象你可能还记得Python把函数当作对象,生成器也不例外!基于我们前面的例子,我们可以保存生成器的一个特定实例。
比如我只想打印斐波那契数列的第10-20个值怎么办?
首先,我将生成器保存在一个变量中,以便可以重用它。限制对我来说不重要,所以我会用大的。使用我的循环范围可以更容易地显示内容,因为它会使限制逻辑接近打印的语句。
fib=斐波那契(100)
接下来,我将使用循环跳过前10个元素。
for _ in范围(10):
下一步(纤维)
next()函数实际上是循环总是用来推进迭代的函数。对于生成器,它将返回yield返回的任何值。在这种情况下,由于我们还不关心这些值,我们就把它们扔掉(对它们什么也不做)。
对了,我也可以叫fib。__next__() ——这样,但我更喜欢更简洁的方法next(fib)。一般看个人喜好。两者同样有效。
现在我准备从生成器中访问一些值,但不是全部。所以我还是会用range()直接用next()从生成器中检索值。
对于范围(10,21)内的n:
打印(第f { n }个值:{next(fib)} )
这样可以很好地打印出所需的值:
第十值:89
第11个值:144
第12个值:233
第13个值:377
第14个值:610
第15个值:987
第16个值:1597
第17个值:2584
第18个值:4181
19号值:6765
第20个值:10946
请记住,我们之前将限制设置为100,现在我们已经完成了我们的生成器,但我们不应该就这样离开,让它等待另一个next()调用!如果我们程序的其余部分都是空闲的,就会浪费资源(虽然很少)。
相反,我们可以手动告诉我们的生成器我们已经完成了它。
fib.close()
这将手动关闭生成器,就像它已经到达return语句一样。现在垃圾收集器可以清理它了。
知道了协同流程生成器,我们就可以快速定义一个迭代对象,在调用之间存储它的状态。但是,如果我们想要相反的结果:传递信息并让函数耐心地等待它得到信息,该怎么办呢?Python为此提供了一个协同流程。
对于已经对协同学有点熟悉的人,你应该明白我说的是简单协同学(虽然我只是为了读者的理智,一直说“协同学”)。)如果你见过任何使用并发的Python代码,你可能见过它的弟弟,原生协程(也叫“异步协程”)。
现在,理解简单协同学和原生协同学正式被认为是协同学,它们有很多共同的原理;原始协同学是基于简单协同学的概念。我们将在后续文章中讨论异步。
类似地,现在假设当我说“协同”时,我指的是简单的协同。
想象一下,你想找出一串字符串之间所有常见的字母,比如一本书里有趣人物的名字。你不知道有多少弦。它们将在运行时输入,不一定是一次输入。
显然,这种方法必须:
可重复使用。有状态(至今共享的信件。)本质上是迭代的,因为我们不知道会得到多少个字符串。的普通函数不适合这种情况,因为我们要一次性把所有的数据作为列表或者元组传递,它们本身并不存储状态。同时,除非第一次调用,否则生成器无法处理输入。
我们可以尝试创建一个新的类,虽然有很多模板。无论如何,让我们从这里开始,只是为了更好地理解我们正在处理的问题。
在我的第一个版本中,我将修改传递给类的列表,这样我可以随时检查结果。如果我坚持使用类实现,我可能不会那样做,但它是实现我们目的最不可行的类。此外,它在功能上与我们将在后面编写的协程相同,这对于比较实现方法很有用。
类别普通字母计数器:
def __init__(self,results):
self.letters={}
self.counted=[]
自我结果=结果
self.i=0
def add_word(self,word):
word=word.lower()
对于word中的c:
if c.isalpha():
如果c不在自己的信件中:
self.letters[c]=0
self.letters[c]=1
self . counted=sorted(self . letters . items(),key=lambda kv: kv[1])
self . counted=self . counted[:-1]
self.results.clear()
对于自行盘点的项目:
self.results.append(项目)
names=[Skimpole , Sloppy , Wopsle , Toodle , Squeers ,
“蜜雷”,“图金霍恩”,“大黄蜂”,“Wegg”,
斯威夫勒,扫管,果冻,斯米克,希普,
索尔贝里,潘波趣,波德snap ,托克斯,瓦克斯,
斯克里奇,斯诺德格拉斯,温克尔,匹克威克]
结果=[]
counter=CommonLetterCounter(结果)
对于名称中的名称:
counter.add_word(名称)
对于信函,计入结果:
打印(f { letter }次,共{count}次)
根据我的输出,这个数据特别喜欢有E,O,S,L,p的名字,谁知道呢?
我们可以通过使用协同过程来实现相同的结果。
定义count_common_letters(结果):
字母={}
虽然正确:
单词=产量
word=word.lower()
对于word中的c:
if c.isalpha():
如果c不在字母中:
字母[c]=0
字母[c]=1
counted=sorted(letters.items(),key=lambda kv: kv[1])
counted=counted[:-1]
results.clear()
对于盘点中的项目:
结果.追加(项目)
names=[Skimpole , Sloppy , Wopsle , Toodle , Squeers ,
“蜜雷”,“图金霍恩”,“大黄蜂”,“Wegg”,
斯威夫勒,扫管,果冻,斯米克,希普,
索尔贝里,潘波趣,波德snap ,托克斯,瓦克斯,
斯克里奇,斯诺德格拉斯,温克尔,匹克威克]
结果=[]
counter=count_common_letters(结果)
counter.send(None) # prime协程
对于名称中的名称:
counter.send(name) #向协程发送数据
counter.close() #手动结束协程
对于信函,计入结果:
打印(f { letter }次,共{count}次)
让我们仔细看看这里发生了什么。乍一看,协程与函数没有什么不同,但是像生成器一样,yield关键字的使用是非常不同的。
在协同学中,yield代表“等到你的输入,然后在这里使用它”。
您会注意到这两种方法之间的大部分处理逻辑是相同的。我们刚刚取消了课程模板。我们存储流程的实例就像存储对象一样,只是为了确保每次发送更多数据时使用相同的实例。
类统筹和类统筹的主要区别在于用法。我们使用教练的send()函数向教练发送数据:
对于名称中的名称:
counter.send(名称)
在我们这样做之前,我们必须首先调用(上面使用counter.send(None)的那个)或counter。__下一个_ _()。该进程无法立即接收该值;它必须首先运行所有的代码直到它的第一个产量。
与生成器一样,当协程到达其正常执行流的末尾或者到达return语句时,它就完成了。因为在我们的示例中,这些情况都不可能发生,所以我选择手动关闭协调流程:
counter.close()
简而言之,使用协同流程:
将其实例保存为变量,例如counter,用counter.send (none),counter输入协程。_ _ next _ _()或next(counter),用counter.send()发送数据,必要时用counter.close()关闭。
还记得关于生成器的规则吗,不能把yield放在语句的try-finally子句里?但是在这里不适用!因为yield在协同过程中的表现非常不同(处理传入数据而不是传出数据),所以以这种方式使用它是完全可以接受的。
Throw()生成器和协同例程也有一个throw()函数,用于在它们暂停的地方抛出异常。您将从文章《错误和异常》中了解到,异常可以用作代码执行过程的正常部分。
例如,假设您想将数据发送到远程服务器。现在您有了一个连接对象,并且您已经使用流程通过连接发送数据。
在你的代码中,当检测到你已经失去了网络连接,但是由于你与服务器通信的方式,进程发送的所有数据都会被毫无保留地丢弃。
考虑下面我已经删除的示例代码。(假设实际的连接逻辑本身不适合处理回退或报告连接错误。)
类别连接:
模拟到服务器的连接的存根对象
def __init__(self,addr):
self.addr=addr
定义传输(自身,数据):
print(fX: {data[0]},Y: {data[1]}发送到{self.addr} )
定义发送到服务器(连接):
演示发送数据协程
虽然正确:
原始数据=产量
raw_data=raw_data.split( )
坐标=(float(raw_data[0]),float(raw_data[1])
连接传输(坐标)
连接=连接(《example.com》)
发件人=发送至服务器(连接)
sender.send(无)
对于范围(1,6)内的我:
发件人。发送(f"{ 100/I } { 200/I } ")
#模拟连接错误.
连接地址=无
# .但是假设发送者对此一无所知。
对于范围(1,6)内的我:
发件人。发送(f"{ 100/I } { 200/I } ")
运行该示例,我们看到前五个发送()调用转到example.com,但后五个调用转到没有。这显然是不行的——我们想抛出问题,然后开始将数据写到文件中,这样它就不会永远丢失。
这就是投掷()的作用。一旦我们知道我们已经失去了连接,我们就可以提醒协程这个事实,让它做出适当的响应。
我们首先在协程中添加一个尝试-除了:
定义发送到服务器(连接):
虽然正确:
尝试:
原始数据=产量
raw_data=raw_data.split( )
坐标=(float(raw_data[0]),float(raw_data[1])
连接传输(坐标)
除了连接错误:
打印(哎呀!连接中断。正在创建回退。)
#创建回退连接!
连接=连接("本地文件")
我们的使用示例只需要进行一处更改:一旦我们知道我们失去了连接,我们就使用sender.throw(ConnectionError)抛出异常:
连接=连接(《example.com》)
发件人=发送至服务器(连接)
sender.send(无)
对于范围(1,6)内的我:
发件人。发送(f"{ 100/I } { 200/I } ")
#模拟连接错误.
连接地址=无
# .但是假设发送者对此一无所知。
sender.throw(ConnectionError) #警告发件人!
对于范围(1,6)内的我:
发件人。发送(f"{ 100/I } { 200/I } ")
这样的话!现在我们会在协程收到警报后立即收到有关连接问题的消息,并将相关错误内容写入到本地文件,也就是所谓的日志文件。
从.屈服使用生成器或协程时,你不仅限于产量,你还可以使用屈服于。
例如,假设我想重写我的斐波那契数列以使其没有限制,并且我只想编码前五个值。
def fibonacci():
启动器=[1,1,2,3,5]
发酵剂产量
n1=启动器[-2]
n2=启动器[-1]
虽然正确:
产量(n :=n1 n2)
n1,n2=n2,n
在这种情况下,从.屈服暂时移交给另一个可迭代对象,无论它是容器、对象还是另一个生成器。一旦该可迭代对象结束,该生成器就会启动并像往常一样继续运行。
仅仅使用这个生成器,你不会知道它在部分时间内使用了另一个迭代器。它只是像往常一样工作。
fib=fibonacci()
对于范围(1,11)内的n:
打印(第f { n }个值:{next(fib)} )
fib.close()
协程也可以以类似的方式进行切换。例如,在我们的连接示例中,如果我们创建第二个协程来处理将数据写入文件会怎样?如果我们遇到连接错误,我们可以切换到在幕后使用它。
类别连接:
模拟到服务器的连接的存根对象
def __init__(self,addr):
self.addr=addr
定义传输(自身,数据):
print(fX: {data[0]},Y: {data[1]}发送到{self.addr} )
极好的保存到文件():
虽然正确:
原始数据=产量
raw_data=raw_data.split( )
坐标=(float(raw_data[0]),float(raw_data[1])
print(fX: {coords[0]},Y: {coords[1]}发送到本地文件)
定义发送到服务器(连接):
虽然正确:
如果连接为无:
保存到文件()的产出
否则:
尝试:
原始数据=产量
raw_data=raw_data.split( )
坐标=(float(raw_data[0]),float(raw_data[1])
连接传输(坐标)
除了连接错误:
打印(哎呀!连接中断。使用回退。)
连接=无
连接=连接(《example.com》)
发件人=发送至服务器(连接)
sender.send(无)
对于范围(1,6)内的我:
发件人。发送(f"{ 100/I } { 200/I } ")
#模拟连接错误.
连接地址=无
# .但是假设发送者对此一无所知。
sender.throw(ConnectionError) #警告发件人!
对于范围(1,6)内的I:
sender . send(f“{ 100/I } { 200/I }”)
您可能想知道,“我能像从生成器那样直接从协同流程中组合两个返回的数据吗?”
写这篇文章的时候我也对此很好奇。显然你可以。这都是关于识别一个函数何时被视为一个生成器而不是一个协程。
关键很简单:其实__next__()。发送(无)在协同过程中也有效。
def count_common_letters():
字母={}
单词=产量
虽然单词不是没有:
word=word.lower()
对于word中的c:
if c.isalpha():
如果c不在字母中:
字母[c]=0
字母[c]=1
单词=产量
counted=sorted(letters.items(),key=lambda kv: kv[1])
counted=counted[:-1]
对于盘点中的项目:
产量项目
names=[Skimpole , Sloppy , Wopsle , Toodle , Squeers ,
“蜜雷”,“图金霍恩”,“大黄蜂”,“Wegg”,
斯威夫勒,扫管,果冻,斯米克,希普,
索尔贝里,潘波趣,波德snap ,托克斯,瓦克斯,
斯克里奇,斯诺德格拉斯,温克尔,匹克威克]
counter=count_common_letters()
counter.send(无)
对于名称中的名称:
counter.send(名称)
对于信件,在计数器中计数:
打印(f { letter }次,共{count}次)
我只需要观察进程何时开始接收None(当然是在初始启动之后)。由于我已经将yield的结果存储在word中,所以当它变成None时,我可以使用word作为判断条件。
当我们将协程转换成生成器时,它需要在yield开始输出数据之前处理一次send(None)。当调用我们的协程时,在切换到use之前,我们从不显式发送(none);Python在后台完成这项工作。
此外,请记住,协同流程/生成器仍然是一个函数。它只是每次遇到收益率就停顿一下。在我的例子中,我不能突然回到使用counter作为协进程,因为没有执行进程可以让我回到word=yield。实际上,它可以被实现为可以来回切换,但是如果牺牲可读性或者变得太复杂,这可能并不明智。
生成器和合作程序允许你快速编写“等待”你的函数。稍后,我们将了解本机协程,一个用于并发的协程。
让我们回顾一下本节的要点:
生成器是一个迭代器,等待您请求的输出。生成器编写为普通函数,只是它们使用yield关键字返回值,方式与类使用__next__()函数的方式相同。当生成器到达其执行序列的自然结尾,或者遇到return语句时,它将引发StopIteration并结束。协同进程类似于生成器,只是它们等待信息通过函数foo.send()发送给它。生成器和协程都可以使用next(foo)或foo。__next__()以输入下一个收益表。在进程可以用foo.send()向它发送任何东西之前,输入必须用foo.send(None)、next(foo)或foo填充。__下一个_ _()。当前yield可以使用foo.throw()抛出异常。您可以使用foo.close()手动停止生成器或协程。单个函数可以首先表现得像一个协程,然后像一个生成器。原创作品来自程,
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。