声明:本内容非盈利性质,也不支持任何组织或个人将其用作盈利用途。本内容来源于参考书或网站,会尽量附上原文链接,并鼓励大家看原文。侵删。
5 进阶语法
5.1 推导式、生成器与迭代器
python为了简化语法方便计算,提供了很多便捷的基础语法。推导式、生成器与迭代器就是这样一种语法。
5.1.1 推导式
推导式的作用是通过原有可迭代类型变量按指定运算规则生成一个新的可迭代类型变量。
推导式有如下四种:
- 条件推导式:根据条件选择表达式进行输出;(条件推导式是其他三种推导式的基础)
- 列表推导式:由旧列表根据指定运算规则形成新列表;
- 集合推导式:由旧集合根据指定运算规则形成新集合;
- 字典推导式:由旧字典根据指定运算规则形成新字典。
四种推导式的形式如下示例:
'''
条件推导式格式:表达式1 if 判断条件 else 表达式2。如果满足条件则执行表达式1,否则执行表达式2
'''
# 新手代码
x = 10
if x%2 == 0:
print("新手说:x是偶数")
else:
print("新手说:x是奇数")
# 老司机
x = 15
print("老司机说:x是偶数") if x%2 == 0 else print("老司机说:x是奇数")
'''
输出结果:
新手说:x是偶数
老司机说:x是奇数
'''
'''
列表推导式格式:[表达式 for 变量 in 旧列表] or [表达式 for 变量 in 旧列表 if 条件]
'''
# 过滤长度小于等于3的人名
names = ['tom', 'lily', 'abc', 'jack', 'steven', 'bob', 'ha']
result = [name for name in names if len(name) > 3]
print(result)
'''
上面这个例子,如果不使用表达式,则要定义函数用for循环实现
def func(names):
newlist = []
for name in names:
if len(names)>3:
newlist.append(name)
return newlist
'''
# 表达式中可以不加条件
newlist3 = [i[-1] for i in newlist2] # 取出newlist2中元组元素的第二个值
# 0~5范围的奇偶元素列表:表达式中可以用双层循环(双层循环产生的元素是笛卡尔积的形式),表达式可以生成元组型元素
newlist2 = [(x, y) for x in range(5) if x % 2 == 0 for y in range(10) if y % 2 != 0]
print(newlist2)
# 由字符串生成的列表推导式:取出字符串中所有的a
s1 = "aabbaaeaacae"
s2 = [s for s in s1 if s=='a']
print(s2)
'''
集合推导式格式:{表达式 for 变量 in 旧系列} or {表达式 for 变量 in 旧系列 if 条件}
'''
# 列表去重并加1
list1 = [1, 2, 1, 3, 5, 2, 1]
set1 = {x+1 for x in list1}
print(set1)
'''
字典推导式格式:与列表和集合的推导式相似
'''
dict1 = {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'D'}
newdict = {value:key for key, value in dict1.items()} # 其中value:key表示要交换key与valeu的位置,for key, value in dict1.items()表示对旧字典进行遍历;
print(newdict)
5.1.2 生成器
通过列表推导式,我们可以直接创建一个列表,然而生成的列表是要占用内存空间的。如果我们创建大量元素的列表,却仅仅需要访问几个元素或多次访问每次访问几个元素,会浪费内存空间。因此,我们在每次调用时产生一个值,这样产生变量占用的内存空间就小多了。在python中这种逐个计算产生数值的机制称为生成器(generator)。
生成器有两种形式:推导式形式的生成器与函数形式的生成器
(1)推导式形式的生成器
推导式形式的生成器与推导式十分相似,如下:
'''
得到这样一串数值[0,3,6,9,12,...,27]
'''
# 如果使用列表推导式方法
newlist = [x*3 for x in range(20)]
print(newlist)
# 创建生成器
g = (x*3 for x in range(20)) # 推导式中的表达式不变,把方括号变为小括号即可
print(type(g)) # 生成器的类型
# 使用生成器产生数值:
print(g.__next__()) # 使用.__next__()方法产生数值;
print(next(g)) # 使用next()函数产生数值;
执行以上例子可以看出:使用生成器时,调用一次产生一个数值,再调用一次产生下一个数值,这就是逐个计算。
值得注意的是,生成器内部是一个列表推导式,它的长度是有限的,如果取值的次数超过了长度限制,将会报错。如下:
# 调用次数超过了表达式的范围,则会出现异常,解决此异常:
g = (x*3 for x in range(20))
while True:
try:
e = next(g)
print(e)
except:
print('元素产生完毕!')
break
(2)函数形式的生成器
在函数中使用yield抛出数值就可以创建一个生成器(函数中只要存在这条语句就是一个生成器)。如下:
# 创建一个生成器函数
def func():
n = 0
while True:
n += 1
yield n # yield的出现使语句执行到这里就执行抛出数值并暂停执行,不接到调用生成器的命令就不会往下执行
g = func() # 用一个变量接收,该变量即成为一个生成器
# 使用生成器产生数值
print(next(g))
与推导式形式的生成器相比,函数式的生成器更方便使用复杂计算规则。比如下面的例子:
# 菲波那契数列的生成:
def fib(length):
a, b = 0, 1
n = 0
while n < length:
yield b
a, b = b, a + b
n += 1
return '没有更多元素了!' # 函数生成器的终止提示信息一般由return完成
g = fib(8)
print(next(g))
# 如果使用函数形式的生成器,可以在知道菲波那契数列递推式的情况下很方便地计算每个元素。然而,如果使用的是推导式形式的生成器,就要使用通项式,或想办法在通项n与元素之间建立关联,这种算法考虑起来是比较复杂的。
函数式的生成器也是逐个计算,由其中的yield关键字的作用,可以很清楚地了解到生成器的执行过程:本质上还是使用循环执行一次产生一个数值;调用一次生成器则循环执行一次,当本次执行遇到yield语句时,阻塞执行并抛出数值;当下一次调用生成器时,接着执行一次循环,并由yield语句阻塞执行并抛出数值。因此,推导式生成器与函数生成器最大的区别其实在于,推导式生成器中常使用for循环,且yield语句是隐式执行的;而函数生成器中两种循环都使用,且yield语句是显式存在的。
推导式生成器调用次数超过了限制会产生异常,函数生成器也一样。由生成器的执行过程可知,生成器抛出异常的根本原因其实是执行循环超过了次数限制。
函数式的生成器是可以通过生成器对函数进行传参的。如下:
# 创建一个生成器函数
def gen():
i = 0
while i < 5:
temp = yield i # 这里使用yield对temp变量进行了赋值
print('temp:', temp)
for x in range(temp): # 这里的temp对for循环长度做了限定
print('-->', x)
i += 1
return '没有更多值了!'
# 创建一个生成器
g = gen()
# 用常规方式调用生成器
print(next(g)) # 根据打印的结果为“0”可知,第一次执行在yield处暂停且抛出的值是0;
print(next(g)) # 当第二次执行时,接着yield后执行,打印出“temp: None”并且报错了。因为yield语句没有对temp进行赋值,temp值为None;而None是不能作为整数的,temp对range(temp)来说是无效参数,因此报错了;
print(next(g)) # 第三次调用,及后面几次调用,都会是一样的结果
g.__next__()
g.__next__()
g.__next__()
# 如果要正常使用这个生成器,可以在用send方法可以对temp传值的情况下调用
print(g.send(None)) # 第一次调用,.send()方法传值必须为None。因为.send()方法是没有指定形参为temp的,之所以知道要传参给temp,是要靠None确定yield语句所在的赋值语句的变量是temp;
print(g.send(2)) # 以后调用就可以传递整数类型的参数。只要保证长度大于等于调用次数,就不会报错
n1 = g.send(5)
print('n1', n1)
(3)生成器的应用
在写协程的时候会使用到生成器,以实现任务的交替执行。(协程与线程的关系相当于线程与进程的关系,后面会介绍)
如下:
# 生成器帮助函数交替执行
def task1(n):
for i in range(n):
print('正在搬第{}块砖!'.format(i))
yield None
def task2(n):
for i in range(n):
print('正在听第{}首歌!'.format(i))
yield None
# 如果不函数定义为生成器则由于代码的顺序执行,无法完成两个函数语句的交替输出
# 采用生成器的方式进行输出,写协程就会采用这种方式,使任务交替执行
g1 = task1(10)
g2 = task2(5)
while True:
try:
g1.__next__()
g2.__next__()
except:
break
5.1.3 迭代器
(1)可迭代对象
认识迭代器之前,我们先要了解一个概念是可迭代对象。可迭代对象是指可迭代类型(Iterable)及其子类型的对象。python中的可迭代类型具体有以下几种:
- 对象类型:生成器;
- 基本类型:元组、列表、集合、字典、字符串。
判断变量是否为可迭代类型,可以使用isinstance()函数:
# isinstance()函数用于判断某个子类型对象是否是某个父类型,即判断两个类型之间是否有继承关系。因此,我们可以判断所创建的变量与可迭代类型Iterable之间是否有继承关系,从而确定变量是否是可迭代类型
from collections.abc import Iterable # 3.8之前版本是form collections import Iterable
list1 = [1, 4, 7, 8, 8]
f = isinstance(list1, Iterable) #判断列表是否可迭代,isinstance()函数用于作判断,括号里第一个参数是要判断的对象,后一个参数是要判断的类型
print(f)
f = isinstance('abc', Iterable)
print(f)
f = isinstance(100, Iterable)
print(f) # 由打印结果可知,整型不是可迭代类型;
g = (x+1 for x in range(10))
f = isinstance(g,Iterable) # 判断生成器是否是可迭代的
print(f)
(2)迭代器
迭代是一种逐个使用集合中元素的访问方式,逐个使用就需要遍历,遍历就需要记住当前访问元素的位置,而迭代器就是记住取出元素位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有元素访问完结束,这期间迭代器只能前进不能后退。
从特征上来看,可以被next()函数调用并不断返回下一个值的对象(或变量)称为迭代器:Iterator。
由迭代器的定义与性质可知:可迭代对象不一定都是迭代器。比如:生成器是一种迭代器,但列表、元组等并不是。
我们可以将可迭代对象变为迭代器,使用的方法函数是iter()。如下:
list1 = [1,2,3,4,5,6]
list1 = iter(list1) #将列表变为iterator
print(next(list1))
print(next(list1))