day11 迭代器和生成器
今日内容概要
f-strings
详解- 迭代器
- 生成器
昨日内容回顾
-
函数的默认参数
- 动态位置参数:
*args
,接受多余位置参数,以元组形式储存 - 动态关键字参数:
**kwargs
,接受多于默认参数,以字典形式储存 - 参数的优先级:位置参数 > 动态位置参数 > 默认参数 > 动态关键字参数
- 函数的定义阶段,碰到
*
和**
是聚合 - 在函数体中碰到
*
是打散,*args
打散元组,**kwargs
打散字典,获取的是键 - 在函数的调用中也可以用到
*
打散,后期可能会用到
- 动态位置参数:
-
函数的注释
def func(a:int,b:str): """ 计算字符串的乘法 :param a: int :param b: str :return: str """ return a * b c = func(3,"a") print(c)
print(func.__doc__)
查看函数的注释print(func.__name__)
查看函数的名字
-
函数的名称空间
- 内置空间
- 全局空间
- 局部空间
- 加载:内置 > 全局 > 局部
- 取值:局部 > 全局 > 内置
- 作用域:
- 全局 = 内置 + 全局
- 局部 = 局部
-
函数名的第一类对象及使用
- 函数名可以作为值赋值给变量
- 函数名可以作为另一个函数的参数
- 函数名可以作为另一个函数的返回值
- 函数名可以作为元素储存在容器中
-
函数的嵌套
- 交叉嵌套
- 回环嵌套
-
global
和nonlocal
global
只能声明要修改全局变量- 当全局中没有这个变量时,会给全局创建这个变量
- 当全局中有这个变量时,会修改这个变量的值
nonlocal
只能声明要修改局部变量- 当局部中没有这个变量时,不但不会创建,还会报错,即便全局中有也不成
- 当局部中有这个变量时,只修改离
nonlocal
最近的一层变量 - 如果
nonlocal
的上一层没有就继续向上一层寻找,直到找到最外层局部空间
今日内容详细
f-strings
详解
f-strings
在字符串格式化的那一部分已经有所讨论。其实当时已经讨论得差不多了,今天只是稍微地有一点点补充。主要还是复习。
f-strings
的基本结构是这样的:
name = '宝元'
age = 18
sex = '男'
msg = F'姓名:{name},性别:{age},年龄:{sex}' # 大写字母也可以
msg = f'姓名:{name},性别:{age},年龄:{sex}' # 建议小写
print(msg)
输出的结果为:
姓名:宝元,性别:18,年龄:男
f-strings
就是在字符串的引号前面加上一个字母f
。字母大小写都可以,但是推荐使用小写。{}
中除了可以使用变量外,还可以放入函数:
def func(a,b):
return a + b
msg = f"运行结果:{func(1,2)}"
print(msg)
输出的结果为:
运行结果:3
甚至可以在{}
中放入input
函数,让用户输入:
print(f"姓名:{input('请输入姓名:')} 年龄:{input('请输入年龄:')} 性别:{input('请输入性别:')}")
输出的结果是:
请输入姓名:alex
请输入年龄:18
请输入性别:男
姓名:alex 年龄:18 性别:男
除了字符串,{}中也可以放入列表和字典:
lst = [1,2,32,34,45,5]
msg = f"运行结果:{lst[0:3]}"
print(msg)
dic = {"key":1,"key1":22}
msg = f"运行结果:{dic['key1']}"
print(msg)
输出的结果为:
运行结果:[1, 2, 32]
运行结果:{'key': 1, 'key1': 22}
f-string
可以写成多行的形式,但依然打印成一行:
msg = f"窗前明月{'光'},"
f"玻璃好上{'霜'}."
f"要不及时{'擦'},"
f"一会就得{'脏'}."
print(msg)
输出的结果为:
窗前明月光,玻璃好上霜.要不及时擦,一会就得脏.
要想打印多行字符串,还是要使用三对引号:
msg = f"""
窗前明月{'光'},
玻璃好上{'霜'}.
要不及时{'擦'},
一会就得{'脏'}.
"""
print(msg)
通过使用三元运算,配合f-strings,我们可以进一步节省代码:
a = 10
b = 20
msg = f"{a if a < b else b}"
print(msg)
同时使用两个括号表示一个可以打印的大括号:
msg = f"{{'alex':'wusir'}}"
print(msg)
输出的结果为:{'alex':'wusir'}
迭代器
迭代器就是用来将可迭代对象的值一个一个取出来的工具。
我们学过的可迭代的数据类型有:字符串、列表、字典、元组、集合
不可迭代的数据类型有:整型、布尔值
Python中规定,只要是具有__iter__()
方法就是可迭代对象:
str.__iter__()
list.__iter__()
tuple.__iter__()
dict.__iter__()
set.__iter__()
可迭代对象可以通过for循环获取每一个元素,且可以重复取值:
s = 'alex'
for i in s:
print(i)
for j in s:
print(j)
输出的结果为:
a
l
e
x
a
l
e
x
我们可以使用可迭代对象的.__iter__()
方法,将其转化为迭代器。可以通过.__next__()
方法取下一位的值:
lst = [1, 2, 3, 4, 5]
l = lst.__iter__() # 生成一个迭代器对象
print(l)
print(l.__next__()) # 获取下一位元素
print(l.__next__())
print(l.__next__())
print(l.__next__())
print(l.__next__())
输出的结果为:
<list_iterator object at 0x0000026525B9B588>
1
2
3
4
5
需要注意的是,迭代器中有多少个元素,就只能使用多少次.__next__()
方法,如果使用次数超过元素个数,就会报错:
File "C:/Users/Sure/PyProject/week03/day11/exercise.py", line 59, in <module>
print(l.__next__())
StopIteration
在Python中,有.__iter__()
和.__next__()
方法的就是一个迭代器。
不难看出,文件句柄也是一个迭代器:
f = open('user_info', 'a', encoding='utf-8')
f.__iter__()
f.__next__()
这里说一句,当我们使用id函数获取到的内存地址,并不是数据真正存放的位置,只是一串供我们参考的数字而已,不可当真。
需要注意的是,当对同一个可迭代对象多次使用.__iter__()
方法创建迭代器的时候,我们是创建了多个生成器,这些生成器之间不会相互影响:
lst = [1, 2, 3, 4, 5]
l1 = lst.__iter__() # 这是一个迭代器1
l2 = lst.__iter__() # 这是一个迭代器2
l3 = lst.__iter__() # 这是一个迭代器3
print(l1.__next__())
print(l2.__next__())
print(l3.__next__())
print(l2.__next__())
print(l3.__next__())
print(l3.__next__())
输出的结果为:
1
1
1
2
2
3
本质上来讲,for循环的底层,就是将可迭代对象转换为生成器,通过循环迭代,获取每一个元素的:
s = 'alex'
s1 = s.__iter__()
while True:
try: # 尝试运行一下缩进体中的代码
print(s1.__next__())
except StopIteration:
break
输出的结果为:
a
l
e
x
除了使用可迭代对象的.__iter__()
和.__next__()
方法创建和操作迭代器,我们还可以使用Python的内置函数,iter()
和next()
来实现同样地功能:
lst = [1, 2, 3, 4, 5]
l = iter(lst)
print(next(l))
print(next(l))
print(next(l))
输出结果为:
1
2
3
需要注意的是,在Python 2中,创建和使用迭代器时只能使用内置函数iter()
和next()
,迭代器的.__iter__()
和.__next__()
方法不可用;在Python 3中,既可以使用内置函数iter()
和next()
,也可以使用.__iter__()
和.__next__()
方法创建和使用迭代器。但是推荐使用兼容性更高的内置函数iter()
和next()
。
可迭代对象与迭代器的比较:
可迭代对象:str list tuple ...
具有.__iter__()方法的就是一个可迭代对象
优点:
1. 使用灵活(每个可迭代对象都有自己的方法)
2. 能够直接且直观地查看元素个数
缺点:
占内存
应用:当内存空间大,数据量比较少的情况,建议使用可迭代对象
迭代器:文件句柄就是一个迭代器
具有.__iter__()和.__next__()方法的,就是一个迭代器
有点:
节省内存
缺点:
1. 只能一个方向执行
2. 一次性的
3. 不能灵活操作,不能直接且直观查看元素个数
应用:当内存小,数据量大的情况,建议使用迭代器
迭代器除了节省内存之外,似乎没有什么好处,但是广泛应用于Python编程过程中,就是因为它能大量节省内存空间。在编程过程中,我们常常会进行空间和时间的抉择:
- 时间换空间:迭代器,生成器,用大量的时间来节省空间的使用
- 空间换时间:可迭代对象,使用大量的空间来节省时间
迭代器同样具有.__iter__()
方法,因此也是一个可迭代对象,可以直接被for循环:
lst = [1, 2, 3, 4]
l = iter(lst)
for i in l: # for循环可以直接循环迭代器
print(i)
生成器
生成器的本质就是一个迭代器。
生成器和迭代器的最大区别为:
- 迭代器:比如文件句柄,是通过数据转换,由Python自带提供的
- 生成器:程序员自己编写实现
生成器的目的为:不再通过数据转换实现迭代器,而是通过代码编写实现。
生成器的定义方式有两种:
- 基于函数实现生成器
- 使用表达式实现生成器
我们可以通过这个方式来定义和使用一个函数:
def func():
print(1)
return 5
print(func())
输出的结果为:
1
5
如果我们把函数中的return替换成yield,就创建了一个生成器:
def func():
print(1)
yield 5 # 函数体中存在yield就是定义了一个生成器
print(func()) # 创建一个生成器对象
输出的结果为:<generator object func at 0x000001D59DAA2EB8>
func
函数调用时,会生成一个生成器对象,print
打印出来的就是这个生成器对象的内存地址。
补充一点支持,下面这段代码:
def func():
print(foo)
def func():
print(foo)
运行过后程序并没有报错,虽然并没有变量或者函数命名为foo。这是因为程序运行过程中,会有两个分析过程:语法分析和词法分析。
词法分析就是分析代码中是否所有的词语都符合规范,如果不规范,则会报错。
语法分析则是判断每个语句是否合乎语法规范。
上面的两个代码,词法分析是可以的,语法分析因为不会进入到函数体中,所以也不会出错,故而没有报错。
生成器的特点是:惰性机制,也就是只有使用next方法调用生成器时,才会开始执行生成器的代码。即便是在创建生成器对象时,也不会运行装饰器中的内容。
yield和return的部分功能很像
我们可以设定多个yield,每次使用next函数,就会运行到下一个yield,直至最后:
def func():
yield 1
yield 2
yield 3
g = func()
print(next(g))
print(next(g))
print(next(g))
输出的结果为:
1
2
3
同迭代器类似,也可以创建多个生成器对象,这些生成器对象彼此独立,互不干扰:
def func():
yield 1
yield 2
yield 3
g1 = func()
g2 = func()
g3 = func()
print(g1, g2, g3)
print(next(g1))
print(next(g2))
print(next(g3))
print(next(g2))
print(next(g3))
print(next(g3))
输出的结果为:
<generator object func at 0x0000026AC9932EB8> <generator object func at 0x0000026AC9932F10> <generator object func at 0x0000026AC9932F68>
1
1
1
2
2
3
三个生成器的内存地址各不相同,使用next方法也是互不影响。
如果yield后面什么也不写,默认返回的值为None:
def func():
yield
print(func().__next__())
输出的结果为:None
同return类似,yield后面也可以接任意数据类型:
def func():
yield [1, 2, 3, 4, 5]
print(func().__next__(), type(next(func())))
def foo():
yield {'key': 1}
print(next(foo()), type(next(foo())))
def f():
def f1():
print(123)
yield f1
print(next(f()), type(next(f())))
def f2():
yield 1, 2, 3, 4, 5
print(next(f2()), type(next(f2())))
输出的结果为:
[1, 2, 3, 4, 5] <class 'list'>
{'key': 1} <class 'dict'>
<function f.<locals>.f1 at 0x0000022675D8DBF8> <class 'function'>
(1, 2, 3, 4, 5) <class 'tuple'>
生成器的好处同迭代器一样,也是可以节省大量的内存空间。
除了yield方法,我们还可以使用yield from方法来逐个返回可迭代对象中的元素:
def func():
yield from [1, 2, 3, 4, 5] # 将列表中的元素逐个返回
g = func()
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
输出的结果为:
1
2
3
4
5
如果两个yield from同时存在,会先将前面的可迭代对象逐个返回之后,再返回后面的可迭代对象:
def func():
yield from [55, 44, 66, 77, 88]
yield from [1, 2, 3, 4, 5]
g = func()
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))
输出结果为:
55
44
66
77
88
1
2
3
4
5
总结
生成器一定是一个迭代器,但是迭代器不一定是一个生成器;
迭代器一定是一个可迭代对象,但是可迭代对象不一定是一个迭代器。
生成器的本质是一个迭代器,迭代器的本质是一个可迭代对象。
迭代器和生成器的有点:
- 节省空间
迭代器和生成器的缺点:
- 不能直接使用元素
- 不能直观查看元素个数
- 使用不灵活
- 稍微消耗时间
- 操作是一次性的,不可逆的
当数据量特别巨大时,要记得使用生成器
区分迭代器和生成器:
lst = [1, 2, 3]
l = lst.__iter__()
def func():
yield 1
g = func()
print(l, g)
输出的结果为:
<list_iterator object at 0x0000021C1E7BB5C0> <generator object func at 0x0000021C1E722EB8>
- 将对象直接用
print
函数打印出来,查看内存地址。如果显示的是iterator
,就是迭代器;如果是generator
,就是生成器(主推荐的); - 产看是否可用
.send()
方法,如果可用,则是生成器,不可用则是迭代器。
yield的特点:
- yield能返回多个数据,以元组的形式存储
- yield能返回各种数据类型(Python中的任意对象)
- yield能够写入多个并且都可以执行
- yield能够记录执行的位置
- yield后面不写内容,默认返回None
- yield都是将数据一次性返回
yield from的特点:
- 将可迭代对象逐个返回
可迭代对象、迭代器和生成器的比较:
-
可迭代对象:具有
.__iter__()
方法的就是可迭代对象 -
迭代器:具有
.__iter__()
和.__next__()
方法的就是一个迭代器 -
生成器:基于函数创建的生成器,函数体中必须存在yield