Python学习笔记——高级迭代(推导式comprehension、生成器generator)

我们的程序往往会在循环上花大量时间。
优化循环有两种方法:①改进循环语法(从而更容易地建立循环)②改进循环的执行(使循环更快地执行)
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循环可以抽象为下面的模式:

  1. 创建一个新列表/字典/集合
  2. for循环遍历一个列表/字典/集合,并对其中的每项数据做处理
  3. 然后将处理后的数据项添加到一个新列表/字典/集合中

推导式(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响应(一个对象),该对象有urlcontentstatus_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循环两个代码轮流执行一样(每次轮换时在二者之间传递数据)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值