python2.7 协程,python中协程

  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的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: