本篇内容主要讲解“Python生成器和协程怎么用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Python生成器和协程怎么用”吧!
你将如何生成任意长度的斐波那契数列?显然,你需要跟踪一些数据,并且需要以某种方式对其进行操作以创建下一个元素。
你的第一直觉可能是创建一个可迭代的类,这不失是一个好方法。让我们开始,使用我们在前面几节中已经介绍过的内容:
class Fibonacci: def __init__(self, limit): self.n1 = 0 self.n2 = 1 self.n = 1 self.i = 1 self.limit = limit def __iter__(self): return self def __next__(self): if self.i > self.limit: raise StopIteration if self.i > 1: self.n = self.n1 + self.n2 self.n1, self.n2 = self.n2, self.n self.i += 1 return self.n fib = Fibonacci(10) for i in fib: print(i)
让我们把它变得更紧凑。
如果你到目前为止一直在关注该系列,那么这里可能不会有任何惊喜。然而,对于像序列这样简单的事情,这种方法可能会让人觉得有点过头了。
这种情况正是生成器的用途。
def fibonacci(limit): if limit >= 1: yield (n2 := 1) n1 = 0 for _ in range(1, limit): yield (n := n1 + n2) n1, n2 = n2, n for i in fibonacci(10): print(i)
生成器看起来肯定更紧凑——只有 9 行长,而类为 22 行——但它同样可读。
关键是yield
关键字,它返回一个值而不退出函数。yield
在功能上与我们类中的__next__()
函数相同。生成器将运行到(并包括)它的yield
语句,然后在它做任何事情之前等待另一个__next__()
调用。一旦它得到那个调用,它将继续运行,直到它碰到另一个yield
。
注意:看起来很奇怪的
:=
是 Python 3.8 中的新“海象运算符”,它分配并返回一个值。如果你使用的是 Python 3.7 或更早版本,则可以将这些语句分成两行(单独去赋值和写yield
语句)。
你还会注意到缺少raise StopIteration
声明。生成器不需要它们;事实上,自PEP 479以来,他们甚至不允许他们这样做。当生成器函数自然终止或使用return
语句终止时,StopIteration
会在幕后自动触发。
修订日期:2019 年 11 月 29 日
曾经规定了yield
不能出现在代码中try
子句中的try-finally
中。PEP 255定义了生成器语法,解释了原因:
难点在于不能保证生成器会被恢复,因此不能保证 finally 块会被执行;这就违背finally的目的了。
这在 PEP 342 PEP 342中进行了更改,并在 Python 2.5 中完成。
那么,为什么要讨论这样一个古老的变化呢?简单:直到今天,我的印象是yield
无法出现在try-finally
中. 一些关于该主题的文章错误地引用了旧规则。
你可能还记得 Python 将函数视为对象,生成器也不例外!在我们之前的示例的基础上,我们可以保存生成器的特定实例。
例如,如果我只想打印斐波那契数列的第 10-20 个值怎么办?
首先,我将生成器保存在一个变量中,以便我可以重用它。限制对我来说并不重要,所以我会使用大的限制。使用我的循环范围来更容易显示内容,因为这会使限制逻辑接近打印语句。
fib = fibonacci(100)
接下来,我将使用循环跳过前 10 个元素。
for _ in range(10): next(fib)
next()
函数实际上是循环始终用于推进迭代的函数。在生成器的情况下,这将返回由yield
返回的任何值。在这种情况下,由于我们还不关心这些值,我们只是将它们扔掉(对它们什么都不做)。
顺便说一句,我也可以这样调用fib.__next__()
——但我更喜欢采取的更简洁方法next(fib)
。它通常取决于个人偏好。两者同样有效。
我现在准备好从生成器访问一些值,但不是全部。因此,我仍将使用range()
,并直接使用next()
从生成器中检索值。
for n in range(10, 21): print(f"{n}th value: {next(fib)}")
这可以很好地打印出所需的值:
10th value: 89 11th value: 144 12th value: 233 13th value: 377 14th value: 610 15th value: 987 16th value: 1597 17th value: 2584 18th value: 4181 19th value: 6765 20th value: 10946
还记得我们之前将限制设置为 100,现在已经完成了我们的生成器,但我们不应该直接离开并让它等待另一个next()
调用!我们程序的其余部分处于空闲状态就会浪费资源(尽管很少)。
相反,我们可以手动告诉我们的生成器我们已经完成了它。
fib.close()
这将手动关闭生成器,就像它已经到达一个return
语句一样。它现在可以由垃圾收集器清理。
生成器允许我们快速定义一个在调用之间存储其状态的可迭代对象。但是,如果我们想要相反的结果:传递信息并让函数耐心等待它得到它呢?Python为此提供了协程。
对于已经有点熟悉协程的人,你应该明白我所指的是简单的协程(尽管我只是为了读者的理智而自始至终都在说“协程”。)如果你已经看过任何使用并发的 Python 代码,你可能已经遇到过它的小弟,原生协程(也称为“异步协程”)。
现在,了解简单协程和原生协程都被官方认为是“协程”,它们有很多共同的原则;原生协程建立在简单协程引入的概念之上。我们会在后续的文章中讨论async
。
同样,现在假设当我说“协程”时,我指的是一个简单的协程。
想象一下,你想找到一堆字符串之间的所有共同字母,比如一本书籍中那些有趣的人物名字。你不知道有多少字符串,它们会在运行时输入,不一定是一次全部输入。
显然,这种方法必须:
可重复使用。
有状态(到目前为止共有的字母。)
本质上是迭代的,因为我们不知道我们会得到多少个字符串。
普通的函数并不适合这种情况,因为我们必须一次将所有数据作为列表或元组传递,而且它们本身不存储状态。同时,生成器不能处理输入,除非是第一次调用。
我们可以尝试新建一个类,尽管有很多模板。不管怎样,让我们从这儿开始,只是为了更好地掌握我们正在处理的内容。
在我的第一个版本中,我将对传递给类的列表进行修改,因此我可以随时查看结果。如果我坚持使用类实现,我可能不会那样做,但它是实现我们目的最小的可行类了。此外,它在功能上与我们稍后将要编写的协程相同,这用来比较实现方法很有用。
class CommonLetterCounter: def __init__(self, results): self.letters = {} self.counted = [] self.results = results self.i = 0 def add_word(self, word): word = word.lower() for c in word: if c.isalpha(): if c not in self.letters: 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() for item in self.counted: self.results.append(item) names = ['Skimpole', 'Sloppy', 'Wopsle', 'Toodle', 'Squeers', 'Honeythunder', 'Tulkinghorn', 'Bumble', 'Wegg', 'Swiveller', 'Sweedlepipe', 'Jellyby', 'Smike', 'Heep', 'Sowerberry', 'Pumblechook', 'Podsnap', 'Tox', 'Wackles', 'Scrooge', 'Snodgrass', 'Winkle', 'Pickwick'] results = [] counter = CommonLetterCounter(results) for name in names: counter.add_word(name) for letter, count in results: print(f'{letter} apppears {count} times.')
根据我的输出,这本数据特别喜欢带有 e、o、s、l 和 p 的名字。谁知道?
我们可以使用协程完成相同的结果。
def count_common_letters(results): letters = {} while True: word = yield word = word.lower() for c in word: if c.isalpha(): if c not in letters: letters[c] = 0 letters[c] += 1 counted = sorted(letters.items(), key=lambda kv: kv[1]) counted = counted[::-1] results.clear() for item in counted: results.append(item) names = ['Skimpole', 'Sloppy', 'Wopsle', 'Toodle', 'Squeers', 'Honeythunder', 'Tulkinghorn', 'Bumble', 'Wegg', 'Swiveller', 'Sweedlepipe', 'Jellyby', 'Smike', 'Heep', 'Sowerberry', 'Pumblechook', 'Podsnap', 'Tox', 'Wackles', 'Scrooge', 'Snodgrass', 'Winkle', 'Pickwick'] results = [] counter = count_common_letters(results) counter.send(None) # prime the coroutine for name in names: counter.send(name) # send data to the coroutine counter.close() # manually end the coroutine for letter, count in results: print(f'{letter} apppears {count} times.')
让我们仔细看看这里发生了什么。乍一看,协程与函数并没有什么不同,但与生成器一样,yield
关键字的使用就大不相同了。
在协程中,yield
它代表“等到你的输入,然后在这里使用它”。
你会注意到两种方法之间的大多数处理逻辑是相同的。我们只是取消了类模板。我们存储协程的实例就像存储对象一样,只是为了确保每次向它发送更多数据时都使用相同的实例。
类和协程之间的主要区别在于用法。我们使用协程的send()
函数向协程发送数据:
for name in names: counter.send(name)
在我们这样做之前,我们必须首先调用(上面使用counter.send(None)
的)或counter.__next__()
。协程不能立即接收值;它必须首先运行它的所有代码,直到它的第一个yield
.
与生成器一样,协程在到达其正常执行流程的末尾或到达return
语句时完成。由于在我们的示例中这些情况都没有发生的机会,所以我选择手动关闭协程:
counter.close()
简而言之,使用协程:
将其实例保存为变量,例如counter
,
用counter.send(None)
,counter.__next__()
或next(counter)
输入协程,
用counter.send()
发送数据,
如有必要,用counter.close()
关闭它。
还记得关于生成器的规则,不能将 yield
放在语句的try
子句中try-finally
吗?但是这里不适用!因为yield
在协程中的行为非常不同(处理传入数据,而不是传出数据),以这种方式使用它是完全可以接受的。
生成器和协程也有一个throw()
函数,用于在它们暂停的地方引发异常。你会从《错误和异常》一文中了解到,异常可以用作代码执行流程的正常部分。
例如,假设你想将数据发送到远程服务器。你现在已经有一个连接对象,并且已使用协程通过该连接发送数据。
在你的代码中,当检测到你已经失去了网络连接,但是由于你与服务器的通信方式,协程发送的所有数据都会毫无保留的被丢弃。
考虑一下下面这个我已经删除的示例代码。(假设实际的连接逻辑本身不适合处理回退或报告连接错误。)
class Connection: """ Stub object simulating connection to a server """ def __init__(self, addr): self.addr = addr def transmit(self, data): print(f"X: {data[0]}, Y: {data[1]} sent to {self.addr}") def send_to_server(conn): """ Coroutine demonstrating sending data """ while True: raw_data = yield raw_data = raw_data.split(' ') coords = (float(raw_data[0]), float(raw_data[1])) conn.transmit(coords) conn = Connection("example.com") sender = send_to_server(conn) sender.send(None) for i in range(1, 6): sender.send(f"{100/i} {200/i}") # Simulate connection error... conn.addr = None # ...but assume the sender knows nothing about it. for i in range(1, 6): sender.send(f"{100/i} {200/i}")
运行该示例,我们看到前五个send()
调用转到example.com
,但后五个调用转到None
。这显然是不行的——我们想抛出问题,然后开始将数据写到文件中,这样它就不会永远丢失。
这就是throw()
的作用。一旦我们知道我们已经失去了连接,我们就可以提醒协程这个事实,让它做出适当的响应。
我们首先在协程中添加一个try-except
:
def send_to_server(conn): while True: try: raw_data = yield raw_data = raw_data.split(' ') coords = (float(raw_data[0]), float(raw_data[1])) conn.transmit(coords) except ConnectionError: print("Oops! Connection lost. Creating fallback.") # Create a fallback connection! conn = Connection("local file")
我们的使用示例只需要进行一处更改:一旦我们知道我们失去了连接,我们就使用sender.throw(ConnectionError)
抛出异常:
conn = Connection("example.com") sender = send_to_server(conn) sender.send(None) for i in range(1, 6): sender.send(f"{100/i} {200/i}") # Simulate connection error... conn.addr = None # ...but assume the sender knows nothing about it. sender.throw(ConnectionError) # ALERT THE SENDER! for i in range(1, 6): sender.send(f"{100/i} {200/i}")
这样的话!现在我们会在协程收到警报后立即收到有关连接问题的消息,并将相关错误内容写入到本地文件,也就是所谓的日志文件。
使用生成器或协程时,你不仅限于yield
,你还可以使用yield from
.
例如,假设我想重写我的斐波那契数列以使其没有限制,并且我只想编码前五个值。
def fibonacci(): starter = [1, 1, 2, 3, 5] yield from starter n1 = starter[-2] n2 = starter[-1] while True: yield (n := n1 + n2) n1, n2 = n2, n
在这种情况下,yield from
暂时移交给另一个可迭代对象,无论它是容器、对象还是另一个生成器。一旦该可迭代对象结束,该生成器就会启动并像往常一样继续运行。
仅仅使用这个生成器,你不会知道它在部分时间内使用了另一个迭代器。它只是像往常一样工作。
fib = fibonacci() for n in range(1,11): print(f"{n}th value: {next(fib)}") fib.close()
协程也可以以类似的方式进行切换。例如,在我们的 连接示例中,如果我们创建第二个协程来处理将数据写入文件会怎样?如果我们遇到连接错误,我们可以切换到在幕后使用它。
class Connection: """ Stub object simulating connection to a server """ def __init__(self, addr): self.addr = addr def transmit(self, data): print(f"X: {data[0]}, Y: {data[1]} sent to {self.addr}") def save_to_file(): while True: raw_data = yield raw_data = raw_data.split(' ') coords = (float(raw_data[0]), float(raw_data[1])) print(f"X: {coords[0]}, Y: {coords[1]} sent to local file") def send_to_server(conn): while True: if conn is None: yield from save_to_file() else: try: raw_data = yield raw_data = raw_data.split(' ') coords = (float(raw_data[0]), float(raw_data[1])) conn.transmit(coords) except ConnectionError: print("Oops! Connection lost. Using fallback.") conn = None conn = Connection("example.com") sender = send_to_server(conn) sender.send(None) for i in range(1, 6): sender.send(f"{100/i} {200/i}") # Simulate connection error... conn.addr = None # ...but assume the sender knows nothing about it. sender.throw(ConnectionError) # ALERT THE SENDER! for i in range(1, 6): sender.send(f"{100/i} {200/i}")
你可能想知道:“我可以像从生成器中那样直接从协程中组合两个返回数据吗?”
我在写这篇文章时也对此感到好奇,显然你可以。这一切都与识别函数何时被视为生成器而不是协程有关。
关键很简单:实际上__next__()
。send(None)
在协程中同样有效。
def count_common_letters(): letters = {} word = yield while word is not None: word = word.lower() for c in word: if c.isalpha(): if c not in letters: letters[c] = 0 letters[c] += 1 word = yield counted = sorted(letters.items(), key=lambda kv: kv[1]) counted = counted[::-1] for item in counted: yield item names = ['Skimpole', 'Sloppy', 'Wopsle', 'Toodle', 'Squeers', 'Honeythunder', 'Tulkinghorn', 'Bumble', 'Wegg', 'Swiveller', 'Sweedlepipe', 'Jellyby', 'Smike', 'Heep', 'Sowerberry', 'Pumblechook', 'Podsnap', 'Tox', 'Wackles', 'Scrooge', 'Snodgrass', 'Winkle', 'Pickwick'] counter = count_common_letters() counter.send(None) for name in names: counter.send(name) for letter, count in counter: print(f'{letter} apppears {count} times.')
我只需要观察协程何时开始接收None
(当然是在初始启动之后)。由于我在word
中存储了yield
的结果,因此我可以用word
变成None
时作为跳出循环的判断条件。
当我们将协程转化为生成器时,它需要在yield
开始输出数据之前处理单个send(None)
。在调用我们的协程时,我们在切换使用之前从未明确地send(None)
;Python 在后台执行此操作。
另外,请记住协程/生成器仍然是一个函数。它只是在每次遇到yield
时暂停。在我的示例中,我不能突然回去使用counter
作为协程,因为没有执行流程可以让我回到word = yield
。其实完全可以实现它,以便你可以来回切换,但如果它以牺牲可读性或变得过于复杂为代价,则可能不明智。
到此,相信大家对“Python生成器和协程怎么用”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。