我们可以使用Python生成器函数做什么?

时间:2020-03-06 14:26:21  来源:igfitidea点击:

我开始学习Python,并且遇到过生成器函数,这些函数中包含yield语句。我想知道这些功能确实可以解决哪些类型的问题。

解决方案

请参阅PEP 255中的"动机"部分。

生成器的一种非显而易见的用法是创建可中断的函数,这使我们可以在不使用线程的情况下执行诸如更新UI或者"同时"(实际上是交错的)运行多个作业的操作。

生成器为我们提供懒惰的评估。我们可以通过遍历它们来使用它们,可以通过" for"显式地使用它们,也可以通过将其传递给任何迭代的函数或者构造来隐式地使用它们。我们可以将生成器视为返回多个项目,就像它们返回一个列表一样,但是与其一次一次返回它们,而不是一次全部返回它们,而是将生成器功能暂停直到请求下一个项目。

生成器适用于计算大量结果(特别是涉及循环本身的计算),在这些情况下我们不知道是否需要所有结果,或者不想在同一时间为所有结果分配内存。或者对于生成器使用另一台生成器或者消耗某些其他资源的情况,如果这种情况发生得越晚越方便。

生成器的另一个用途(实际上是相同的)是将迭代替换为迭代。在某些情况下,我们希望函数执行大量工作,并偶尔向调用者报告。传统上,我们将为此使用回调函数。我们将此回调传递给工作函数,它将定期调用此回调。生成器方法是工作函数(现在是生成器)对回调一无所知,仅在需要报告某些内容时才产生。调用者没有编写单独的回调并将其传递给工作函数,而是在生成器周围的一个" for"循环中完成所有报告工作。

例如,假设我们编写了一个"文件系统搜索"程序。我们可以完整地执行搜索,收集结果,然后一次显示一个。在显示第一个结果之前,必须先收集所有结果,并且所有结果将同时存储在内存中。或者,我们可以在找到结果的同时显示结果,这样可以提高内存效率,并且对用户友好得多。后者可以通过将结果打印功能传递给文件系统搜索功能来完成,也可以仅通过使搜索功能成为生成器并遍历结果来完成。

如果要查看后两种方法的示例,请参见os.path.walk()(带有回调的旧文件系统行走功能)和os.walk()(新的文件系统行走生成器。)当然,如果我们确实想将所有结果收集到列表中,生成器方法可以轻松转换为大列表方法:

big_list = list(the_generator)

遍历输入保持状态时,基本上避免使用回调函数。

请参见此处和此处,以了解使用生成器可以完成的操作的概述。

使用生成器的原因之一是使某种解决方案的解决方案更清晰。

另一种是一次处理一个结果,避免建立庞大的结果列表,而这些结果无论如何都要分开处理。

如果我们有一个像这样的fibonacci-up-to-n函数:

# function version
def fibon(n):
    a = b = 1
    result = []
    for i in xrange(n):
        result.append(a)
        a, b = b, a + b
    return result

我们可以这样更轻松地编写函数:

# generator version
def fibon(n):
    a = b = 1
    for i in xrange(n):
        yield a
        a, b = b, a + b

功能更清晰。如果我们使用这样的功能:

for x in fibon(1000000):
    print x,

在此示例中,如果使用生成器版本,则将根本不会创建整个1000000项列表,一次只能创建一个值。使用列表版本时,情况并非如此,先创建列表。

我最喜欢的用法是"过滤器"和"减少"操作。

假设我们正在读取文件,只需要以" ##"开头的行。

def filter2sharps( aSequence ):
    for l in aSequence:
        if l.startswith("##"):
            yield l

然后,我们可以在适当的循环中使用generator函数

source= file( ... )
for line in filter2sharps( source.readlines() ):
    print line
source.close()

减少示例类似。假设我们有一个文件,需要在其中定位" <Location> ... </ Location>"行的块。 [不是HTML标签,而是恰好看起来像标签的行。]

def reduceLocation( aSequence ):
    keep= False
    block= None
    for line in aSequence:
        if line.startswith("</Location"):
            block.append( line )
            yield block
            block= None
            keep= False
        elif line.startsWith("<Location"):
            block= [ line ]
            keep= True
        elif keep:
            block.append( line )
        else:
            pass
    if block is not None:
        yield block # A partial block, icky

同样,我们可以在适当的for循环中使用此生成器。

source = file( ... )
for b in reduceLocation( source.readlines() ):
    print b
source.close()

这个想法是,生成器函数允许我们过滤或者减少一个序列,一次生成另一个序列一个值。

缓冲。当以大块获取数据但以小块处理数据效率很高时,生成器可能会有所帮助:

def bufferedFetch():
  while True:
     buffer = getBigChunkOfData()
     # insert some code to break on 'end of data'
     for i in buffer:    
          yield i

上面的内容使我们可以轻松地将缓冲与处理分开。现在,消费者函数可以仅一个接一个地获取值,而不必担心缓冲。

成堆的东西。任何时候我们都想生成一系列项目,但又不想一次将它们"物化"到一个列表中。例如,我们可能有一个简单的生成器,该生成器返回质数:

def primes():
    primes_found = set()
    primes_found.add(2)
    yield 2
    for i in itertools.count(1):
        candidate = i * 2 + 1
        if not all(candidate % prime for prime in primes_found):
            primes_found.add(candidate)
            yield candidate

然后,我们可以使用它来生成后续素数的乘积:

def prime_products():
    primeiter = primes()
    prev = primeiter.next()
    for prime in primeiter:
        yield prime * prev
        prev = prime

这些是相当琐碎的示例,但是我们可以看到它在不预先生成大型数据集(可能无限!)的情况下如何有用,这只是更明显的用途之一。

简单的解释:
考虑一个for语句

for item in iterable:
   do_stuff()

很多时候," iterable"中的所有项目并不需要一开始就存在,而是可以根据需要即时生成。两者都可以更有效率

  • 空间(我们无需同时存储所有物品)和
  • 时间(迭代可能会在需要所有项目之前完成)。

有时,我们甚至都不知道所有项目。例如:

for command in user_input():
   do_stuff_with(command)

我们无法事先知道所有用户的命令,但是如果我们有生成器来处理命令,则可以使用类似这样的循环:

def user_input():
    while True:
        wait_for_command()
        cmd = get_command()
        yield cmd

使用生成器,我们还可以在无限序列上进行迭代,这在遍历容器时当然是不可能的。

当我们的Web服务器充当代理时,我使用生成器:

  • 客户端从服务器请求代理的URL
  • 服务器开始加载目标网址
  • 服务器屈服于将结果尽快返回给客户端

我发现生成器在清理代码以及为我们提供封装和模块化代码的独特方法方面非常有帮助。在我们需要某种东西根据其自身的内部处理不断吐出值的情况下,并且需要从代码中的任何位置(例如,不仅在循环或者块内)调用某些东西时,生成器都是使用。

一个抽象的例子是斐波那契数字生成器,它不存在于循环中,当从任何地方调用它时,它将始终返回序列中的下一个数字:

def fib():
    first = 0
    second = 1
    yield first
    yield second

    while 1:
        next = first + second
        yield next
        first = second
        second = next

fibgen1 = fib()
fibgen2 = fib()

现在,我们有两个斐波那契数字生成器对象,可以在代码中的任何位置调用它们,它们将始终按以下顺序依次返回更大的斐波那契数字:

>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5

生成器的妙处在于它们封装状态而无需经历创建对象的麻烦。考虑它们的一种方法是记住它们内部状态的"功能"。

我从Python Generators获得了Fibonacci示例,它们是什么?稍加想象,我们就会想到很多其他情况,其中生成器为for循环和其他传统迭代构造提供了绝佳的替代方案。