Python基础 8.迭代器 生成器 包
8.1 列表推导式
-
循环添加:
li = [] for i in range(1,10): li.append(i)
-
列表推导:
li = [i for i in range(1, 10)]
-
列表推导+条件判断:
li2 = [i*10 for i in range(1, 10) if i % 2 == 0]
-
列表推导+三目运算:
li3 = [i100 if i % 3 == 0 else i10 for i in range(1, 10)]
-
集合推导:
se = {i for i in range(1,10)}
-
字典推导:
{i: j for i, j in enumerate(li)}
8.2 迭代器
1.迭代器:生成迭代器的方法:
1,iterator = iter(可迭代对象)
2,iterator = 可迭代对象.__iter__()
2.迭代器对象本身需要支持以下两种方法,它们一起构成迭代器协议:
iterator.__iter__()
iterator.__next__()
3.取值:
next(iterator)
iterator.__next__()
可迭代对象 只有iter方法 不能使用next取值
注意:如果迭代器值取完之后,会返回 StopIteration 错误
8.3 生成器:
方法 一:列表推导式的 [ ] 改成 ( )
方法 二:在函数里面加上yield
生成器不会一下子把所以内容生成出来,在我们需要的时候用next()去生成
1 def func(num) : 2 a=0 3 while a< num: 4 yield a 5 a+=1 6 7 ge = func(10) 8 next(ge) 0 9 next(ge) 1 10 next(ge) 2 11 12 13 b = (x for x in range(10)) 14 next(b)0 15 next(b)1 16 next(b)2 17 next(b)3
注意:
1.yield一个对象:
返回这个对象
暂停这个函数
等待下次next重新激活
2.yield 表达式只能在函数中使用,在函数体中使用
3.yield 表达式可以使函数成为一个生成器
4.yield 可以返回表达式结果,并且暂定函数执行,直到next激活下一个yield
Python使用生成器对延迟操作提供了支持。所谓延迟操作,是指在需要的时候才产生结果,而不是立即产生结果,从而节省大量的空间,这也是生成器的主要好处
1 def fib(n): 2 num_before=0 3 num_behind=1 4 for i in range(n): 5 yield num_behind 6 num_before,num_behind=num_behind,num_behind+num_before 7 8 result=fib(10) 9 for i in result: 10 print(i)
8.4 if name == 'main':
- py文件的两种功能
1 #编写好的一个python文件可以有两种用途: 2 1、脚本,一个文件就是整个程序,用来被执行 3 2、模块,文件中存放着一堆功能,用来被导入使用 4 5 6 #python为我们内置了全局变量__name__, 7 当文件被当做脚本执行时:__name__ 等于'__main__' 8 当文件被当做模块导入时:__name__等于模块名 9 10 #作用:用来控制.py文件在不同的应用场景下执行不同的逻辑 11 if __name__ == '__main__':
-
name
python会自动的给模块加上这个属性
如果模块是被直接调用的,则 name 的值是 main 否则就是该模块的模块名
-
if name == 'main':
该语句可以控制代码在被其他模块导入时不被执行
8.5 导入模块
8.5.1、什么是模块
模块就是一组功能的集合体,我们的程序可以导入模块来复用模块的功能
在python中,模块的使用方式都是一样的,但其实细说的话,模块可以分为四个通用类别:
- 使用python编写的代码(.py文件)
- 已被编译为共享库或DLL的C或C++扩展
- 包好一组模块的包
- 使用C编写并链接到python解释器的内置模块
8.5.2、为什么使用模块
- 从文件级别组织程序,更方便管理。随着程序的发展,功能越来越多,为了方便管理,我们通常将程序分成一个个的文件,这样做程序的结构更清晰,方便管理。这时我们不仅仅可以把这些文件当做脚本去执行,还可以把他们当做模块来导入到其他的模块中,实现了功能的重复利用
- 拿来主义,提升开发效率。同样的原理,我们也可以下载别人写好的模块然后导入到自己的项目中使用,这种拿来主义,可以极大地提升我们的开发效率
8.5.3、模块使用之import
- 使用import导入
模块可以包含可执行的语句和函数的定义,这些语句的目的是初始化模块,它们只在模块名第一次遇到导入import语句时才执行
import语句是可以在程序中的任意位置使用的,且针对同一个模块为了防止你重复导入,python的优化手段是:
第一次导入后就将模块名加载到内存了,后续的import语句仅是对已经加载到内存中的模块对象增加了一次引用,不会重新执行模块内的语句
8.5.4、为模块起别名
1 import test as ts 2 #可以将过长的模块命名改成短的,便于操作 3 #有利于代码的拓展,优化。
8.5.5、导入多个模块
1 import sys,os,json # 可以这样写,但是不推荐 2 3 #推荐应该这样: 4 import sys 5 import os 6 import json
8.5.6、from .... import ....
1 from test import t1 2 from test import t1 as t 3 #支持as 4 from test import t1,t2,te 5 #支持导入多个 6 7 #与import区别 8 使用from...import...则是将模块中的名字直接导入到当前的名称空间中,所以在当前名称空间中,直接使用名字就可以了、无需加前缀 9 10 #from...import...的方式有好处也有坏处:好处:使用起来方便了,坏处:容易与当前执行文件中的名字冲
8.5.7、模块的搜索路径
模块的查找顺序是:内存中已经加载的模块->内置模块->sys.path路径中包含的模块
(Python解释器只认执行文件的os.path,与同级目录无关)
1 import os 2 import sys 3 dir=os.path.dirname(os.path.dirname(os.path.abspath(__file__)) #上级目录 4 sys.path.append(dir) #增加sys.path路径
#sys.path.append('path') (不会用请查百度)
8.6、包
8.6.1、包是什么
包是一种通过使用‘.模块名’来组织python模块名称空间的方式
具体的:包就是一个包含有__init__.py文件的文件夹,所以其实我们创建包的目的就是为了用文件夹将文件/模块组织起来
需要强调的是:
- 在python3中,即使包下没有__init__.py文件,import 包仍然不会报错,而在python2中,包下一定要有该文件,否则import 包报错
- 创建包的目的不是为了运行,而是被导入使用,记住,包只是模块的一种形式而已,包的本质就是一种模块
8.6.2、导入使用注意事项
- 关于包相关的导入语句也分为import和from ... import ...两种,但是无论哪种,无论在什么位置,在导入时都必须遵循一个原则:凡是在导入时带点的,点的左边都必须是一个包,否则非法。可以带有一连串的点,如item.subitem.subsubitem,但都必须遵循这个原则。但对于导入后,在使用时就没有这种限制了,点的左边可以是包,模块,函数,类(它们都可以用点的方式调用自己的属性)。
- import导入文件时,产生名称空间中的名字来源于文件,import 包,产生的名称空间的名字同样来源于文件,即包下的__init__.py,导入包本质就是在导入该文件
- 包A和包B下有同名模块也不会冲突,如A.a与B.a来自俩个命名空间
8.6.3、绝对导入和相对导入
我们的最顶级包是写给别人用的,然后在包内部也会有彼此之间互相导入的需求,这时候就有绝对导入和相对导入两种方式:
绝对导入:以包作为起始
相对导入:用.或者..的方式最为起始(只能在一个包中使用,不能用于不同目录内)
1 # 绝对导入: 以执行文件的sys.path为起始点开始导入,称之为绝对导入 2 # 优点: 执行文件与被导入的模块中都可以使用 3 # 缺点: 所有导入都是以sys.path为起始点,导入麻烦 4 5 # 相对导入: 参照当前所在文件的文件夹为起始开始查找,称之为相对导入 6 # 符号: .代表当前所在文件的文件夹,..代表上一级文件夹,...代表上一级的上一级文件夹 7 # 优点: 导入更加简单 8 # 缺点: 只能在导入包中的模块时才能使用 9 #注意: 10 #相对导入只能用于包内部模块之间的相互导入,导入者与被导入者都必须存在于一个包内 11 12 #试图在顶级包之外使用相对导入是错误的,言外之意,必须在顶级包内使用相对导入,每增加一个.代表跳到上一级文件夹,而上一级不应该超出顶级包
8.7 补充(迭代器)
8.7.1、可迭代对象
可迭代对象指的是内置有__iter__方法的对象,即obj.__iter__,如:
1 from collections import Iterable 2 li = [1, 2, 3, 4] 3 tu = (1, 2, 3, 4) 4 di = {1, 2, 3, 4} 5 se = {1, 2, 3, 4} 6 print(isinstance(se, Iterable)) #判断是否是可迭代对象 7 print(isinstance(di, Iterable)) 8 print(isinstance(tu, Iterable)) 9 print(isinstance(li, Iterable))
执行结果:
字符串、列表、元组、字典、集合都可以被for循环,说明他们都是可迭代的,
我们现在所知道:可以被for循环的都是可迭代的,要想可迭代,内部必须有一个__iter__方法。(查看内部方法dir(obj))
8.7.2、迭代器
迭代器是访问集合元素的一种方式。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。
迭代器只能往前不会后退。迭代器必须有__iter__方法和__next__方法。将可迭代对象转化成迭代器。(可迭代对象.__iter__())
1 from collections import Iterator 2 li = [1,2,3,4] 3 list_iter = li.__iter__() # 将可迭代的转化成迭代器 4 print(isinstance(list_iter,Iterator)) #判断是否是迭代器,结果为True
看个例子:
1 from collections import Iterable 2 from collections import Iterator 3 class a(): 4 def __next__(self):pass 5 def __iter__(self):pass #为a定义两个方法 6 dd=a() #实例化 7 print(isinstance(dd, Iterator)) 8 print(isinstance(dd, Iterable)) 9 #结果都为True,如果去掉__next__方法即第一个结果为False 10 #迭代器对象一定是可迭代对象,而可迭代对象不一定是迭代器对象
8.7.3、for循环
for循环,能遍历一个可迭代对象,他的内部到底进行了什么?
- 将可迭代对象转化成迭代器。(可迭代对象.__iter__())
- 内部使用__next__方法,一个一个取值。
- 加了异常处理功能,取值到底后自动停止。
用while循环模拟for循环
1 l=[1,2,3,4] 2 ss=l.__iter__() 3 while 1: 4 try: 5 print(ss.__next__()) 6 except Exception as e: 7 break
执行结果:
对于序列类型:字符串、列表、元组,我们可以使用索引的方式迭代取出其包含的元素。但对于字典、集合、文件等类型是没有索引的,怎样取出其内部包含的元素?而for循环就是基于迭代器协议提供了一个统一的可以遍历所有对象的方法,即在遍历之前,先调用对象的__iter__方法将其转换成一个迭代器,然后使用迭代器协议去实现循环访问,这样所有的对象就都可以通过for循环来遍历了,而且你看到的效果也确实如此,这就是无所不能的for循环,最重要的一点,转化成迭代器,在循环时,同一时刻在内存中只出现一条数据,极大限度的节省了内存。
1 #基于for循环,我们可以完全不再依赖索引去取值了 2 dic={'a':1,'b':2,'c':3} 3 for k in dic: 4 print(dic[k])
8.7.4、迭代器优缺点:
优点:
- 提供一种统一的、不依赖于索引的迭代方式
- 惰性计算,节省内存
缺点:
- 无法获取长度(只有在next完毕才知道到底有几个值)
- 一次性的,只能往后走,不能往前退
8.8、补充(生成器)
本质:迭代器(所以自带了__iter__方法和__next__方法,不需要我们去实现)
特点:惰性运算节省内存,开发者自定
Python中提供的生成器:
- 生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次重它离开的地方继续执行
- 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表
8.8.1、生成器函数
包含yield关键字的函数就是一个生成器函数。yield可以为我们从函数中返回值,但是yield又不同于return,return的执行意味着程序的结束,调用生成器函数不会得到返回的具体的值,而是得到一个可迭代的对象。每一次获取这个可迭代对象的值,就能推动函数的执行,获取新的返回值。直到函数执行结束。
1 def fun(): 2 a=123 3 yield a 4 b=456 5 yield b 6 7 ret=fun() 8 print("ret:",ret) #ret为生成器 9 print("1:",next(ret)) #返回第一个yield结果 10 print("2:",next(ret)) #返回第二个yield结果 next()等同__next__()
执行结果:
生成器有什么好处呢?就是不会一下子在内存中生成太多数据。
例如:假设你需要去工厂订购10000000台苹果手机,工厂应该是先答应下来然后再去生产,你可以一台一台的取,
也可以根据需要一批一批的找工厂拿。而不能是一说要生产10000000台苹果手机,工厂就立刻生产出10000000台苹果手机,这样工厂的工人和生产线肯定都是要爆掉的.....
1 def produce(): 2 """生产手机""" 3 for i in range(1,10000000): 4 yield "生产了第%s台手机"%i 5 6 ret = produce() 7 print(ret.__next__()) #要第一台手机 8 print(ret.__next__()) #第二台手机 9 print(ret.__next__()) #再要一台手机 10 count = 0 11 for i in ret: #要一批手机,比如6台 12 print(i) 13 count +=1 14 if count == 6: 15 break
执行结果:
send传值
1 import time 2 def consumer(name): 3 print("%s 准备生产手机了!" %name) 4 while True: 5 iPhone = yield 6 7 print("[%s]被[%s]生产出来了!" %(iPhone,name)) 8 9 def producer(): 10 c = consumer('A') 11 c2 = consumer('B') 12 c.__next__() 13 c2.__next__() 14 print("开始生产手机了!") 15 for i in range(10): 16 time.sleep(1) 17 c.send(i) 18 c2.send(i) #传值给yield 19 20 producer() 21 22 23 24 #send 获取下一个值的效果和next基本一致 25 #只是在获取下一个值的时候,给上一yield的位置传递一个数据 26 #使用send的注意事项 27 # 第一次使用生成器的时候 是用next获取下一个值 28 # 最后一个yield不能接受外部的值
执行结果:
8.8.2、生成器表达式
- 三元表达式
1 name="jump" 2 res='SB' if name == 'crazyjump' else 'xxx' 3 print(res) 4 5 #结果xxx
- 列表推导式
1 j=[i for i in range(10) if i >5] #没有else 2 print(j)
执行结果:
- 生成器表达式
1 j=(i for i in range(10) if i >5) 2 print(next(j)) 3 print(j)
执行结果:
- 把列表推导式的[]换成()得到的就是生成器表达式
- 列表推导式与生成器表达式都是一种便利的编程方式,只不过生成器表达式更节省内存