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

liftword4个月前 (01-29)技术文章41

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。

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

相关文章

列表常见操作—修改添加删除元素(列表中元素的增加、删除、修改、访问的方法)

在实际工作中,我们创建的大多数列表是动态列表,这就意味着在列表创建后,我们是可以根据需要动态增加、修改和删除列表中的元素的。01—修改列表元素修改列表元素的语法与访问列表元素的语法类似,如果要修改列表...

Python之列表list(python中list怎么用)

Python 内置了一种名为列表(list)的数据类型。list 是一种有序的集合,具备随时添加和删除其中元素的便利性。例如,若要列出班里所有同学的名字,就可以借助 list 来表示:classmat...

python中元组,列表,字典,集合删除项目方式的归纳

九三,君子终日乾乾,夕惕若,厉无咎。在使用python过程中会经常遇到这四种集合数据类型,今天就对这四种集合数据类型中删除项目的操作做个总结性的归纳。列表(List)是一种有序和可更改的集合。允许重复...

list列表基本操作(list的基本操作)

【实验目的】1、掌握list列表的基本操作【实验原理】列表是Python中最基本的数据结构,列表是最常用的Python数据类型,列表的数据项不需要具有相同的类型。列表中的每个元素都分配一个数字 - 它...

Python基础教程:在Python中访问列表元素详解

欢迎你来到站长在线的站长学堂学习Python知识,本文分享的是《在Python中访问列表元素》。本知识点主要内容有:使用print()函数输出内容、通过索引(index)的方式访问列表元素、输出每日一...