一、什么是斐波那契数列
数学中有个著名的斐波那契数列(Fibonacci sequence),又称黄金分割数列 ,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称“兔子数列”,其数值为:1、1、2、3、5、8、13、21、34……
在数学上,这一数列以如下递推的方法定义:
F(0)=1,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)。
简单概括,斐波那契数列的第n项等于第(n-1)和第(n-2)项,即前两项和,其中n>2
要求:数列中第一个数为1,第二个数为1,其后的每一个数都可由前两个数相加得到:
例子:1, 1, 2, 3, 5, 8, 13, 21, 34, ...
二、关于Python生成器
1.什么是生成器
在Python中,一边循环一边计算的机制,称为生成器:generator。
生成器是一种特殊的迭代器,它允许你按需生成值,而不是一次性生成所有值。这使得生成器非常适合处理大数据集或无限序列。
2.为什么要使用生成器
列表所有数据都在内存中,如果有海量数据的话将会非常耗内存。
例如通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
如果列表元素按照某种算法推算出来,那我们就可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的list,从而节省大量的空间。
生成器使用了yield语句,将返回值给调用者,而不是通过return语句。它允许函数在每次调用时产生一个值,并在下一次调用时从上次停止的地方继续执行。这样的机制避免了一次性加载所有数据到内存中,从而提高了效率。
例子:
通过列表推导式,生成器推导式创建生成器,比较使用原来的列表方式生成数据集和使用生成器生成数据集所需要的运行时间
import memory_profiler as mem
# nums = [1, 2, 3, 4, 5]
# print([i*i for i in nums])
nums = list(range(10000000))
print('运算前内存:', mem.memory_usage())
# 列表
# square_nums = [n * n for n in nums]
# 生成器
square_nums = (n * n for n in nums)
print('运算后内存:', mem.memory_usage())
如下是在Pycharm中运行的上述代码,可见利用生成器生成1000万的列表数据使用的时间为0.0,可以忽略不记
总运行时间比较:
程序运行的总时间为:0.5286157131195068
程序运行的总时间为:0.0
总结成一句话:如果想得到庞大的数据集又想让它占用和使用的计算机资源少,就使用生成器
三、Python生成器的创建方式
1. 生成器推导式
如上述例子中的代码,我们知道列表推导式是如下这样的:
square_nums = [i * i for i in range(10000000)]
固然,同为推导式,生成器推导式类似于列表推导式,区别在于列表推导式的两端是[ ]中括号,而生成器推导式则是( ),和元组所使用的括号一致:
square_nums = (i * i for i in range(10000000))
上面是一个示范,接下来介绍生成器推导式需要注意的地方及底层原理:
# 创建生成器
my_generator = (i * 2 for i in range(5))
print(my_generator)
# next获取生成器下一个值
# value = next(my_generator)
# print(value)
# 遍历生成器
for value in my_generator:
print(value)
上面代码可以看出,其实生成器推导式与列表推导式的功能实现方式完全一样,唯一的区别就是只不过生成器推导式使用小括号。
上面代码中利用生成器推导式创建生成器的部分,我们需要注意的是,它与列表推导式不同,如果是列表推导式,那么使用print打印变量名时打印的是列表;
而对于生成器推导式而言,使用print语句打印变量名将打印出他为一个生成器(generator)对象,而实际生成的不是像列表推导式一样的列表迭代器,而是一种规则。
<generator object <genexpr> at 0x000001FA68FF8110>
那么这种规则是怎样的执行原理呢?我们该怎样去获取生成器生成的数据呢?
我将用以下来讲解生成器的执行原理以及如何获取生成器中的数据:
① 执行原理
在生成器中有一个特别重要的函数next(),每调用一次next()则自动从生成器中根据生成规则获取一个元素。(所以从这里我们也可以得知因为生成器不需要储存大量的数据,只是在调用next()时,才生成一个元素,所以才使得能耗大大降低)
my_generator = (i * 2 for i in range(5))
value = next(my_generator)
print(value)
上面代码即是使用next()函数获取该生成器中的一个元素,其运行结果打印的是0
那么如何遍历整个生成器中的数据呢?
②获取数据
有两种方法:
第一种:多次使用next()函数获取生成器中的数据
my_generator = (i * 2 for i in range(5))
print(next(my_generator)) #第一次
print(next(my_generator)) #第二次
print(next(my_generator)) #第三次
print(next(my_generator)) #第四次
print(next(my_generator)) #第五次
上面代码将获取生成器中的所有数据,但需要注意的是使用次数应小于等于数据中的个数,如果我们在上述代码中再次调用next()函数,Python解释器将会报错,抛出StopIteration异常。
第二种:使用for循环遍历生成器对象,生成器是一个特殊的迭代器,所以我们能够用一般迭代器的迭代方法迭代生成器。
my_generator = (i * 2 for i in range(5))
print(my_generator)
# 遍历生成器
for value in my_generator:
print(value)
第二种跟第一种效果相同,但是第一种容易出错,所以推荐使用for循环迭代生成器获取数据。
2.yield 关键字
在Python中除了可以使用生成器推导式的方式来创建生成器对象外,我们还可以通过:
函数 + yield关键字方式来创建一个生成器对象(这种创建方式是Python3中比较推荐的一种方式)
使用yield关键字的基本语法为:
def 函数名称():
...
yield 值
...
以下给出案例代码:
def generator(n):
for i in range(n):
print('开始生成数据')
yield i # 暂时可以把yield关键字理解为return,相当于返回数据的含义 => return 0
print('数据生成完成')
上面我们在yield关键字执行前后都打印一句话,用以接下来的介绍。
首先,我们创建一个变量调用我们定义的generator()函数,同样的,调用next()函数:
g = generator(5)
print(next(g))
上述代码的运行结果为:
开始生成数据
0
你会想: 为啥我调用一次只打印了yield前面的语句和一个0呢?
带着疑惑往下看,以上打印next(g),先执行了print('开始生成数据'),然后通过yield关键字返回了第一个数据0,然后函数中止执行,但是要特别注意,这里只是暂停在yield i这个位置,当我们下一次执行生成器的时候,程序会在此结束位置开始继续执行。
简单的说yield关键字与return语句类似,将得到的数据返回给调用者,然后在该位置相当于按下了一个暂停键,当下次再调用时从上次暂停的位置继续执行,直到再次遇到yield关键字,循环往复,直至迭代结束。
所以在第二次调用next()函数时,将从第一次暂停的地方开始执行,直到第二次遇到yield关键字结束:
g = generator(5)
print(next(g))
print(next(g))
运行结果:
开始生成数据
0
数据生成完成
开始生成数据
1
同样的使用yield生成器时,也不能使得调用next()函数次数大于数据集个数,否则同样将报错。
def generator(n):
for i in range(n):
print('开始生成数据')
yield i #类似于return语句,但yield不结束循环,相当于暂停
print('完成一次数据生成')
g = generator(5)
print(next(g)) # 开始生成数据,弹出0
print(next(g)) # 弹出1
print(next(g))
print(next(g))
print(next(g))
print(next(g))
报错消息:
Traceback (most recent call last):
File "D:\BaiduNetdiskDownload\08-Python中yield生成器.py", line 38, in <module>
print(next(g))
^^^^^^^
StopIteration
yield生成器生成的对象也可以使用for循环遍历,这里就不过多赘述。
到这里关于生成器的介绍结束,希望有帮助到各位。
四、使用yield生成器生成斐波那契数列
比起原来的方法生成斐波那契数列,使用生成器生成斐波那契数列将大大减少能耗(运行时间和内存)
现在我给大家做个简单的示例:
1.使用原始的递归算法生成斐波那契数列
import time
start_time = time.time()
def fibonacci(n):
def fn(i):
if i <2:
return 1
else:
return (fn(i-2)+fn(i-1))
for i in range(n):
print(fn(i))
fibonacci(40)
end_time = time.time()
print(f'\n程序运行的总时间为:{end_time - start_time}')
我们调用定义的fibonacci()函数生成前四十位斐波那契数列,通过time模块中的time()方法来计算整个代码的运行时间。
运行结果如下:
程序运行的总时间为:21.86132502555847
2.使用yield生成器生成斐波那契数列
import time
start_time = time.time()
def fibonacci(max):
n, a, b = 1, 0, 1
while n <= max:
yield b
a, b = b, a+b
n += 1
f = fibonacci(40)
for i in f:
print(i, end=' ')
end_time = time.time()
print(f'\n程序运行的总时间为:{end_time - start_time}')
运行上面代码,得出运行时间:
程序运行的总时间为:0.20143699645996094
由此看来使用yield生成器生成40位斐波那契数列所使用的时间远远少于递归方法,证明了文章的结论。
五、注意事项
① 代码执行到 yield 会暂停,然后把结果返回出去,下次启动生成器会在暂停的位置继续往下执行
② 生成器如果把数据生成完成,再次获取生成器中的下一个数据会抛出一个StopIteration 异常,表示停止迭代异常
③ while 循环内部没有处理异常操作,需要手动添加处理异常操作
④ for 循环内部自动处理了停止迭代异常,使用起来更加方便,推荐大家使用。
☆ yield关键字和return关键字
如果不太好理解`yield`,可以先把`yield`当作`return`的同胞兄弟来看,他们都在函数中使用,并履行着返回某种结果的职责。这两者的区别是:
有return的函数直接返回所有结果,程序终止不再运行,并销毁局部变量。
六、总结
Python生成器是处理迭代任务的强大工具,通过按需生成值,提高了效率,减少了时间消耗。在大数据集处理、无限序列表示和惰性计算方面,生成器都显示出了其优越性。在编写Python代码时,不妨考虑使用生成器来使代码更加优雅和高效。
通过深入了解和合理使用生成器,我们可以编写出更加高效和易维护的Python代码。希望这篇文章对你更好地理解和应用生成器提供了一些帮助。
七、结论
如果想生成大量的数据,选择yield生成器来生成是不二之选。