225

我开始学习 Python 并且遇到了生成器函数,它们中有一个 yield 语句。我想知道这些函数真正擅长解决哪些类型的问题。

4

16 回答 16

252

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

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

生成器的另一个用途(实际上是一样的)是用迭代替换回调。在某些情况下,您希望函数完成大量工作并偶尔向调用者报告。传统上,您会为此使用回调函数。您将此回调传递给工作函数,它会定期调用此回调。生成器方法是工作函数(现在是生成器)对回调一无所知,并且只在它想要报告某些内容时产生。调用者不是编写单独的回调并将其传递给工作函数,而是在生成器周围的一个小“for”循环中完成所有报告工作。

例如,假设您编写了一个“文件系统搜索”程序。您可以完整地执行搜索,收集结果,然后一次显示一个。在显示第一个结果之前,必须收集所有结果,并且所有结果都将同时存储在内存中。或者您可以在找到结果时显示结果,这样会更节省内存并且对用户更友好。后者可以通过将结果打印函数传递给文件系统搜索函数来完成,或者可以通过将搜索函数作为生成器并迭代结果来完成。

如果您想查看后两种方法的示例,请参阅 os.path.walk()(带有回调的旧文件系统遍历函数)和 os.walk()(新的文件系统遍历生成器)。当然,如果你真的想在一个列表中收集所有结果,生成器方法很容易转换为大列表方法:

big_list = list(the_generator)
于 2008-09-19T15:09:25.340 回答
92

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

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

如果您有这样的斐波那契向上到 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 项列表,一次只创建一个值。使用列表版本时不会出现这种情况,首先会创建一个列表。

于 2008-09-19T15:09:28.520 回答
46

我发现这个解释消除了我的疑问。因为有可能不知道的人Generators也不知道yield

返回

return 语句是销毁所有局部变量并将结果值返回(返回)给调用者的地方。如果稍后调用相同的函数,该函数将获得一组新的变量。

屈服

但是,如果我们退出函数时没有丢弃局部变量怎么办?这意味着我们可以resume the function在我们离开的地方。这是generators引入的概念和yield语句function从中断处继续的地方。

  def generate_integers(N):
    for i in xrange(N):
    yield i

    In [1]: gen = generate_integers(3)
    In [2]: gen
    <generator object at 0x8117f90>
    In [3]: gen.next()
    0
    In [4]: gen.next()
    1
    In [5]: gen.next()

return这就是 Python 中和yield语句之间的区别。

Yield 语句使函数成为生成器函数。

所以生成器是创建迭代器的简单而强大的工具。它们的编写方式与常规函数类似,但它们在yield想要返回数据时使用该语句。每次调用 next() 时,生成器都会从中断处继续(它会记住所有数据值以及最后执行的语句)。

于 2013-01-18T08:17:56.467 回答
44

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

生成器的一个不明显的用途是创建可中断的函数,它可以让您在不使用线程的情况下执行更新 UI 或“同时”(实际上是交错)运行多个作业等操作。

于 2008-09-19T15:07:13.410 回答
43

现实世界的例子

假设您的 MySQL 表中有 1 亿个域,并且您想更新每个域的 Alexa 排名。

您需要做的第一件事是从数据库中选择您的域名。

假设您的表名是domains,列名是domain

如果您使用SELECT domain FROM domains它将返回 1 亿行,这将消耗大量内存。所以你的服务器可能会崩溃。

所以你决定批量运行程序。假设我们的批量大小是 1000。

在我们的第一批中,我们将查询前 1000 行,检查每个域的 Alexa 排名并更新数据库行。

在我们的第二批中,我们将处理接下来的 1000 行。在我们的第三批中,它将是从 2001 到 3000 等等。

现在我们需要一个生成器函数来生成我们的批次。

这是我们的生成器函数:

def ResultGenerator(cursor, batchsize=1000):
    while True:
        results = cursor.fetchmany(batchsize)
        if not results:
            break
        for result in results:
            yield result

正如你所看到的,我们的函数保持yield结果。如果您使用关键字return而不是yield,则整个函数将在返回时结束。

return - returns only once
yield - returns multiple times

如果一个函数使用关键字yield,那么它就是一个生成器。

现在你可以像这样迭代:

db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
    doSomethingWith(result)
db.close()
于 2014-05-07T23:20:11.100 回答
27

缓冲。如果以大块的形式获取数据是有效的,但以小块的形式处理它,那么生成器可能会有所帮助:

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

以上使您可以轻松地将缓冲与处理分开。消费者函数现在可以一一获取值,而无需担心缓冲。

于 2008-09-19T15:14:10.087 回答
22

我发现生成器在清理代码方面非常有帮助,它为您提供了一种非常独特的方式来封装和模块化代码。在您需要根据自己的内部处理不断吐出值的情况下,并且当需要从代码中的任何位置(而不仅仅是在循环或块中)调用该东西时,生成器是一个功能采用。

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

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 获得了斐波那契示例——它们是什么?稍加想象,您可以想出许多其他情况,其中生成器是for循环和其他传统迭代结构的绝佳替代品。

于 2009-04-11T20:55:59.403 回答
20

简单的解释:考虑一个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

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

于 2008-09-19T15:15:03.500 回答
12

我最喜欢的用途是“过滤”和“减少”操作。

假设我们正在读取一个文件,并且只想要以“##”开头的行。

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

然后我们可以在适当的循环中使用生成器函数

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

reduce 示例类似。假设我们有一个文件,我们需要在其中定位<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()

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

于 2008-09-19T15:13:16.640 回答
10

您可以使用生成器的一个实际示例是,如果您有某种形状并且想要迭代它的角、边缘或其他任何东西。对于我自己的项目(这里的源代码),我有一个矩形:

class Rect():

    def __init__(self, x, y, width, height):
        self.l_top  = (x, y)
        self.r_top  = (x+width, y)
        self.r_bot  = (x+width, y+height)
        self.l_bot  = (x, y+height)

    def __iter__(self):
        yield self.l_top
        yield self.r_top
        yield self.r_bot
        yield self.l_bot

现在我可以创建一个矩形并在其角上循环:

myrect=Rect(50, 50, 100, 100)
for corner in myrect:
    print(corner)

而不是__iter__你可以有一个方法iter_corners并用for corner in myrect.iter_corners(). 使用起来更加优雅,__iter__因为我们可以直接在for表达式中使用类实例名称。

于 2014-09-27T12:40:27.043 回答
8

在迭代输入维护状态时基本上避免回调函数。

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

于 2008-09-19T15:09:26.650 回答
5

由于没有提到生成器的发送方法,这里举个例子:

def test():
    for i in xrange(5):
        val = yield
        print(val)

t = test()

# Proceed to 'yield' statement
next(t)

# Send value to yield
t.send(1)
t.send('2')
t.send([3])

它显示了向正在运行的生成器发送值的可能性。下面视频中关于生成器的更高级课程(包括yield来自解释、用于并行处理的生成器、逃避递归限制等)

David Beazley 在 PyCon 2014 上谈发电机

于 2014-04-28T07:21:10.390 回答
4

这里有一些很好的答案,但是,我还建议完整阅读 Python函数式编程教程,该教程有助于解释生成器的一些更有效的用例。

于 2015-03-16T14:17:35.183 回答
2

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

  1. 客户端向服务器请求代理 url
  2. 服务器开始加载目标url
  3. 服务器在得到结果后立即将结果返回给客户端
于 2008-09-19T15:17:51.060 回答
1

一堆东​​西。任何时候您想生成一系列项目,但又不想一次将它们全部“物化”到一个列表中。例如,您可以有一个返回素数的简单生成器:

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

这些都是相当琐碎的例子,但您可以看到它在处理大型(可能是无限的!)数据集时如何有用,而无需提前生成它们,这只是更明显的用途之一。

于 2008-09-19T15:14:43.237 回答
0

Also good for printing the prime numbers up to n:

def genprime(n=10):
    for num in range(3, n+1):
        for factor in range(2, num):
            if num%factor == 0:
                break
        else:
            yield(num)

for prime_num in genprime(100):
    print(prime_num)
于 2017-09-22T14:06:40.320 回答