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

What can you use Python generator functions for?

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


生成器会给你懒惰的评价。您可以通过迭代它们来使用它们,或者显式地使用"for",或者通过将其传递给任何迭代的函数或构造来隐式地使用它们。您可以将生成器视为返回多个项,就好像它们返回了一个列表,但是它们不是一次返回所有项,而是一个接一个地返回它们,并且生成器函数将暂停,直到请求下一个项为止。

生成器很适合计算大型结果集(尤其是涉及循环本身的计算),在这些结果集中,您不知道是否需要所有结果,或者不希望同时为所有结果分配内存。或者对于生成器使用另一个生成器或消耗其他资源的情况,如果发生得越晚越好。

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

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

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

1
big_list = list(the_generator)


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

另一种方法是一次处理一个结果,避免构建一个庞大的结果列表,不管怎样,这些结果都会被分开处理。

如果你有这样的斐波那契函数:

1
2
3
4
5
6
7
8
# function version
def fibon(n):
    a = b = 1
    result = []
    for i in xrange(n):
        result.append(a)
        a, b = b, a + b
    return result

您可以更容易地编写如下函数:

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

功能更清晰。如果你使用这样的函数:

1
2
for x in fibon(1000000):
    print x,

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


参见PEP 255中的"激励"部分。

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


我发现这个解释可以澄清我的怀疑。因为不了解Generators的人也可能不了解yield

返回

RETURN语句销毁所有局部变量,并将结果值返回(返回)给调用方。如果以后某个时候调用同一个函数,该函数将得到一组新的变量。

但是如果在我们退出函数时局部变量没有被丢弃呢?这意味着我们可以在我们停止的地方继续进行。这就是引入Generators概念的地方,yield语句在function停止的地方恢复。

1
2
3
  def generate_integers(N):
    for i in xrange(N):
    yield i
1
2
3
4
5
6
7
8
    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()

这就是python中returnyield语句之间的区别。

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

因此,生成器是创建迭代器的简单而强大的工具。它们的编写方式与常规函数类似,但只要它们想返回数据,就使用yield语句。每次调用next()时,生成器都会从停止的位置恢复(它会记住所有数据值以及上次执行的语句)。


现实世界的例子

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

首先,您需要从数据库中选择域名。

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

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

所以你决定批量运行这个程序。我们的批量是1000。

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

在第二批中,我们将处理接下来的1000行。第三批是2001年到3000年,以此类推。

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

以下是我们的发电机功能:

1
2
3
4
5
6
7
def ResultGenerator(cursor, batchsize=1000):
    while True:
        results = cursor.fetchmany(batchsize)
        if not results:
            break
        for result in results:
            yield result

如您所见,我们的函数使yield保持结果。如果您使用关键字return而不是yield,那么一旦达到返回值,整个函数就会结束。

1
2
return - returns only once
yield - returns multiple times

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

现在可以这样迭代:

1
2
3
4
5
6
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()


缓冲。当以大数据块的形式获取数据,但以小数据块的形式处理数据时,生成器可能有助于:

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

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


我发现生成器在清理代码和提供一种非常独特的方法来封装和模块化代码方面非常有用。在这种情况下,您需要一些东西来根据其自身的内部处理不断地输出值,并且当需要从代码中的任何地方(而不仅仅是在循环或块中)调用某些东西时,生成器是要使用的特性。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()

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

1
2
3
4
5
6
7
8
9
10
11
>>> 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生成器中得到了fibonacci示例-它们是什么?再加上一点想象力,您可以想出许多其他情况,在这些情况下,生成器可以成为for循环和其他传统迭代构造的一个很好的替代方案。


简单解释:考虑一个for声明

1
2
for item in iterable:
   do_stuff()

很多时候,iterable中的所有项从一开始就不需要存在,但可以根据需要即时生成。这两种方法都能更有效

  • 空间(不需要同时存储所有项目)和
  • 时间(迭代可能在需要所有项之前完成)。

有时,你甚至不知道所有的项目提前。例如:

1
2
for command in user_input():
   do_stuff_with(command)

您不可能事先知道所有用户的命令,但是如果有一个生成器向您传递命令,则可以使用类似这样的好循环:

1
2
3
4
5
def user_input():
    while True:
        wait_for_command()
        cmd = get_command()
        yield cmd

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


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

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

1
2
3
4
def filter2sharps( aSequence ):
    for l in aSequence:
        if l.startswith("##"):
            yield l

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

1
2
3
4
source= file( ... )
for line in filter2sharps( source.readlines() ):
    print line
source.close()

reduce示例类似。假设我们有一个文件,我们需要在其中定位...行的块。[不是HTML标记,而是恰好看起来像标记的行。]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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循环中使用这个生成器。

1
2
3
4
source = file( ... )
for b in reduceLocation( source.readlines() ):
    print b
source.close()

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


可以使用生成器的一个实际示例是,如果您有某种形状,并且希望在它的角、边或其他地方进行迭代。对于我自己的项目(这里是源代码),我有一个矩形:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

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

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

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


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

有关如何使用生成器的概述,请参阅此处和此处。


不过,这里有一些很好的答案,我还建议您完整阅读一下Python函数编程教程,它有助于解释一些更有效的生成器使用案例。

  • 特别有趣的是,现在可以从生成器函数的外部更新yield变量,从而使创建动态的和交织的协程的工作相对较少。
  • 另请参阅PEP 342:Coroutines via enhanced generators了解更多信息。

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

  • 客户端从服务器请求代理的URL
  • 服务器开始加载目标URL
  • 服务器会在收到结果后立即返回给客户机。

  • 由于没有提到生成器的发送方法,下面是一个示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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关于2014年Pycon发电机的报道


    成堆的东西。任何时候你想生成一系列的项目,但不想一次把它们"具体化"到一个列表中。例如,您可以有一个返回质数的简单生成器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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

    然后可以使用它生成后续素数的产物:

    1
    2
    3
    4
    5
    6
    def prime_products():
        primeiter = primes()
        prev = primeiter.next()
        for prime in primeiter:
            yield prime * prev
            prev = prime

    这些都是非常简单的例子,但是您可以看到它如何对处理大型(可能是无限的)有用。没有提前生成数据集,这只是更明显的用途之一。


    也适用于打印最多n个质数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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)