Fed*_*rer 194 python generator
我正在阅读Python食谱,目前我正在研究发电机.我发现很难理解我的头脑.
因为我来自Java背景,是否有Java等价物?这本书讲的是"制片人/消费者",但是当我听到我想到线程时.
什么是发电机,为什么要使用它?显然没有引用任何书籍(除非你能直接从书中找到一个体面的,简单的答案).也许有例子,如果你感觉很慷慨!
Ste*_*202 368
注意:本文假定使用Python 3.x语法.†
一个发电机仅仅是它返回一个对象,你可以调用一个函数next
,这样在每次调用它返回一定的价值,直到它提出了一个StopIteration
例外,这表明所有值已经产生.这样的对象称为迭代器.
正常函数使用返回单个值return
,就像在Java中一样.然而,在Python中,有一种替代方法,称为yield
.yield
在函数中的任何位置使用它使其成为生成器.观察此代码:
>>> def myGen(n):
... yield n
... yield n + 1
...
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Run Code Online (Sandbox Code Playgroud)
正如你所看到的,myGen(n)
是一个产生n
和的函数n + 1
.每次调用都会next
产生一个值,直到产生所有值.for
循环调用next
后台,因此:
>>> for n in myGen(6):
... print(n)
...
6
7
Run Code Online (Sandbox Code Playgroud)
同样,有生成器表达式,它提供了简洁描述某些常见类型的生成器的方法:
>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Run Code Online (Sandbox Code Playgroud)
请注意,生成器表达式与列表推导非常相似:
>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]
Run Code Online (Sandbox Code Playgroud)
观察生成器对象生成一次,但其代码不会一次全部运行.只调用next
实际执行(部分)代码.一旦yield
达到语句,生成器中代码的执行就会停止,然后返回一个值.然后,下一次调用next
会导致执行继续处于最后一个生成器之后的状态yield
.这与常规函数有根本区别:它们总是在"顶部"开始执行,并在返回值时丢弃它们的状态.
关于这个问题还有更多的话要说.例如,可以将send
数据返回到发电机(参考)中.但是,在你理解发电机的基本概念之前,我建议你不要研究这个问题.
现在您可能会问:为什么要使用发电机?有几个很好的理由:
生成器允许以自然的方式描述无限流.考虑例如Fibonacci数:
>>> def fib():
... a, b = 0, 1
... while True:
... yield a
... a, b = b, a + b
...
>>> import itertools
>>> list(itertools.islice(fib(), 10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Run Code Online (Sandbox Code Playgroud)
此代码用于itertools.islice
从无限流中获取有限数量的元素.建议您仔细查看itertools
模块中的功能,因为它们是轻松编写高级发生器的基本工具.
† 关于Python <= 2.6:在上面的例子中next
是一个调用__next__
给定对象上的方法的函数.在Python <= 2.6中,使用稍微不同的技术,即o.next()
代替next(o)
.Python 2.7有next()
调用,.next
所以你不需要在2.7中使用以下内容:
>>> g = (n for n in range(3, 5))
>>> g.next()
3
Run Code Online (Sandbox Code Playgroud)
Cal*_*ngh 47
生成器实际上是一个在完成之前返回(数据)的函数,但它在此时暂停,您可以在该点恢复该函数.
>>> def myGenerator():
... yield 'These'
... yield 'words'
... yield 'come'
... yield 'one'
... yield 'at'
... yield 'a'
... yield 'time'
>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words
Run Code Online (Sandbox Code Playgroud)
等等.生成器的(或一个)好处是,因为它们一次处理一个数据,所以可以处理大量数据; 对于列表,过多的内存要求可能会成为一个问题.生成器,就像列表一样,是可迭代的,因此它们可以以相同的方式使用:
>>> for word in myGeneratorInstance:
... print word
These
words
come
one
at
a
time
Run Code Online (Sandbox Code Playgroud)
请注意,例如,生成器提供了另一种处理无穷大的方法
>>> from time import gmtime, strftime
>>> def myGen():
... while True:
... yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000
Run Code Online (Sandbox Code Playgroud)
生成器封装了一个无限循环,但这不是问题,因为每次你要求它时你只得到每个答案.
nik*_*kow 26
首先,术语生成器最初在Python中有些不明确,导致很多混乱.你可能意味着迭代器和迭代器(见这里).然后在Python中还有生成器函数(返回生成器对象),生成器对象(它们是迭代器)和生成器表达式(它们被计算到生成器对象).
根据发电机的词汇表条目,似乎官方术语现在是发电机是"发电机功能"的缩写.在过去,文档不一致地定义了这些术语,但幸运的是,这已得到修复.
确切地说,在没有进一步说明的情况下避免使用术语"发电机"可能仍然是一个好主意.
ove*_*ink 22
生成器可以被认为是创建迭代器的简写.它们的行为类似于Java Iterator.例:
>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g) # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next() # iterator is at the end; calling next again will throw
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Run Code Online (Sandbox Code Playgroud)
希望这有助于/正是您所寻找的.
更新:
正如许多其他答案所示,创建生成器的方法有很多种.您可以使用上面示例中的括号语法,也可以使用yield.另一个有趣的特性是生成器可以是"无限的" - 不停止的迭代器:
>>> def infinite_gen():
... n = 0
... while True:
... yield n
... n = n + 1
...
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...
Run Code Online (Sandbox Code Playgroud)
Wer*_*sey 11
没有Java等价物.
这是一个人为的例子:
#! /usr/bin/python
def mygen(n):
x = 0
while x < n:
x = x + 1
if x % 3 == 0:
yield x
for a in mygen(100):
print a
Run Code Online (Sandbox Code Playgroud)
生成器中有一个循环,从0到n,如果循环变量是3的倍数,则生成变量.
在for
循环的每次迭代期间,执行生成器.如果它是第一次执行发生器,它将从头开始,否则它将从上一次产生的时间开始继续.
在堆栈框架方面,我喜欢向那些在编程语言和计算方面具有良好背景的人描述生成器.
在许多语言中,堆栈顶部是当前堆栈"帧".堆栈帧包括为函数本地变量分配的空间,包括传递给该函数的参数.
调用函数时,将当前执行点("程序计数器"或等效函数)压入堆栈,并创建新的堆栈帧.然后执行转移到被调用函数的开头.
使用常规函数,在某些时候函数返回一个值,并且"弹出"堆栈.函数的堆栈帧被丢弃,执行将在之前的位置恢复.
当一个函数是一个生成器时,它可以使用yield语句返回一个没有丢弃堆栈帧的值.保留局部变量的值和函数内的程序计数器.这允许生成器稍后恢复,从yield语句继续执行,并且它可以执行更多代码并返回另一个值.
在Python 2.5之前,这是所有生成器都做到的.Python 2.5的加入将值传递回的能力在到发电机为好.在这样做时,传入的值可用作由yield语句得到的表达式,该语句暂时从生成器返回控制(和值).
生成器的关键优势在于保留了函数的"状态",与常规函数不同,每次丢弃堆栈帧时,都会丢失所有"状态".第二个优点是避免了一些函数调用开销(创建和删除堆栈帧),尽管这通常是一个小优势.
它有助于明确区分函数foo和生成器foo(n):
def foo(n):
yield n
yield n+1
Run Code Online (Sandbox Code Playgroud)
foo是一个功能.foo(6)是一个生成器对象.
使用生成器对象的典型方法是循环:
for n in foo(6):
print(n)
Run Code Online (Sandbox Code Playgroud)
循环打印
# 6
# 7
Run Code Online (Sandbox Code Playgroud)
将发电机视为可恢复功能.
yield
表现得像return
生成的值被生成器"返回"的意义.然而,与return不同,下次生成器被要求输入一个值时,生成器的函数foo将从中断处继续 - 在最后一个yield语句之后 - 继续运行直到它到达另一个yield语句.
在幕后,当你调用bar=foo(6)
生成器对象栏时,为你定义了一个next
属性.
你可以自己调用它来检索foo产生的值:
next(bar) # Works in Python 2.6 or Python 3.x
bar.next() # Works in Python 2.5+, but is deprecated. Use next() if possible.
Run Code Online (Sandbox Code Playgroud)
当foo结束(并且没有更多的生成值)时,调用next(bar)
抛出StopInteration错误.
我唯一可以添加到Stephan202的答案是建议您看看David Beazley的PyCon '08演示文稿"系统程序员的生成器技巧",这是我见过的生成器的方法和原因的最佳单一解释任何地方.这让我从"Python看起来很有趣"到"这就是我一直在寻找的东西".它位于http://www.dabeaz.com/generators/.
macOS Big Sur 11.1
MacBook Pro (13-inch, M1, 2020)
Chip Apple M1
Memory 8gb
Run Code Online (Sandbox Code Playgroud)
macOS Big Sur 11.1
MacBook Pro (13-inch, M1, 2020)
Chip Apple M1
Memory 8gb
Run Code Online (Sandbox Code Playgroud)
输出:
import random
import psutil # pip install psutil
import os
from datetime import datetime
def memory_usage_psutil():
# return the memory usage in MB
process = psutil.Process(os.getpid())
mem = process.memory_info().rss / float(2 ** 20)
return '{:.2f} MB'.format(mem)
names = ['John', 'Milovan', 'Adam', 'Steve', 'Rick', 'Thomas']
majors = ['Math', 'Engineering', 'CompSci', 'Arts', 'Business']
print('Memory (Before): {}'.format(memory_usage_psutil()))
def people_list(num_people):
result = []
for i in range(num_people):
person = {
'id': i,
'name': random.choice(names),
'major': random.choice(majors)
}
result.append(person)
return result
t1 = datetime.now()
people = people_list(1000000)
t2 = datetime.now()
print('Memory (After) : {}'.format(memory_usage_psutil()))
print('Took {} Seconds'.format(t2 - t1))
Run Code Online (Sandbox Code Playgroud)
1 million results
。50.38 megabytes
是在我创建该列表之后的内存使用量1 million records
,因此您可以在这里看到它几乎增加了1140.41 megabytes
,并且花费了1,1 seconds
。Memory (Before): 50.38 MB
Memory (After) : 1140.41 MB
Took 0:00:01.056423 Seconds
Run Code Online (Sandbox Code Playgroud)
输出:
import random
import psutil # pip install psutil
import os
from datetime import datetime
def memory_usage_psutil():
# return the memory usage in MB
process = psutil.Process(os.getpid())
mem = process.memory_info().rss / float(2 ** 20)
return '{:.2f} MB'.format(mem)
names = ['John', 'Milovan', 'Adam', 'Steve', 'Rick', 'Thomas']
majors = ['Math', 'Engineering', 'CompSci', 'Arts', 'Business']
print('Memory (Before): {}'.format(memory_usage_psutil()))
def people_generator(num_people):
for i in range(num_people):
person = {
'id': i,
'name': random.choice(names),
'major': random.choice(majors)
}
yield person
t1 = datetime.now()
people = people_generator(1000000)
t2 = datetime.now()
print('Memory (After) : {}'.format(memory_usage_psutil()))
print('Took {} Seconds'.format(t2 - t1))
Run Code Online (Sandbox Code Playgroud)
在我运行这个之后,the memory is almost exactly the same
那是因为生成器实际上还没有做任何事情,它还没有在内存中保存这百万个值,它正在等待我获取下一个值。
基本上是didn't take any time
因为一旦到达第一个yield 语句它就会停止。
我认为它是生成器更具可读性,并且它还为您提供了big performance boosts not only with execution time but with memory
.
同样,您仍然可以在此处使用所有推导式和此生成器表达式,这样您就不会丢失该区域中的任何内容。这些是您使用生成器以及一些the advantages that come along with that
.
这篇文章将使用斐波那契数作为工具来解释Python生成器的有用性。
这篇文章将同时介绍C ++和Python代码。
斐波那契数定义为以下顺序:0、1、1、2、3、5、8、13、21、34,...。
或一般来说:
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2
Run Code Online (Sandbox Code Playgroud)
这可以非常容易地转移到C ++函数中:
size_t Fib(size_t n)
{
//Fib(0) = 0
if(n == 0)
return 0;
//Fib(1) = 1
if(n == 1)
return 1;
//Fib(N) = Fib(N-2) + Fib(N-1)
return Fib(n-2) + Fib(n-1);
}
Run Code Online (Sandbox Code Playgroud)
但是,如果要打印前六个斐波那契数字,则将使用上述函数重新计算很多值。
例如:Fib(3) = Fib(2) + Fib(1)
,Fib(2)
还会重新计算Fib(1)
。您要计算的值越高,您的收益就越差。
因此,可能会想通过跟踪中的状态来重写上面的内容main
。
// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
int result = pp + p;
pp = p;
p = result;
return result;
}
int main(int argc, char *argv[])
{
size_t pp = 0;
size_t p = 1;
std::cout << "0 " << "1 ";
for(size_t i = 0; i <= 4; ++i)
{
size_t fibI = GetNextFib(pp, p);
std::cout << fibI << " ";
}
return 0;
}
Run Code Online (Sandbox Code Playgroud)
但这很丑陋,并且使中的逻辑复杂化main
。最好不必担心我们main
功能的状态。
我们可以返回一个vector
值a ,并使用一个a iterator
来迭代该组值,但是对于大量的返回值,这一次需要大量的内存。
回到我们以前的方法,如果我们除了打印数字还想做其他事情,会发生什么?我们必须复制并粘贴整个代码块,main
然后将输出语句更改为我们想要做的其他事情。而且,如果您复制并粘贴代码,则应该被枪杀。你不想被枪杀吗?
为了解决这些问题并避免被枪杀,我们可以使用回调函数重写此代码块。每次遇到新的斐波那契数字时,我们都会调用回调函数。
void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
if(max-- == 0) return;
FoundNewFibCallback(0);
if(max-- == 0) return;
FoundNewFibCallback(1);
size_t pp = 0;
size_t p = 1;
for(;;)
{
if(max-- == 0) return;
int result = pp + p;
pp = p;
p = result;
FoundNewFibCallback(result);
}
}
void foundNewFib(size_t fibI)
{
std::cout << fibI << " ";
}
int main(int argc, char *argv[])
{
GetFibNumbers(6, foundNewFib);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
显然,这是一种改进,您的输入逻辑main
并不那么混乱,您可以使用斐波那契数字进行任何操作,只需定义新的回调即可。
但这仍然不是完美的。如果您只想获得前两个斐波那契数,然后做某事,然后再获取更多,然后再做其他事情,该怎么办?
好吧,我们可以像main
往常一样继续,我们可以再次将状态添加到中,从而允许GetFibNumbers从任意点开始。但这将使我们的代码更加膨胀,对于像打印斐波那契数字这样的简单任务而言,它看起来已经太大了。
我们可以通过几个线程来实现生产者和消费者模型。但这使代码更加复杂。
相反,让我们谈论发电机。
Python具有很好的语言功能,可以解决诸如此类的生成器之类的问题。
生成器允许您执行功能,在任意点处停止,然后从上次中断的地方继续执行。每次返回一个值。
考虑以下使用生成器的代码:
def fib():
pp, p = 0, 1
while 1:
yield pp
pp, p = p, pp+p
g = fib()
for i in range(6):
g.next()
Run Code Online (Sandbox Code Playgroud)
这给了我们结果:
0 1 1 2 3 5
该yield
语句与Python生成器结合使用。它保存函数的状态并返回yeilded值。下次您在生成器上调用next()函数时,它将在中断收益率的地方继续。
到目前为止,这比回调函数代码更干净。我们拥有更干净的代码,更小的代码,更不用说更多的功能代码(Python允许任意大的整数)。
我放置了这段代码,它解释了有关生成器的 3 个关键概念:
def numbers():
for i in range(10):
yield i
gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers
for i in gen: #we iterate over the generator and the values are printed
print(i)
#the generator is now empty
for i in gen: #so this for block does not print anything
print(i)
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
118836 次 |
最近记录: |