我们的程序往往会在循环上花大量时间。
优化循环有两种方法:①改进循环语法(从而更容易地建立循环)②改进循环的执行(使循环更快地执行)
Python实现这两种方法的语言特性是推导式(comprehension)
背景问题
给出一个字典,键值对代表起飞时刻和目的地:
问题1:将时间转化为AM/PM形式,然后将所有时刻保存在一个列表time中
问题2:将时间转化为AM/PM形式,将目的地转化为仅首字母大写形式,然后保存在一个新的字典flights2中
问题3:将字典flights2的键值对反转,键key为目的地,值value为起飞时刻的列表,保存在字典departureTime中
flights = {'9:35':'FREEPORT',
'9:55':'WEST END',
'10:45':'TREASURE CAY',
'11:45':'ROCK SOUND',
'12:00':'TREASURE CAY',
'17:00':'FREEPORT',
'17:55':'ROCK SOUND',
'19:00':'WEST END'}
从for循环到推导式(comprehension)
将数据从一种形式转换为另一种形式时,使用的for循环可以抽象为下面的模式:
- 创建一个新列表/字典/集合
- for循环遍历一个列表/字典/集合,并对其中的每项数据做处理
- 然后将处理后的数据项添加到一个新列表/字典/集合中
推导式(comprehension)用于完成这一类工作
- 推导式是标准for循环的等价写法,但代码量更少
- 推导式比等价的for循环代码执行得更快,解释器已优化为尽可能快地运行推导式
- 推导式可以放在代码中几乎任何地方(作为表达式),而等价for循环代码无法作为表达式的一部分
- 用推导式创建一个新列表,称为列表推导式(listcomp)
用推导式创建一个新字典,称为字典推导式(dictcomp)
用推导式创建一个新字典,称为集合推导式(setcomp)
没有“元组推导式”,因为元组不符合这种“在循环中不断新增数据项”的模式 - 如何分辨不同的推导式:
for循环写在括号内,就是推导式
推导式写在中括号[]
内,就是列表推导式
推导式写在大括号{}
内(有冒号:
),就是字典推导式
推导式写在大括号{}
内(无冒号),就是集合推导式
推导式写在小括号()
内,就是生成器
问:什么时候使用列表推导式呢?
- 从现有新列表/字典/集合生成一个新列表/字典/集合,问问自己能否把循环转换成一个等价的推导式,这样效率更高
- 如果要创建“临时的”新列表/字典/集合(只使用一次,然后就丢弃),问问自己是否能改用嵌套的列表推导式,这样能避免在代码中引入(只使用一次的)临时变量。
列表推导式
问题1:将时间转化为AM/PM形式,然后将所有时刻保存在一个列表time中
from pprint import pprint
from datetime import datetime
def convert2ampm(time24: str) -> str:
return datetime.strptime(time24, '%H:%M').strftime('%I:%M%p')
flights = {'9:35':'FREEPORT',
'9:55':'WEST END',
'10:45':'TREASURE CAY',
'11:45':'ROCK SOUND',
'12:00':'TREASURE CAY',
'17:00':'FREEPORT',
'17:55':'ROCK SOUND',
'19:00':'WEST END'}
times = []
for time in flights:#对于字典,默认迭代key值
times.append(convert2ampm(time))
pprint(times)
其中,处理核心工作的for循环为:
...
times = []
for time in flights:#对于字典,默认迭代key值
times.append(convert2ampm(time))
...
================= RESTART: C:/Users/13272/Desktop/flights.py =================
['05:55PM',
'09:55AM',
'10:45AM',
'07:00PM',
'05:00PM',
'12:00PM',
'09:35AM',
'11:45AM']
等价的列表推导式:(结果与上面相同)
times = [convert2ampm(time) for time in flights]
map函数可以达到等价的效果
>>> list(map(convert2ampm,flights.keys()))
['09:35AM', '09:55AM', '10:45AM', '11:45AM', '12:00PM', '05:00PM', '05:55PM', '07:00PM']
map效率和等价的列表推导式的区别在于
map返回的是生成器,所以map对于大容量的操作,不会导致内存爆掉;
列表推导式则可能爆内存,但也有解决方法:用()
代替[]
,变为生成器推导式,同样不会导致内存爆掉
字典推导式
问题2:将时间转化为AM/PM形式,将目的地转化为仅首字母大写形式,然后保存在一个新的字典flights2中
核心for循环:
...
flights2 = {}#空字典
for k,v in flights.items():
flights2[convert2ampm(k)]=v.title()
...
================= RESTART: C:/Users/13272/Desktop/flights.py =================
{'05:00PM': 'Freeport',
'05:55PM': 'Rock Sound',
'07:00PM': 'West End',
'09:35AM': 'Freeport',
'09:55AM': 'West End',
'10:45AM': 'Treasure Cay',
'11:45AM': 'Rock Sound',
'12:00PM': 'Treasure Cay'}
等价的字典推导式:(结果与上面相同)
flights2 = {convert2ampm(k):v.title() for k,v in flights.items()}
推导式的过滤器
问题3:将字典flights2的键值对反转,键key为目的地,值value为起飞时刻的列表,保存在字典departureTime中
先考虑如何获取一个目的地的起飞时刻列表
首先,只考虑获取’Freeport’对应的起飞时刻列表
...
departureTime_freeport=[]
for k,v in flights2.items():
if v=='Freeport':
departureTime_freeport.append(k)
...
================= RESTART: C:/Users/13272/Desktop/flights.py =================
['05:00PM', '09:35AM']
注意,这里在for循环中加入了if
条件判断语句,等效于推导式中的过滤器
(结果与上面相同)
departureTime_freeport=[k for k,v in flights2.items() if v=='Freeport']
另外,为了使推导式更易读,这里将推导式写为多行(别忘了,代码出现在一对括号[]
里, Python的“行末即语句结束”规则会临时关闭,因此可以写为多行)
departureTime_freeport=[k
for k,v in flights2.items()
if v=='Freeport']
用同样方法处理所有目的地
知道了如何得出单个目的地的起飞时刻列表,就可以对每个目的地作同样的处理
其中,set(flights2.values())
表示所有目的地的集合
...
departureTime={}
for dest in set(flights2.values()):#对每个目的地作同样的处理
departureTime[dest]=[k for k,v in flights2.items() if v==dest]
...
================= RESTART: C:/Users/13272/Desktop/flights.py =================
{'Freeport': ['05:00PM', '09:35AM'],
'Rock Sound': ['05:55PM', '11:45AM'],
'Treasure Cay': ['12:00PM', '10:45AM'],
'West End': ['07:00PM', '09:55AM']}
等等…又一个推导式
上面的代码已经让人觉得很神奇了…
然而,这段代码同样符合典型的推导式模式,可以把for循环重写为推导式
(结果与上面相同)
departureTime={dest:[k for k,v in flights2.items() if v==dest]
for dest in set(flights2.values())}
这时一个复杂的推导式:外部字典推导式包含了一个内部列表推导式
可以把推导式放在代码中几乎任何地方(而for循环则无法作为表达式的一部分)
生成器generator
- 将列表推导式的
[]
改为()
,就得到了生成器
[x*2 for x in [1,2,3]]
得到一个列表,
(x*2 for x in [1,2,3])
得到一个生成器 - 生成器用于送入迭代器,并被迭代处理
简单来说,就是用于实现“一边循环一边计算” - 列表推导式和生成器会生成相同的结果,但它们的行为完全不同
- 使用列表推导式的循环,需要等待,一次性输出
使用列表推导式:必须等待整个列表生成,然后返回完整的列表,才能继续进行其他工作 - 使用生成器的循环,一边循环一边输出
使用生成器:生成器一次生成一个数据,且一旦生成数据,就会释放这个数据,等待使用数据项的代码会立即执行
>>> for i in [x*2 for x in [1,2,3]]:
print(i,end=' ')
2 4 6
#推导式完成之前,for循环不会处理列表中的数据,"2 4 6"一次性打印
>>> for i in (x*2 for x in [1,2,3]):
print(i,end=' ')
2 4 6
#for循环立即可以使用生成的数据,然后进入下一次迭代,"2 4 6"逐个打印
数据项很少时,两种方式没有区别
不过假设你的列表推导式包括1000万个数据项:
- 使用列表推导式①必须等待这个列表推导式处理这1000万个数据项,然后才能做其他事情②列表推导式可能会耗尽计算机所有内存,这时解释器会终止
- 使用生成器:①无需等待:生成器每生成一个数据项后立即释放,等待使用数据项的代码会立即执行②就算要生成1000万个数据项,解释器也只需要一个数据项的内存
生成器函数:将生成器封装在函数中
- 可以将生成器封装在函数中,称为生成器函数
- 生成器函数的行为:与生成器相同
生成器函数每处理完一项数据,会把结果传回调用它的代码(多为for循环),然后挂起等待;for循环执行一次后,进入下一次迭代,再次从挂起状态唤起生成器函数… - 生成器函数的使用位置:与生成器相同
- 生成器函数使用
yield
语句(而不用return
),从而支持于生成器类似的“一边循环一边计算”的效果
用例
requests
库允许你通过程序与Web交互(使用指令pip install requests
安装库)
requests.get(URL)
返回值是对应URL的web响应(一个对象),该对象有url
、content
、status_code
等属性
1.使用列表推导式
下面提供含有3个URL的元组,分别对每个URL调用requests.get(URL)
,获得响应对象的列表[requests.get(url) for url in urls]
迭代此列表,打印列表中每个响应对象的页面字节数、HTTP状态码和所使用的URL
import requests
urls = ('https://www.baidu.com/','https://www.icourse163.org/','https://mail.qq.com/')
for resp in [requests.get(url) for url in urls]:
print(len(resp.content), '->', resp.status_code, '->', resp.url)
================= RESTART: C:\Users\13272\Desktop\example.py =================
2443 -> 200 -> https://www.baidu.com/
74134 -> 200 -> https://www.icourse163.org/
14489 -> 200 -> https://mail.qq.com/
需要等待列表推导式完成后,一次性输出结果
2.使用生成器
将列表推导式的[]
换成()
,就得到了生成器
for resp in (requests.get(url) for url in urls):
print(len(resp.content), '->', resp.status_code, '->', resp.url)
最终输出结果相同
但是,可以发现结果是逐个依次输出的(而不是之前的一次性输出)
3.使用生成器函数
用生成器函数封装生成器,可以隐藏复杂性(使用时无需关心底层实现)
将gen_from_urls生成器函数提供给他人使用时,他们只需要知道:向gen_from_urls提供含有多个URL的元组,gen_from_urls会对每个URL返回
(大小,状态码,URL)
,且能够被迭代
使用生成器函数的等价代码
import requests
def gen_from_urls(urls: tuple) -> tuple:
"""传入URL的元组,对每个URL分别返回(大小,状态码,URL)元组"""
for resp in (requests.get(url) for url in urls):
yield len(resp.content), resp.status_code, resp.url
urls = ('https://www.baidu.com/','https://www.icourse163.org/','https://mail.qq.com/')
for resp_len,status,url in gen_from_urls(urls):#使用生成器函数,代码更友好
print(resp_len, '->', status, '->', url)
在生成器函数gen_from_urls中,使用的是yield
而不是return
yield
帮助实现了生成器函数“一边循环一边计算”的行为:- 生成器函数gen_from_urls每处理完
urls
中的一项数据,会把结果(resp_len,status,url)
传回调用它的代码,然后挂起等待; - for循环接收
(resp_len,status,url)
,执行一次print
,然后进入下一次迭代; - 下一次迭代时,for循环再次从挂起状态唤起生成器函数gen_from_urls,它处理
urls
中的下一项数据并传回结果,然后再次挂起 - 最终,就好像这生成器函数和for循环两个代码轮流执行一样(每次轮换时在二者之间传递数据)