我开始学习 Python 并且遇到了生成器函数,它们中有一个 yield 语句。我想知道这些函数真正擅长解决哪些类型的问题。
16 回答
生成器为您提供惰性评估。您可以通过迭代它们来使用它们,可以使用 'for' 显式地使用它们,也可以通过将其传递给任何迭代的函数或构造来隐式地使用它们。您可以将生成器视为返回多个项目,就好像它们返回一个列表一样,但它们不是一次全部返回它们,而是一个接一个地返回它们,并且生成器函数暂停直到请求下一个项目。
生成器适用于计算大量结果(特别是涉及循环本身的计算),您不知道是否需要所有结果,或者您不想同时为所有结果分配内存. 或者对于生成器使用另一个生成器或消耗其他资源的情况,如果这种情况发生得越晚越好。
生成器的另一个用途(实际上是一样的)是用迭代替换回调。在某些情况下,您希望函数完成大量工作并偶尔向调用者报告。传统上,您会为此使用回调函数。您将此回调传递给工作函数,它会定期调用此回调。生成器方法是工作函数(现在是生成器)对回调一无所知,并且只在它想要报告某些内容时产生。调用者不是编写单独的回调并将其传递给工作函数,而是在生成器周围的一个小“for”循环中完成所有报告工作。
例如,假设您编写了一个“文件系统搜索”程序。您可以完整地执行搜索,收集结果,然后一次显示一个。在显示第一个结果之前,必须收集所有结果,并且所有结果都将同时存储在内存中。或者您可以在找到结果时显示结果,这样会更节省内存并且对用户更友好。后者可以通过将结果打印函数传递给文件系统搜索函数来完成,或者可以通过将搜索函数作为生成器并迭代结果来完成。
如果您想查看后两种方法的示例,请参阅 os.path.walk()(带有回调的旧文件系统遍历函数)和 os.walk()(新的文件系统遍历生成器)。当然,如果你真的想在一个列表中收集所有结果,生成器方法很容易转换为大列表方法:
big_list = list(the_generator)
使用生成器的原因之一是使某种解决方案的解决方案更清晰。
另一种是一次处理一个结果,避免构建庞大的结果列表,无论如何都要分开处理。
如果您有这样的斐波那契向上到 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 项列表,一次只创建一个值。使用列表版本时不会出现这种情况,首先会创建一个列表。
我发现这个解释消除了我的疑问。因为有可能不知道的人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() 时,生成器都会从中断处继续(它会记住所有数据值以及最后执行的语句)。
请参阅PEP 255中的“动机”部分。
生成器的一个不明显的用途是创建可中断的函数,它可以让您在不使用线程的情况下执行更新 UI 或“同时”(实际上是交错)运行多个作业等操作。
现实世界的例子
假设您的 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()
缓冲。如果以大块的形式获取数据是有效的,但以小块的形式处理它,那么生成器可能会有所帮助:
def bufferedFetch():
while True:
buffer = getBigChunkOfData()
# insert some code to break on 'end of data'
for i in buffer:
yield i
以上使您可以轻松地将缓冲与处理分开。消费者函数现在可以一一获取值,而无需担心缓冲。
我发现生成器在清理代码方面非常有帮助,它为您提供了一种非常独特的方式来封装和模块化代码。在您需要根据自己的内部处理不断吐出值的情况下,并且当需要从代码中的任何位置(而不仅仅是在循环或块中)调用该东西时,生成器是一个功能采用。
一个抽象的例子是一个不在循环中的斐波那契数生成器,当从任何地方调用它时,总是会返回序列中的下一个数:
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
循环和其他传统迭代结构的绝佳替代品。
简单的解释:考虑一个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
使用生成器,您还可以对无限序列进行迭代,这在迭代容器时当然是不可能的。
我最喜欢的用途是“过滤”和“减少”操作。
假设我们正在读取一个文件,并且只想要以“##”开头的行。
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()
这个想法是生成器函数允许我们过滤或减少序列,一次生成另一个序列一个值。
您可以使用生成器的一个实际示例是,如果您有某种形状并且想要迭代它的角、边缘或其他任何东西。对于我自己的项目(这里的源代码),我有一个矩形:
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
表达式中使用类实例名称。
由于没有提到生成器的发送方法,这里举个例子:
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
来自解释、用于并行处理的生成器、逃避递归限制等)
这里有一些很好的答案,但是,我还建议完整阅读 Python函数式编程教程,该教程有助于解释生成器的一些更有效的用例。
- Particularly interesting is that it is now possible to update the yield variable from outside the generator function, hence making it possible to create dynamic and interwoven coroutines with relatively little effort.
- Also see PEP 342: Coroutines via Enhanced Generators for more information.
当我们的 Web 服务器充当代理时,我使用生成器:
- 客户端向服务器请求代理 url
- 服务器开始加载目标url
- 服务器在得到结果后立即将结果返回给客户端
一堆东西。任何时候您想生成一系列项目,但又不想一次将它们全部“物化”到一个列表中。例如,您可以有一个返回素数的简单生成器:
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
这些都是相当琐碎的例子,但您可以看到它在处理大型(可能是无限的!)数据集时如何有用,而无需提前生成它们,这只是更明显的用途之一。
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)