生成器函数可以用来做什么?
- 2025-01-20 09:07:00
- admin 原创
- 119
问题描述:
我开始学习 Python,并且遇到了生成器函数,其中有一个yield语句。我想知道这些函数真正擅长解决哪些类型的问题。
解决方案 1:
生成器为您提供惰性求值。您可以通过迭代它们来使用它们,可以显式地使用“for”,也可以隐式地将其传递给任何迭代的函数或构造。您可以将生成器视为返回多个项目,就好像它们返回一个列表一样,但它们不是一次返回所有项目,而是逐个返回,并且生成器函数会暂停,直到请求下一个项目。
生成器非常适合计算大量结果(尤其是涉及循环本身的计算),在这种情况下,您不知道是否需要所有结果,或者您不想同时为所有结果分配内存。或者,生成器使用另一个生成器或消耗其他资源的情况,如果这种情况尽可能晚地发生会更方便。
生成器的另一个用途(实际上是一样的)是用迭代代替回调。在某些情况下,您希望一个函数执行大量工作并偶尔向调用者报告。传统上,您会为此使用回调函数。您将此回调传递给工作函数,它会定期调用此回调。生成器的方法是,工作函数(现在是生成器)对回调一无所知,并且仅在需要报告某些内容时产生结果。调用者无需编写单独的回调并将其传递给工作函数,而是在生成器周围的一个小“for”循环中完成所有报告工作。
例如,假设您编写了一个“文件系统搜索”程序。您可以执行整个搜索,收集结果,然后一次显示一个结果。在显示第一个结果之前,必须收集所有结果,并且所有结果将同时在内存中。或者您可以在找到结果时显示它们,这将更节省内存,对用户也更友好。后者可以通过将结果打印函数传递给文件系统搜索函数来完成,或者只需将搜索函数设为生成器并迭代结果即可。
如果您想查看后两种方法的示例,请参阅 os.path.walk()(带回调的旧文件系统遍历函数)和 os.walk()(新的文件系统遍历生成器)。当然,如果您真的想将所有结果收集到一个列表中,则生成器方法很容易转换为大列表方法:
big_list = list(the_generator)
解决方案 2:
使用生成器的原因之一是为了让某些类型的解决方案更加清晰。
另一种方法是一次处理一个结果,避免构建庞大的结果列表(无论如何您都需要分开处理)。
如果你有一个像这样的斐波那契数列函数:
# function version
def fibon(n):
a = b = 1
result = []
for i in range(n):
result.append(a)
a, b = b, a + b
return result
您可以更轻松地编写如下函数:
# generator version
def fibon(n):
a = b = 1
for i in range(n):
yield a
a, b = b, a + b
该函数更加清晰。如果你像这样使用该函数:
for x in fibon(1_000_000):
print(x),
在此示例中,如果使用生成器版本,则根本不会创建整个 1,000,000 个项目的列表,而只会一次创建一个值。使用列表版本时则不会出现这种情况,因为会先创建一个列表。
解决方案 3:
真实世界的例子
假设您的 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,整个函数就会结束。
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()
解决方案 4:
我发现这个解释消除了我的疑虑。因为有可能不知道的人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() 时,生成器都会从上次中断的地方恢复(它会记住所有数据值以及最后执行了哪条语句)。
解决方案 5:
请参阅PEP 255中的“动机”部分。
生成器的一个不太明显的用途是创建可中断函数,它允许您执行诸如更新 UI 或“同时”(实际上是交错)运行多个作业等操作,而无需使用线程。
解决方案 6:
缓冲。当以大块的形式获取数据效率高,但以小块的形式处理数据时,生成器可能会有所帮助:
def bufferedFetch():
while True:
buffer = getBigChunkOfData()
# insert some code to break on 'end of data'
for i in buffer:
yield i
上面的代码让你可以轻松地将缓冲与处理分开。消费者函数现在可以逐个获取值,而不必担心缓冲。
解决方案 7:
我发现生成器在清理代码方面非常有用,它为您提供了一种非常独特的封装和模块化代码的方法。在您需要某个东西根据其自身的内部处理不断输出值,并且需要从代码中的任何地方调用该东西(而不仅仅是在循环或块中)的情况下,生成器是可以使用的功能。
一个抽象的例子是斐波那契数生成器,它不存在于循环中,并且当从任何地方调用它时,它将始终返回序列中的下一个数字:
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 生成器 - 它们是什么?中得到了斐波那契的例子,只要稍加想象,你就可以想出很多其他的情况,在这些情况下,生成器可以成为for
循环和其他传统迭代结构的绝佳替代品。
解决方案 8:
简单的解释:考虑一个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
使用生成器,您还可以对无限序列进行迭代,这在对容器进行迭代时当然是不可能的。
解决方案 9:
我最喜欢的用途是“过滤”和“减少”操作。
假设我们正在读取一个文件,并且只想要以“##”开头的行。
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()
这个想法是,生成器函数允许我们过滤或减少一个序列,一次一个值地产生另一个序列。
解决方案 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
。
解决方案 11:
在迭代输入维持状态时基本上避免回调函数。
请参阅此处和此处了解使用生成器可以做什么的概述。
解决方案 12:
由于尚未提及生成器的 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 上讨论生成器
解决方案 13:
这里有一些很好的答案,但是,我还建议完整阅读 Python函数式编程教程,它有助于解释生成器的一些更有效的用例。
特别有趣的是,现在可以从生成器函数外部更新yield变量,从而可以以相对较少的努力创建动态和交织的协程。
另请参阅PEP 342:通过增强生成器实现协程以获取更多信息。
解决方案 14:
当我们的 Web 服务器充当代理时,我使用生成器:
客户端向服务器请求代理 URL
服务器开始加载目标url
服务器在获得结果后立即返回给客户端
解决方案 15:
一堆东西。任何时候你想生成一个项目序列,但又不想一次性将它们全部“具体化”到一个列表中。例如,你可以有一个返回素数的简单生成器:
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
这些都是相当简单的例子,但您可以看到它如何用于处理大型(可能是无限的!)数据集而无需提前生成它们,这只是更明显的用途之一。
解决方案 16:
还适用于打印最多 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)
扫码咨询,免费领取项目管理大礼包!