python散装笔记—21: 列表推导式(使用列表推导式)
Python 中的列表推导式是一种简洁的语法结构。通过对列表中的每个元素应用函数,它们可以用来从其他列表生成列表。下文将解释并演示这些表达式的使用。
一: 列表推导式
列表推导式通过对可迭代元素的每个元素应用表达式来创建新列表。最基本的形式是
[ for in ]
还有一个可选的 “if ”条件:
[ for in if ]
如果(可选)
创建平方整数列表
squares = [x * x for x in (1, 2, 3, 4)]
# squares: [1, 4, 9, 16]
for 表达式将 x 依次设置为 (1, 2, 3, 4) 中的每个值。表达式 x * x 的结果会追加到一个内部列表中。完成后,内部列表将分配给变量 squares。
除了速度上的提升(如这里所解释的),列表推导式大致等同于下面的 for 循环:
squares = []
for x in (1, 2, 3, 4):
squares.append(x * x)
# squares: [1, 4, 9, 16]
应用于每个元素的表达式可以根据需要尽可能复杂:
# 获取字符串中的大写字符列表
[s.upper() for s in "Hello World"]
# ['H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D']
# 删除列表中字符串元素末尾的逗号
[w.strip(',') for w in ['these,', 'words,,', 'mostly', 'have,commas,']]
# ['these', 'words', 'mostly', 'have,commas']
# 更合理地组织单词中的字母 - 按字母顺序排列
sentence = "Beautiful is better than ugly"
["".join(sorted(word, key = lambda x: x.lower())) for word in sentence.split()]
# ['aBefiltuu', 'is', 'beertt', 'ahnt', 'gluy']
else else 可以用于 list 推导式结构,但要注意语法。if/else 子句应在 for 循环之前使用,而不是之后:
# 创建 apple 中的字符列表,用 “*”替换非元音
# 例如 - 'apple' --> ['a', '*', '*', '*' ,'e']
[x for x in 'apple' if x in 'aeiou' else '*']
# SyntaxError: invalid syntax
# 当同时使用 if/else 时,应将它们放在循环之前
[x if x in 'aeiou' else '*' for x in 'apple']
# ['a', '*', '*', '*', 'e']
请注意,这使用了不同的语言结构,即条件表达式,而条件表达式本身并不是推导式语法的一部分。而 for...in 后面的 if 是列表推导式的一部分,用于从源可迭代元素中过滤 元素。
双迭代
双迭代的顺序[... for x in ... for y in ...] 要么是自然的,要么是违反直觉的。经验法则是遵循等效的 for 循环:
def foo(i):
return i, i + 0.5
for i in range(3):
for x in foo(i):
print(str(x))
这就变成了
[str(x) for i in range(3) for x in foo(i) ]
这可以压缩成一行,如[[str(x) for i in range(3) for x in foo(i)]。
原地突变和其他副作用
在使用列表推导式之前,请先了解为副作用而调用的函数(突变函数或就地函数)(通常返回 None)与返回有趣值的函数之间的区别。
许多函数(尤其是纯函数)只是接收一个对象并返回某个对象。就地函数会修改现有对象,这就是所谓的副作用。其他例子包括输入和输出操作,如打印。
list.sort()对列表进行就地排序(即修改原始列表),并返回 None。因此,它在列表理解中无法正常工作:
[x.sort() for x in [[2, 1], [4, 3], [0, 1]]]
# [None, None, None]
相反,sorted() 返回一个已排序的list,而不是就地排序:
[sorted(x) for x in [[2, 1], [4, 3], [0, 1]]]
# [[1, 2], [3, 4], [0, 1]]
可以对副作用(如 I/O 或就地函数)使用理解式。然而,for 循环通常更具可读性。虽然这在 Python 3:
[print(x) for x in (1, 2, 3)]
而不是使用
for x in (1, 2, 3):
print(x)
random.randrange() 的副作用是改变随机数生成器的状态,但它也会返回一个有趣的值。此外,next() 可以在迭代器上调用。
下面的随机数生成器并不纯粹,但却很合理,因为每次对表达式进行求值时,随机数生成器都会重置:
from random import randrange
[randrange(1, 7) for _ in range(10)]
# [2, 3, 2, 1, 1, 5, 2, 4, 3, 5]
列表推导式中的空白空间
更复杂的列表推导式理解可能会达到不希望达到的长度,或者可读性变差。虽然在示例中不常见,但也可以像这样将列表推导式理解分成多行:
[
x for x
in 'foo'
if x not in 'bar'
]
二: 条件列表推导式
给定一个列表推导式,您可以附加一个或多个 if 条件来过滤值。
[ for in if ]
对于
例如,这可用于从整数序列中只提取偶数:
[x for x in range(10) if x % 2 == 0]
# Out: [0, 2, 4, 6, 8]
上述代码相当于
even_numbers = []
for x in range(10):
if x % 2 == 0:
even_numbers.append(x)
print(even_numbers)
# Out: [0, 2, 4, 6, 8]
此外,[e for x in y if c](其中 e 和 c 是以 x 为单位的表达式)形式的条件列表推导式理解等价于list(filter(lambda x: c, map(lambda x: e, y)))。
尽管结果相同,但要注意的是,前一个示例比后一个快了近 2 倍。对于那些好奇的人,这是对原因的一个很好的解释。
请注意,这与...if...else... 条件表达式(有时称为三元表达式)完全不同,您可以将其用于列表推导式的“<表达式>”部分。请看下面的示例:
[x if x % 2 == 0 else None for x in range(10)]
# Out: [0, None, 2, None, 4, None, 6, None, 8, None]
这里的条件表达式不是过滤器,而是确定列表项所用值的运算符:
if else
如果将它与其他运算符结合起来,就会更加明显:
[2 * (x if x % 2 == 0 else -1) + 1 for x in range(10)]
# Out: [1, -1, 5, -1, 9, -1, 13, -1, 17, -1]
上面的代码相当于:
numbers = []
for x in range(10):
if x % 2 == 0:
temp = x
else:
temp = -1
numbers.append(2 * temp + 1)
print(numbers)
# Out: [1, -1, 5, -1, 9, -1, 13, -1, 17, -1]
我们可以将三元表达式和 if 条件结合起来。三元运算符对过滤后的结果起作用:
[x if x > 2 else '*' for x in range(10) if x % 2 == 0]
# Out: ['*', '*', 4, 6, 8]
仅靠三元运算符无法实现同样的效果:
[x if (x > 2 and x % 2 == 0) else '*' for x in range(10)]
# Out:['*', '*', '*', '*', 4, '*', 6, '*', 8, '*']
三: 使用条件子句避免重复和高消耗的操作
请看下面的推导式清单:
>>> def f(x):
... import time
... time.sleep(.1) # 模拟资源高消耗的功能
... return x**2
>>> [f(x) for x in range(1000) if f(x) > 10]
[16, 25, 36, ...]
这样,在 1000 个 x 值中,需要两次调用 f(x):一次用于生成值,另一次用于检查 if 条件。如果 f(x) 是一个特别高消耗的操作,就会对性能产生重大影响。更糟糕的是,如果调用 f() 会产生副作用,结果可能会出人意料。
取而代之的方法是,通过生成一个中间可迭代函数(生成器表达式),对 x 的每个值都只评估一次昂贵的操作,如下所示:
>>> [v for v in (f(x) for x in range(1000)) if v > 10]
[16, 25, 36, ...]
或使用内置的地图等效功能:
>>> [v for v in map(f, range(1000)) if v > 10]
[16, 25, 36, ...]
另一种方法可以使代码更具可读性,那就是将部分结果(上一示例中的 v)放入可迭代器(如列表或元组)中,然后对其进行迭代。由于 v 将是可遍历器中的唯一元素,因此我们现在只需引用一次慢函数的输出结果即可:
[v for x in range(1000) for v in [f(x)] if v > 10]
[16, 25, 36, ...]
在实际应用中,代码的逻辑可能会更加复杂,因此保持代码的可读性非常重要。一般来说,建议使用独立的生成器函数,而不是复杂的单行代码:
>>> def process_prime_numbers(iterable):
... for x in iterable:
... if is_prime(x):
... yield f(x)
...
>>> [x for x in process_prime_numbers(range(1000)) if x > 10]
[11, 13, 17, 19, ...]
另一种防止多次计算 f(x) 的方法是在 f(x) 上使用 @functools.lru_cache()(Python 3.2+) 装饰器。这样,由于 f 对输入 x 的输出已经计算过一次,所以第二次调用原始列表推导式函数的速度将和查字典一样快。这种方法使用 memoization 来提高效率,与使用生成器表达式相当。
假设要将一个列表
l = [[1, 2, 3], [4, 5, 6], [7], [8, 9]]
其中一些方法可以是
reduce(lambda x, y: x+y, l)
sum(l, [])
list(itertools.chain(*l))
然而,列表推导式将提供最佳的时间复杂性。
[item for sublist in l for item in sublist]
当有 L 个子列表时,基于 + 的快捷方式(包括在求和中的隐含使用)必然是 O(L^2) -- 随着中间结果列表不断变长,每一步都会分配一个新的中间结果列表对象,并且必须复制前一个中间结果中的所有项(以及在最后添加的一些新项)。因此(为简单起见,在不失一般性的前提下),假设有 L 个子列表,每个列表有 I 个条目:第一个 I 个条目被来回复制 L-1 次,第二个 I 个条目被复制 L-2 次,以此类推;复制的总次数是 x 的总和的 I 倍,x 从 1 到 L 不包括在内,即 I * (L**2)/2。
列表推导式只生成一次列表,并将每个项目复制一次(从其原始位置复制到结果列表)。