Python生成器和协程怎么用

本篇内容主要讲解“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()

生成器和协程也有一个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 from

使用生成器或协程时,你不仅限于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生成器和协程怎么用”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!


文章名称:Python生成器和协程怎么用
当前网址:http://ybzwz.com/article/jecdjg.html