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


Answers:


239

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

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

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

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

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

big_list = list(the_generator)

诸如生成文件系统列表的生成器之类的生成器是否执行与在循环中运行该生成器的代码并行的动作?理想情况下,计算机将运行循环的主体(处理最后的结果),同时执行生成器为获得下一个值而必须执行的所有操作。
史蒂文·卢

@StevenLu:除非它去的麻烦手动启动线程之前yieldjoin他们之后拿到下一个结果,它不会并行执行(没有标准库生成做到这一点,偷偷启动线程是令人难以接受的)。生成器在每个处暂停,yield直到请求下一个值。如果生成器包装了I / O,则OS可能会在不久之后要求主动从文件中缓存数据,但这就是OS,不涉及Python。
ShadowRanger

90

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

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

如果您有一个像这样的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项列表,一次只能创建一个值。使用列表版本时,情况并非如此,先创建列表。


18
如果您需要列表,则可以随时执行list(fibon(5))
endolith

41

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

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


1
动机部分很好,因为它有一个特定的示例:“当生产者函数完成一项艰巨的工作以至于需要维持所生成的值之间的状态时,大多数编程语言都无法提供一种愉快而有效的解决方案,只能在生产者的参数中添加回调函数列表...例如,标准库中的tokenize.py采用这种方法”
Ben Creasy

38

我发现这种解释消除了我的怀疑。因为有可能不认识的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()

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

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

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


33

真实的例子

假设您的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()

如果可以用递归/动态编程来解释收益的话,那会更加实际!
igaurav 2014年

27

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

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

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


3
如果getBigChuckOfData不懒惰,那么我不明白这里的收益率是多少。此功能的用例是什么?
肖恩·杰弗里·皮茨

1
但是关键是,IIUC,bufferedFetch正在延迟getBigChunkOfData的调用。如果getBigChunkOfData已经是惰性的,则bufferedFetch将无用。即使已读取BigChunk,每次对bufferedFetch()的调用都将返回一个缓冲元素。您无需显式地保留要返回的下一个元素的计数,因为yield的机制只是隐式地执行此操作。
hmijail哀悼辞职者,

21

我发现生成器在清理代码以及为您提供封装和模块化代码的独特方法方面非常有帮助。在你需要的东西,不停地吐了价值基于其自身的内部处理和情况时,从任何地方在你的代码调用的东西需要(而不仅仅是内部的循环或例如块),发电机功能,用。

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

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循环和其他传统迭代构造提供了绝佳的替代方案。


19

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

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


...无限序列可能是通过反复循环遍历一个小列表而生成的,到达终点后又返回到起点。我用它来选择图形中的颜色,或在文本中产生忙碌的rob动或微调。
Andrej Panjkov 2012年

@mataap:有一个itertool功能-请参阅cycles
martineau 2014年

12

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

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

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()

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


8
fileobj.readlines()会将整个文件读取到内存中的列表中,从而破坏了使用生成器的目的。由于文件对象已经是可迭代的,因此可以for b in your_generator(fileobject):代替使用。这样,您的文件将一次只能读取一行,从而避免读取整个文件。
nosklo

reduceLocation非常奇怪地产生一个列表,为什么不只产生每一行呢?此外,filter和reduce是具有预期行为的内置程序(请参阅ipython等中的帮助),您对reduce的用法与filter相同。
詹姆斯·安迪尔

关于readlines()的要点。我通常意识到文件在单元测试期间是一流的行迭代器。
S.Lott

实际上,“归约”是将多条单独的线组合成一个合成对象。好的,这是一个列表,但是仍然从源头上减少了。
S.Lott

9

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

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表达式中使用类实例名称。


我很喜欢通过类似的类字段作为生成器的想法
eusoubrasileiro

7

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

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



3

由于未提及生成器的send方法,因此下面是一个示例:

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上的发电机


2

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

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

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

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


如果没有的话(primes_found中的素数的候选素数)如果所有(primes_found中的素数的候选素数)
rjmunro

是的,我的意思是写“如果没有的话(primes_found中的素数的候选%素数== 0)。不过,您的稍显整洁。:)
Nick Johnson

我猜您忘了如果不是全部则从中删除“ not”(primes_found中素数的候选素数)
Thava

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.