首页 > Python资料 博客日记
[Python学习日记-42] Python 中的生成器
2024-10-26 11:00:05Python资料围观31次
[Python学习日记-42] Python 中的生成器
简介
Python 中的生成器(Generator)是一种特殊的迭代器,它又被成为惰性运算,它可以在迭代过程中动态生成值,而不需要事先把所有的值存储在内存中。生成器可以通过两种方式来定义:使用生成器表达式或函数生成器。下面我们一起来看看生成器到底是怎么回事吧。
表达式生成器
在前面学习了列表生成式,通过列表生成式,我们可以直接创建一个列表。但是会受到内存限制,所以列表容量肯定是有限的。如果创建一个包含100万个元素的列表,不仅会占用很大的存储空间,而且如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。例如要循环100万次,如果按 Python 的语法,代码如下
注意:Python3 对 range() 进行了优化,使用了生成器,所以我们要切换到 Python2 当中来查看
# Python2 的环境下
for i in range(1000000):
print(i)
# 会看到包含0-999999的列表的出现
print(range(1000000))
代码输出如下:
上面的代码会先生成100万个值的列表。假如现在循环到第50次时就不想继续就退出了。但是90多万的列表元素就白为你提前生成了。代码如下
# Python2 的环境下
for i in range(1000000):
if i == 50:
break
print(i)
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?像上面代码中的循环,每次循环只是 +1 而已。我们完全可以写一个算法,让他执行一次就自动 +1,这样就不必创建完整的列表了,从而节省大量的空间。在 Python 中,这种一边循环一边计算后面元素的机制,称为生成器(generator)。
要创建一个生成器,有很多种方法。第一种方法很简单,只要把一个列表生成式的 [] 改成 (),就创建了一个生成器了,即生成器表达式,代码如下
# 列表生成式
l = [x * x for x in range(10)]
print(l)
# 生成器
l = (x * x for x in range(10))
print(l)
代码输出如下:
上面的代码当中 (x * x for x in range(10)) 生成的就是一个生成器了。可问题来了,我们可以直接打印出列表当中的每一个元素,但是我们怎么打印出生成器的每一个元素呢?
如果要一个一个打印出来,可以通过 next() 函数获得生成器的下一个返回值,代码如下
g = (x * x for x in range(10))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
代码输出如下:
前面我们讲过,生成器保存的是算法,每次调用 next(g) 就计算出 g 的下一个元素的值,直到计算到最后一个元素,当没有更多的元素时,则会抛出 StopIteration 的错误。当然,像上面代码那样不断地使用 next(g) 来获取下一个值实在是不太正常,正确的方法是使用 for 循环来获取,这是因为生成器也是一个可迭代(遍历)对象,代码如下
g = (x * x for x in range(10))
for i in g:
print(i)
代码输出如下:
函数生成器
生成器非常强大,但是如果推算的算法比较复杂,用生成器表达式生成的生成器配合 for 循环无法实现想要效果的时候,还可以用函数来实现生成器。例如著名的斐波拉契数列(Fibonacci),即除第一个和第二个数外,任意一个数都可由前两个数相加得到,如下
1,1,2,3,5,8,13,21,34,...
下面先试用普通代码来实现100以内的斐波拉契数列,代码如下
a,b = 0,1
n = 0 # 斐波那契数
while n < 100:
n = a + b
a = b # 把 b 的旧值给到 a
b = n # 新的 b = a + b(旧 b 的值)
if n > 100:
break
else:
print(n)
代码输出如下:
下面我们把上面的代码改写成函数看看应该如何实现呢?代码如下
def fib(max):
a,b = 0,1
n = 0 # 斐波那契数
while n < max:
n = a + b
a = b # 把 b 的旧值给到 a
b = n # 新的 b = a + b(旧 b 的值)
if n > 100:
break
else:
print(n)
fib(100)
代码输出如下:
从输出可以看出,fib() 函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似生成器。也就是说,上面的 fib() 函数和生成器仅一步之遥。要把 fib() 函数变成生成器,只需要把 print(n) 改为 yield n 就可以了,代码如下
def fib(max):
a,b = 0,1
n = 0 # 斐波那契数
while n < max:
n = a + b
a = b # 把 b 的旧值给到 a
b = n # 新的 b = a + b(旧 b 的值)
if n > 100:
break
else:
yield n # 程序走到这,就会暂停下来,返回 n 到函数外面,直到被 next 方法调用时唤醒
f = fib(100) # 注意这句调用时,函数并不会执行,只有下一次调用 next 时,函数才会真正执行
print(next(f))
print(next(f))
print(f.__next__()) # next(f) 和 f.__next__() 是一样的
print(f.__next__())
代码输出如下:
这就是定义生成器的另一种方法,函数生成器。如果一个函数定义中包含 yield 关键字,那么这个函数就不再是一个普通函数,而是一个函数生成器。
这里,最难理解的就是生成器和函数的执行流程不一样。函数是顺序执行的,遇到 return 语句或者最后一行函数语句就返回。而变成生成器的函数后,在每次调用 next() 的时候函数才执行,遇到 yield 语句就暂停并返回数据到函数外,再次被 next() 调用时从上次返回的 yield 语句处继续执行。
我们尝试一下在总多 next() 和 __next__() 中插入一些别的事情,看看会不会有影响,代码如下
def fib(max):
a,b = 0,1
n = 0 # 斐波那契数
while n < max:
n = a + b
a = b # 把 b 的旧值给到 a
b = n # 新的 b = a + b(旧 b 的值)
if n > 100:
break
else:
yield n # 程序走到这,就会暂停下来,返回 n 到函数外面,直到被 next 方法调用时唤醒
f = fib(100) # 注意这句调用时,函数并不会执行,只有下一次调用 next 时,函数才会真正执行
print(next(f))
print(next(f))
print(f.__next__()) # next(f) 和 f.__next__() 是一样的
print("干点别的事情")
print(f.__next__())
代码输出如下:
在上面输出中,我们在循环过程中不断调用 yield,函数就会不断的中断(暂停),即使中间有其他代码插进来运行了。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。同样的,把函数改成生成器后,我们基本上从来不会用 next() 来获取下一个返回值,而是直接
使用 for 循环来迭代,代码如下
def fib(max):
a,b = 0,1
n = 0 # 斐波那契数
while n < max:
n = a + b
a = b # 把 b 的旧值给到 a
b = n # 新的 b = a + b(旧 b 的值)
if n > 100:
break
else:
yield n # 程序走到这,就会暂停下来,返回 n 到函数外面,直到被 next 方法调用时唤醒
f = fib(100) # 注意这句调用时,函数并不会执行,只有下一次调用 next 时,函数才会真正执行
for i in f:
print(i)
代码输出如下:
用生成器实现并发编程
虽然我们还没讲过并发编程,但我们肯定听过 CPU 多少核或者多少线程之类的,CPU 的多核就是为了可以实现并行运算的,就是让你同时边听歌、边聊 QQ、边刷知乎。而单核的 CPU 同一时间只能干一个事,所以你用单核电脑同时做好几件事的话,就会变的很慢,因为cpu要在不同程序任务间来回切换。
而通过函数生成器(yield),我们可以实现单核下并发做多件事的效果,即单线程多并发。在此之前我们要先说一个知识点,上面说了 yield 可以返回运算后的结果后暂停函数,那我们能不能把数据传进去然后暂停函数呢?这当然是可以的,我们只需要把 yield n 后面的 n 去掉,然后把 yield 赋值给一个变量就好了,并且传入的方法从 next()/__next__() 变为 send(),代码如下
def g_test():
while True:
n = yield # 收到的值给n
print("receive from outside:",n)
g = g_test()
g.__next__() # 调用生成器,同时会发送None到yield
for i in range(10):
# can't send non-None value to a just-started generator
# 生成器一开始需要发送一个None的值来激活yield,send()不能发送空值
g.send(i) # 调用生成器,同时发送i
代码输出如下:
上面的代码有几点需要注意的,yield 的输入要求一定要先运行 __next__() 或者 next(),然后再使用 send() 发送数据到函数,否则会抛出 TypeError 的错误,如下图所示
这是因为在没有进行第一次 next()/__next__() 之前函数并没有运行,就是说这个时候函数并没有进行初始化,而当运行了第一次 next()/__next__() 之后函数则会在 yield 处暂停,这个时候运行 send() 就会畅通无阻了。
说完这个知识点我们回到如何通过函数生成器(yield)实现单线程多并发的,我们以一个包子店的例子为例,我们的需求有以下几点:
- 包子店会接待多个消费者
- 包子店的师傅会在消费者吃包子的时候会生产一批包子
- 师傅做包子的时候消费者也在吃包子
代码如下
# 吃包子的消费者 c1,c2,c3
def consumer(name):
print("消费者%s准备吃包子啦。。。"%name)
while True:
baozi = yield # 接受外面的包子
print("消费者%s收到包子:%s"%(name,baozi))
c1 = consumer("C1")
c1.__next__()
c2 = consumer("C2")
c2.__next__()
c3 = consumer("C3")
c3.__next__()
for i in range(10):
print("--------生产了第%s批包子--------"%i)
c1.send(i)
c2.send(i)
c3.send(i)
代码输出如下:
从输出来看包子店的师傅在一边制作包子,一边分发给每一位顾客,并不是像普通的函数那样,先做好所有包子,然后再分发给顾客。函数生成器(yield)是如何实现单线程多并发的呢?其实它并不是同时在工作的,在 CPU 看来都是顺序作业,只不过速度非常快,从人的感觉来看就像是在并行工作一样。
他们的工作顺序是这样的:消费者 C1 到店——消费者 C2 到店——消费者 C3 到店——生产第1批包子——消费者 C1 收到包子:1——消费者 C2 收到包子:1——消费者 C3 收到包子:1——生产第2批包子——消费者 C1 收到包子:2——消费者 C2 收到包子:2——消费者 C3 收到包子:2——生产第3批包子...
如此类推,这就是通过函数生成器(yield)实现单线程多并发了
标签:
相关文章
最新发布
- 【Python】selenium安装+Microsoft Edge驱动器下载配置流程
- Python 中自动打开网页并点击[自动化脚本],Selenium
- Anaconda基础使用
- 【Python】成功解决 TypeError: ‘<‘ not supported between instances of ‘str’ and ‘int’
- manim边学边做--三维的点和线
- CPython是最常用的Python解释器之一,也是Python官方实现。它是用C语言编写的,旨在提供一个高效且易于使用的Python解释器。
- Anaconda安装配置Jupyter(2024最新版)
- Python中读取Excel最快的几种方法!
- Python某城市美食商家爬虫数据可视化分析和推荐查询系统毕业设计论文开题报告
- 如何使用 Python 批量检测和转换 JSONL 文件编码为 UTF-8
点击排行
- 版本匹配指南:Numpy版本和Python版本的对应关系
- 版本匹配指南:PyTorch版本、torchvision 版本和Python版本的对应关系
- Python 可视化 web 神器:streamlit、Gradio、dash、nicegui;低代码 Python Web 框架:PyWebIO
- 相关性分析——Pearson相关系数+热力图(附data和Python完整代码)
- Python与PyTorch的版本对应
- Anaconda版本和Python版本对应关系(持续更新...)
- Python pyinstaller打包exe最完整教程
- Could not build wheels for llama-cpp-python, which is required to install pyproject.toml-based proj