Python:列表推导式和字典推导式
1 列表推导式和序列元素筛选
在Python语言中,要筛选序列中的元素,通常最简单的方法是使用列表推导式(list comprehension)。例如:
mylist = [1, 4, -5, 10, -7, 2, 3, 1]
pos_list = [n for n in mylist if n > 0]
print(pos_list) # [1, 4, 10, 2, 3, 1]
neg_list = [n for n in mylist if n < 0]
print(neg_list) # [1, 4, 10, 2, 3, 1]
使用列表推导式的一个潜在缺点就是如果原始输入非常大的话,这么做可能会产生一个庞大的结果。如果这是你需要考虑的问题,那么可以使用生成器表达式通过迭代的方式产生筛选的结果。例如:
pos = (n for n in mylist if n > 0)
print(pos) # <generator object <genexpr> at 0x101cbfeb0>
for x in pos:
print(x)
# 1
# 4
# 10
# 2
# 3
# 1
除了筛选,列表推导式和生成器表达式也具有同时对数据做转换的能力,例如:
mylist = [1, 4, -5, 10, -7, 2, 3, -1]
import math
transformed_list = [math.sqrt(n) for n in mylist if n > 0]
print(transformed_list)
# [1.0, 2.0, 3.1622776601683795, 1.4142135623730951, 1.7320508075688772]
列表推导式也支持布尔值,比如如果我们想判断原列表里面的元素的奇偶性,可以这样:
mylist = [1, 4, -5, 10, -7, 2, 3, 1]
is_odd =[ x % 2 == 1 for x in mylist]
print(is_odd)
# [True, False, True, False, True, False, True, True]
让我们回到序列元素筛选的话题。关于筛选的数据,有一种情况是用新值替换掉不满足标准的值,而不是丢弃它们。例如,除了要找到正整数之外,我们也许还希望在指定的范围内将不满足要求的值替换掉。通常,这可以通过将筛选条件移到一个条件表达式中来轻松实现。就像下面这样:
mylist = [1, 4, -5, 10, -7, 2, 3, -1]
clip_neg = [n if n > 0 else 0 for n in mylist]
print(clip_neg) # [1, 4, 0, 10, 0, 2, 3, 0]
clip_pos = [n if n < 0 else 0 for n in mylist]
print(clip_pos) # [0, 0, -5, 0, -7, 0, 0, -1]
这里需要注意一下,如果是在列表推导式中使用
if ... else ...
语句,那么if ... else ...
语句需要放到for ... in ...
语句之前,不然如果像下面这样错误地放到后面:clip_pos = [n for n in mylist if n < 0 else 0 ] # 错误写法
则会报下列语法错误:
clip_pos = [n for n in mylist if n < 0 else 0 ] ^ SyntaxError: invalid syntax
有时候筛选的标准没法简单地表示在列表推导式或生成器表达式中。比如,假设筛选过程设计异常处理或其他一些复杂的细节。基于此,可以将处理筛选逻辑的代码放到单独的函数中,然后使用内建的filter()
函数处理。示例如下:
values = ['1', '2', '-3', '-', '4', 'N/A', '5']
def is_int(val):
try:
x = int(val)
return True
except ValueError:
return False
ivals = list(filter(is_int, values))
print(ivals)
# ['1', '2', '-3', '4', '5']
除了filter()
之外,Python中还有与其搭配使用的map()
函数,这两个函数的来源和函数式编程有关,大家可以参见我的博客《SICP: 层次性数据和闭包性质(Python实现) 》。需要注意的是,filter()
和map
函数都创建了一个迭代器,因此如果我们想要的是列表形式的结果,请确保加上list()
。此外,filter()
和map
函数的第一个参数都是函数名(也可以支持匿名函数),第二个参数才是对应的序列,这里和往sorted()
函数中传序列和回调函数的顺序相反,这一点要特别注意。
另一个值得一提的筛选工具是itertools.compress()
,它接受一个可迭代对象以及一个布尔选择器序列作为输入。输出时,它会给出所有在相应的布尔选择器中为True
的可迭代对象元素。如果想把对一个序列的筛选结果施加到另一个相关的序列上时,这就会非常有用。例如,假设有以下两列数据:
addresses = [
'5412 N CLARK',
'5148 N CLARK',
'5800 E 58TH',
'2122 N CLARK',
'5645 N RAVENSWOOD',
'1060 W ADDISON',
'4801 N BROADWAY',
'1039 W GRANVILLE',
]
counts = [0, 3, 10, 4, 1, 7, 6, 1]
现在我们想构建一个地址列表,其中相应的count
值要大于5。下面是我们可以尝试的方法:
from itertools import compress
more5 = [n > 5 for n in counts]
print(more5)
# [False, False, True, False, False, True, True, False]
print(list(compress(addresses, more5)))
# ['5800 E 58TH', '1060 W ADDISON', '4801 N BROADWAY']
这里的关键在于首先创建一个布尔序列,用来表示哪个元素可满足我们的条件。然后compress()
函数挑选出满足布尔值为True
的相应元素。
同filter()
函数一样,正常情况下compress()
会返回一个迭代器。因此,如果需要的话,得使用list()
将结果转为列表。
2 字典推导式和字典子集提取
如果我们想创建一个字典,其本身是另一个字典的子集呢?与上面类似的,我们利用字典推导式(dictionary comprehension)即可轻松解决。例如:
prices = {
'ACME': 45.23,
'AAPL': 612.78,
'IBM': 205.55,
'HPQ': 37.20,
'FB': 10.75
}
# Make a dictionary of all prices over 200
p1 = { key: value for key, value in prices.items() if value > 200}
print(p1) # {'AAPL': 612.78, 'IBM': 205.55}
# Make a dictionary of tech stocks
tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'}
p2 = { key: value for key, value in prices.items() if key in tech_names}
print(p2) # {'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.2}
大部分可以用字典推导式解决的问题也可以通过创建元组序列然后将它们传给dict()
函数来完成。例如:
p1 = dict([(key, value) for key, value in prices.items() if value > 200])
print(p1) # {'AAPL': 612.78, 'IBM': 205.55}
事实上,上面列表推导式两周的[]
都可以省略,如下所示:
p1 = dict((key, value) for key, value in prices.items() if value > 200)
print(p1) # {'AAPL': 612.78, 'IBM': 205.55}
但是字典推导式的方案更加清晰,而且实际运行起来也要快很多(以本例中的字典prices
来测试,效率要高2倍多)。
有时候会有多种方法来完成同一件事情。例如,字典推导式的第二个例子还可以重写成:
# Make a dictionary of tech stocks
tech_names = { 'AAPL', 'IBM', 'HPQ', 'MSFT'}
p2 = { key:prices[key] for key in prices.keys() & tech_names}
print(p2) # {'AAPL': 612.78, 'HPQ': 37.2, 'IBM': 205.55}
# 这里有个值得注意的地方就是遍历的顺序和前面字典推导式第二个例子顺序不同。
# 遍历prices.keys()会默认按照字典key的字典序遍历,
# 但按照prices.items()遍历就按照原本的顺序遍历了
但是,计时测试表明这种解决方案几乎要比第一种慢上1.6倍。可见如果需要考虑性能因素,那么通常都需要花一点时间来研究它。有关计时和性能分析方面的信息,请参见我的博客《Python:对程序做性能分析及计时统计 》。
参考
- [1] Beazley D, Jones B K. Python cookbook: Recipes for mastering Python 3[M]. " O'Reilly Media, Inc.", 2013.