第九讲
一. 推导式
1. 列表推导式
列表推导式是一种快速生成列表的方式,其形式我们通过案例来讲一下:
比如我们有一个需求,想要生成一个列表[0.5,1.0,1.5,2.0,2.5…10.0],我们可以这样实现:
li = []
for i in range(1,21):
li.append(i/2)
print(li)
通过列表推导式,我们可以将for循环写入列表内,则可以这么写:
li = [i/2 for i in range(1,21)]
print(li)
这样就可以更快速,更方便的生成列表,前面是对于i的处理,后面是循环语句。
多讲个例子,比如我们想随机生成一个列表,里面的整数大于-10,小于10:
import random
li = [random.randint(-10,10) for i in range(10)]
random库需要导入,其中.randint(x,y)是在x和y之间随机取一个整数的意思,需要注意的是,这里是可以取到x和y的,for循环在这里只是充当了一个计数器的作用。
再讲一个例子:
现在有一个字典:
dic = {'name':'Tom','age':'18','gender':'male'}
想抽取其中的元素,变成以下形式的列表:
[key:value,key:value]
代码如下:
li = [i+':'+j for i,j in dic.items()]
需要注意一下,dic.items()
是返回dict_items([(key,value),(key,value)......])
的形式,如果是for i in dic.items()
,i代表的是元组(key,value)
,使用两个参数才会分别代表key和value。
2. 列表推导式和if联用
现在有一个列表:
li = [-7, 2, -2, -3, -5, 10, 5, -1, 10, -8]
我们想要将其中小于0的数字拿出来,平方后组成一个新的数列,可以这么处理:
li = [-7, 2, -2, -3, -5, 10, 5, -1, 10, -8]
li1 = []
for i in li:
if i < 0:
i = i**2
li1.append(i)
print(li1)
如果用列表推导式呢?可以模仿三目运算符的形式:
li = [-7, 2, -2, -3, -5, 10, 5, -1, 10, -8]
li1 = [i**2 for i in li if i < 0]
print(li1)
3. 列表推导式嵌套循环
如果我们单看下面的语句:
li = [i+j for i in '123' for j in 'abc']
print(li)
这个列表里面嵌套了两个for循环,它的运行逻辑并非是将‘123’和‘abc’里面的字符一一对应组合,它相当于这个:
for i in '123':
for j in 'abc':
li.append(i+j)
print(li)
每循环到一个i,都会在这个层级上循环一套j,所以输出结果是有九个元素的。
4. 字典推导式
字典推导式采用{}就可以定义。
举个例子,现在有个列表:
li = ['age','name','gender']
现在有个需求,我们想让列表中的元素对应其下标组成一个字典:
{'age':0,'name':1,'gender':2}
这里我们可以这样写:
dic = {i:li.index(i) for i in li}
print(dic)
个人认为实际情况可能用到的不多,可能用map函数会解决实际情况多一点。
5. 集合推导式
大括号除了能用作字典推导式,也可以用作集合推导式,只在细微处有差别。
比如现在有个需求,随机生成10个1-100之间的数字,并且去重:
import random
s = {random.randint(1,100) for i in range(10)}
print(s)
相当于:
import random
s = set()
for i in range(10):
s.add(random.randint(1,100))
print(s)
6. 没有元组推导式的说法
如果这样写:
tup = (i for i in range(10))
print(tup)
结果是:
<generator object <genexpr> at 0x00000......> # 省略一部分
这是一个generator生成器,之后会讲到。
如果想生成元组,可以这么写:
print(tuple(tup))
这样就可以了。
7. 讲一个有意思的题目
有这样一个式子:
li = [lambda x:x+i for i in range(10)]
print(li[0](10))
print(li[1](10))
print(li[2](10))
大家猜一下打印输出结果为多少?
答案是三个均为19。
接下来需要就这个问题讲一下原理,这里是我借鉴的别人的解释,可以随时复习一下,下面自己解释一遍。
我们必须要知道的是,上面的题目的等价形式是什么:
li = []
for i in range(10):
def func(x):
return x+i
li.append(func)
print(li[0](10))
print(li[1](10))
print(li[2](10))
可一看到,每循环到一个i,都会往列表li里面添加一个函数func,需要注意的是,在定义函数func的局部作用域里并未给i赋值,并且每循环一次只是给列表li里面添加了函数,并未调用函数,所以函数func也不会去外部寻找数值赋给i,在这种情况下,当我们再重新去调用函数的时候,比如这里的li[0](10)
,函数会从函上一级作用域找数值赋给i,这个时候for循环早已完成,i最后的值是9,所以无论我调用列表里的哪个函数,结果都是一样的。
那如何解决这个问题呢?达到我们想要的目的?
很简单,我们再每一次循环的时候给i赋值即可,这么写:
li = []
for i in range(10):
def func(x,i=i):
return x+i
li.append(func)
print(li[0](10))
print(li[1](10))
print(li[2](10))
如果写成推导式,也就是:
li = [lambda x,i=i:x+i for i in range(10)]
print(li[0](10))
print(li[1](10))
print(li[2](10))
这个时候我们也可以借助闭包的思想,还记得闭包的作用吗?保存局部信息不被销毁,我们可以在函数func外面包一层函数:
li = []
for i in range(10):
def outer(i):
def func(x):
return x+i
return func
li.append(outer(i))
print(li[0](10))
print(li[1](10))
print(li[2](10))
只不过这样似乎没法改成推导式了…………
二. 迭代器
1. 迭代和迭代器
迭代:通过for循环遍历对象的每一个元素的过程。
迭代器:是一种可以遍历的对象,里面的元素可以使用next()内置方法一一访问,但是只可以按序访问,不能回溯。
2. 判断对象是否是迭代类型和迭代器
我们可以通过isinstance来判断对象是否是迭代类型和迭代器。
Iterable:可迭代类型
Iterator:迭代器
from collections import Iterable,Iterator
print(isinstance('abc',Iterable))
print(isinstance([1,2,3],Iterable))
print(isinstance(12345,Iterable))
print(isinstance([1,2,3],Iterator))
'''
输出结果依次为:
True
True
False
False
在这里需要说明一下,整型不是可迭代类型,字符串,列表,元组,字典,字节,集合是可迭代类型,但是他们并不是迭代器,迭代器是可以调用next()方法来一一访问的,你可以把迭代器想象成一个压缩包,我们可以通过iter()方法将可迭代对象编程迭代器,这样可以节约内存。
3. _ iter_()和iter()
假设有一个列表li,我们想把它编程迭代器,可以这么写:
li = [1,2,3]
li1 = li.__iter__()
# 或者
li1 = iter(li) # 这个内置方法比较常用
需要注意的是,这两种方法并不改变原列表的类型,需要用一个新变量来重新接收。
4. _ next _()和next()
当我们已经有一个迭代器的时候,拿上一个例子来说明,可以通过以下方式一一访问:
print(li1.__next__())
print(li1.__next__())
print(li1.__next__())
print(li1.__next__())
或者
print(next(li1))
print(next(li1))
print(next(li1))
print(next(li1))
一次输出只显示一个元素,多个输出按顺序访问元素,输出次数大于元素数量的时候,会显示StopIteration
5. 迭代器不支持索引取值
迭代器不可以像列表那样按照索引取值,会报错。
三. 生成器
1. 什么是生成器?
有时候,序列或集合内元素数量巨大,如果全部制造出来放入内存将对计算机造成巨大压力。如果采用某种算法,需要用到哪个元素就推算出哪个,然后在循环的过程中不断推算出后续的元素,这样就可以节省大量空间,生成器我们称之为generator.
2. 生成器的构建方式(圆括号和yield)
目前学到两种方式:
- 圆括号推导式;
- yield函数定义
先来讲第一个,第一个在之前其实已经遇到过了,比如下面代码:
g = (i for i in range(10))
这样就已经构成了一个生成器g,生成器和迭代器一样可以用next()方法逐一访问,并且超出范围会抛出异常。
那如何用yield关键字构建生成器?
比如以下代码:
def test(x):
n = 0
while n < x:
yield n
n += 1
res = test(10)
如果只构建一个test(x)函数,那么test仍旧是函数,当我们赋予变量x具体值,在函数中运用到关键字yield,并用一个变量res来接收这个函数的调用,那么res就是一个生成器。
yield本身就有“产出”的意思,我们在这里需要拿其和return做一个对比:
- 当函数遇到yield关键字时,函数会暂停,并将对象返回出去,下次会继续从上次暂停的地方开始执行;
- 当函数遇到return关键字时,函数直接返回该对象。
通过一个其它例子,我们来看更其它场景的过程剖析:
def test():
print('---1---')
while True:
a = yield 2
print(a)
res = test()
print(next(res))
print(next(res))
上述代码已经把res变成了一个生成器,它的执行结果为:
---1---
2
None
2
我们需要解释一下这个结果:
- 需要注意的是,虽然test函数里面先设置了一个print输出,由于有yield关键字,所以test函数并不会执行,也就是说
res = test()
命令不会导致---1---
输出,当使用next方法时,test函数开始执行,输出---1---
后开始进入while循环,遇到yield返回一个2后,程序停止运行,并且程序并没有执行给a的赋值操作,到此为止,是输出结果的前两行是来源于第一个print(next(res))
的操作。 - 第二个
print(next(res))
的操作会从上一次next方法结束的地方开始运行,也就是给a赋值的操作,需要注意的是,此时整型2在刚才已经被yield给生成出去了,所以并没有元素留下来可以给a赋值了,因而打印输出a结果为None。在打印输出a之后进入下一次while循环,一直到yield返回整型2程序再次停止。
3. send()方法
采用send方法可以给上一个程序停止的地方(yield返回一个值的地方)输送一个数据,这样不会可以解决数据空白的问题,同时接着往下运行程序。
我们在这里接着上面的例子讲一下生成器里的send()方法:
def test():
print('---1---')
while True:
a = yield 2
print(a)
res = test()
print(next(res))
print(res.send(4))
结果为:
---1---
2
4
2
解释一下:
这里需要解释的就是输出的a不再是None,因为我们通过send方法给上一个yield返回值的地方输送了一个整型4,所以给a赋值了4,程序继续往下运行。
强调一下send方法使用既不能放在开头,也不能放在结尾,否则报错。