python散装笔记—21: 列表推导式(使用列表推导式)

liftword3个月前 (01-29)技术文章24

Python 中的列表推导式是一种简洁的语法结构。通过对列表中的每个元素应用函数,它们可以用来从其他列表生成列表。下文将解释并演示这些表达式的使用。

一: 列表推导式

列表推导式通过对可迭代元素的每个元素应用表达式来创建新列表。最基本的形式是

[  for  in  ]

还有一个可选的 “if ”条件:

[  for  in  if  ]

如果(可选) 的值为 true,则会将 中的每个 插入到 中。所有结果都会在新列表中一次性返回。生成器表达式的求值过程比较缓慢,而列表推导式会立即求值整个迭代器,消耗的内存与迭代器的长度成正比。

创建平方整数列表

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 ]

对于 中的每个 ;如果 评估为 True,则将 (通常是 的函数)添加到返回的列表中。

例如,这可用于从整数序列中只提取偶数:

[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](其中 ec 是以 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。

列表推导式只生成一次列表,并将每个项目复制一次(从其原始位置复制到结果列表)。

相关文章

Python 中删除列表元素的三种方法

列表基本上是 Python 中最常用的数据结构之一了,并且删除操作也是经常使用的。那到底有哪些方法可以删除列表中的元素呢?这篇文章就来总结一下。一共有三种方法,分别是 remove,pop 和 del...

在 Python 中从列表中删除换行符的多种方法

在这里,我们将学习使用不同的方法从 python 的列表中删除换行符。换行符使用特殊字符“\n”指定。每当我们使用字符“\n”时,它都会自动生成一个新行。在 Python 中,有许多内置函数可用于从列...

玩转Python—列表使用教程(python列表讲解)

上一讲给大家介绍了Python的列表,今天继续给大家介绍Python中列表的使用。1.列表的元素的赋值#实例 >>>num=[1,2,3,4,5,6,7,7,8,8,9] >...

Python中的列表和元组,你了解多少?

0列表和元组都是Python中的序列类型,它们可以存存储多个值,并且可以通过索引和切片来访问元素。列表和元组的区别在于,列表是可变的,也就是说,可以在列表中添加、删除或修改元素,而元组是不可变的,也就...

4.Python列表、元组、字典与集合(在python中列表,元组,字典的区别)

在Python编程中,列表、元组、字典和集合是四种非常重要的数据结构,它们各自扮演着不同的角色,为数据的存储和处理提供了极大的便利。特别是在量化交易中,这些数据结构的应用尤为关键:列表(List):主...

全面解析 Python 列表(python列表教程)

Python 列表(list) 是开发中 最常用 的 Python 数据结构之一。列表支持各种 Python 数据类型,也能方便与 JSON 数据进行转换。前后端交互中,列表扮演重要角色。虽然列表灵活...