MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

深入Python的列表推导式

2021-10-083.5k 阅读

列表推导式基础

在Python编程中,列表推导式(List Comprehensions)是一种简洁而强大的创建列表的方式。它允许我们通过简洁的语法从现有可迭代对象(如列表、元组、集合等)中快速生成新的列表。

基本语法

列表推导式的基本语法结构为:[expression for item in iterable]。其中,expression 是对 item 进行某种操作后返回的结果,item 是从 iterable 中依次取出的元素。

例如,我们想要创建一个包含 1 到 10 每个数平方的列表,传统方式可能是这样:

squares = []
for num in range(1, 11):
    squares.append(num ** 2)
print(squares)

而使用列表推导式,只需要一行代码:

squares = [num ** 2 for num in range(1, 11)]
print(squares)

这两种方式实现的功能相同,但列表推导式明显更加简洁。

包含条件的列表推导式

列表推导式还可以包含条件语句,其语法为:[expression for item in iterable if condition]。这里的 condition 是一个布尔表达式,只有当 conditionTrue 时,expression 才会被添加到新列表中。

比如,我们只想要 1 到 10 中的偶数的平方,代码可以这样写:

even_squares = [num ** 2 for num in range(1, 11) if num % 2 == 0]
print(even_squares)

在这个例子中,if num % 2 == 0 就是条件,只有满足这个条件的 num 才会进行平方运算并添加到新列表 even_squares 中。

列表推导式深入分析

执行逻辑

从执行逻辑上看,列表推导式首先迭代 iterable,对于每一个 item,先判断 if condition(如果存在条件语句)是否为 True。如果为 True,则对 item 应用 expression,并将结果添加到新的列表中。整个过程是顺序执行的,类似于传统的 for 循环结构,但更加紧凑。

[num ** 2 for num in range(1, 4) if num % 2 == 0] 为例,Python 首先从 range(1, 4) 中取出 1,判断 1 % 2 == 0False,所以不进行平方运算也不添加到列表。接着取出 2,判断 2 % 2 == 0True,将 2 ** 24 添加到列表。最后取出 3,判断 3 % 2 == 0False,不做处理。最终得到的列表就是 [4]

与传统循环的性能对比

在性能方面,一般情况下列表推导式会比传统的 for 循环稍快。这是因为列表推导式是在底层用 C 语言实现的,其执行效率更高。

我们可以通过 timeit 模块来进行性能测试。例如,对比生成 1 到 10000 每个数平方的列表,使用传统 for 循环和列表推导式的性能:

import timeit

def using_loop():
    squares = []
    for num in range(1, 10001):
        squares.append(num ** 2)
    return squares

def using_comprehension():
    return [num ** 2 for num in range(1, 10001)]

loop_time = timeit.timeit(using_loop, number = 1000)
comprehension_time = timeit.timeit(using_comprehension, number = 1000)

print(f"Using loop: {loop_time} seconds")
print(f"Using comprehension: {comprehension_time} seconds")

运行上述代码,通常会发现列表推导式所用的时间更短。不过,当 iterable 非常小或者 expressioncondition 非常复杂时,这种性能差异可能会变得不明显。

复杂列表推导式

嵌套列表推导式

列表推导式支持嵌套,语法形式为 [expression for item1 in iterable1 for item2 in iterable2]。这类似于嵌套的 for 循环结构。

例如,我们有两个列表 a = [1, 2]b = [3, 4],想要生成所有可能的组合 (x, y),可以这样使用嵌套列表推导式:

a = [1, 2]
b = [3, 4]
combinations = [(x, y) for x in a for y in b]
print(combinations)

上述代码等价于下面的嵌套 for 循环:

a = [1, 2]
b = [3, 4]
combinations = []
for x in a:
    for y in b:
        combinations.append((x, y))
print(combinations)

嵌套列表推导式的执行顺序是从左到右,先迭代外层的 iterable1,对于每一个 item1,再迭代内层的 iterable2,对每一个 item2 应用 expression 并添加到新列表。

在列表推导式中使用函数

在列表推导式的 expression 部分可以使用自定义函数,这大大扩展了列表推导式的功能。

假设我们有一个函数 is_prime 用于判断一个数是否为素数,现在要生成 1 到 100 中的所有素数列表:

def is_prime(n):
    if n <= 1:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True

primes = [num for num in range(1, 101) if is_prime(num)]
print(primes)

在这个例子中,列表推导式借助 is_prime 函数,筛选出了 1 到 100 中的素数。

多层条件的列表推导式

列表推导式中可以有多层条件判断。语法形式为 [expression for item in iterable if condition1 if condition2]

例如,我们要从 1 到 20 中找出既是偶数又能被 3 整除的数的平方,代码如下:

results = [num ** 2 for num in range(1, 21) if num % 2 == 0 if num % 3 == 0]
print(results)

这里的两个条件 num % 2 == 0num % 3 == 0 是层层递进的关系,只有同时满足这两个条件,num 才会进行平方运算并添加到新列表。

列表推导式与其他推导式的对比

与集合推导式的对比

集合推导式(Set Comprehensions)的语法与列表推导式类似,只是使用花括号 {} 代替方括号 [],语法为 {expression for item in iterable}。集合推导式生成的是集合,集合中的元素是唯一的。

例如,我们有一个列表 nums = [1, 2, 2, 3, 3, 3],使用列表推导式和集合推导式分别生成新的序列:

nums = [1, 2, 2, 3, 3, 3]
list_result = [num for num in nums]
set_result = {num for num in nums}
print(list_result)
print(set_result)

列表推导式生成的 list_result 仍然包含重复元素 [1, 2, 2, 3, 3, 3],而集合推导式生成的 set_result 会去除重复元素 {1, 2, 3}

与字典推导式的对比

字典推导式(Dictionary Comprehensions)用于快速创建字典,语法为 {key_expression: value_expression for item in iterable}

比如,我们有一个列表 keys = ['a', 'b', 'c'] 和一个列表 values = [1, 2, 3],要创建一个字典,可以使用字典推导式:

keys = ['a', 'b', 'c']
values = [1, 2, 3]
my_dict = {keys[i]: values[i] for i in range(len(keys))}
print(my_dict)

这与列表推导式不同,字典推导式生成的是键值对组成的字典结构,而列表推导式生成的是列表。

列表推导式的注意事项

可读性问题

虽然列表推导式非常简洁,但在使用复杂的嵌套、多层条件或复杂函数时,可能会影响代码的可读性。例如,一个多层嵌套且包含多个条件和复杂函数调用的列表推导式,可能让其他开发人员难以理解其逻辑。

在这种情况下,适当使用传统的 for 循环结构可能会使代码更易读。不过,如果使用得当,列表推导式的简洁性也可以成为代码可读性的优势,例如简单的生成平方列表的例子,就比传统循环更清晰明了。

内存使用问题

当处理非常大的可迭代对象时,列表推导式会一次性生成整个列表,这可能会导致内存占用过高。例如,要生成一个包含 1 到 10 亿每个数平方的列表,使用列表推导式可能会耗尽系统内存。

在这种情况下,可以考虑使用生成器表达式(Generator Expressions),其语法与列表推导式类似,只是使用圆括号 () 代替方括号 []。生成器表达式是按需生成数据,而不是一次性生成整个列表,从而节省内存。例如:

square_generator = (num ** 2 for num in range(1, 100000001))

这里的 square_generator 是一个生成器对象,只有在需要时才会生成下一个数的平方,而不是一次性生成所有数的平方并占用大量内存。

实际应用场景

数据清洗与转换

在数据处理中,列表推导式常用于数据清洗和转换。例如,我们有一个包含字符串数字的列表 ['1', '2', '3a', '4'],要将其中的数字字符串转换为整数并去除非数字字符串,可以使用列表推导式:

data = ['1', '2', '3a', '4']
cleaned_data = [int(num) for num in data if num.isdigit()]
print(cleaned_data)

这里通过 num.isdigit() 条件判断来清洗数据,然后使用 int(num) 将数字字符串转换为整数。

生成测试数据

在测试开发中,经常需要生成一些测试数据。例如,要生成一个包含 100 个随机整数的列表,每个整数在 1 到 100 之间,可以使用列表推导式结合 random 模块:

import random
test_data = [random.randint(1, 100) for _ in range(100)]
print(test_data)

这里 _ 表示一个临时变量,因为我们并不关心 range(100) 中的具体值,只需要执行 100 次生成随机数的操作。

矩阵操作

在处理矩阵相关的编程任务时,列表推导式也非常有用。例如,要创建一个 3x3 的单位矩阵(主对角线为 1,其余为 0),可以这样使用列表推导式:

identity_matrix = [[1 if i == j else 0 for j in range(3)] for i in range(3)]
print(identity_matrix)

这里外层的列表推导式控制行,内层的列表推导式控制列,通过条件判断 if i == j 来确定是否为 1,从而构建出单位矩阵。

总之,列表推导式是 Python 中一个功能强大且灵活的特性,在各种编程场景中都有广泛的应用。通过深入理解其语法、执行逻辑、性能特点以及注意事项,可以更加高效地使用它来编写简洁、可读且高性能的代码。无论是简单的数据转换,还是复杂的矩阵操作,列表推导式都能为我们提供便捷的解决方案。同时,在实际应用中,需要根据具体情况权衡其优势和劣势,以达到最佳的编程效果。在与其他推导式(如集合推导式、字典推导式)的对比中,也能更好地选择合适的数据结构生成方式。对于内存使用和可读性等问题的关注,有助于编写更加健壮和易于维护的 Python 程序。