本篇内容主要讲解“Python生成器和协程怎么用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Python生成器和协程怎么用”吧!

我们提供的服务有:成都网站制作、做网站、微信公众号开发、网站优化、网站认证、秀屿ssl等。为超过千家企事业单位解决了网站和推广的问题。提供周到的售前咨询和贴心的售后服务,是有科学管理、有技术的秀屿网站制作公司
你将如何生成任意长度的斐波那契数列?显然,你需要跟踪一些数据,并且需要以某种方式对其进行操作以创建下一个元素。
你的第一直觉可能是创建一个可迭代的类,这不失是一个好方法。让我们开始,使用我们在前面几节中已经介绍过的内容:
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生成器和协程怎么用”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!