[译] 列表推导式与表达式生成器在 Python 中的滥用

列表推导式是我喜欢的 Python 特性之一。我很是喜好列表推导式,为此我写过一篇关于它们的文章,作过一次针对它们的演讲,还在 PyCon 2018 上办过一个三小时推导式教程html

我喜好推导式,可是我发现一旦一个新的 Python 使用者开始真正使用推导式,他们会在全部可能的地方用这些推导式。推导式很可爱,但也很容易被滥用前端

这篇文章展现的案例中,从可读性的角度来看,推导式都不是完成任务的最佳工具。咱们会讨论一些案例,它们有比使用推导式更具备可读性的选择,咱们还会看到一些不明显的案例,它们根本就不须要使用推导式。python

若是你还不是推导式的爱好者,那么这篇文章并非为了吓退你,而是为了鼓励那些须要它的人(包括我)适度地使用它。android

注意:本文中涉及到的“推导式”是涵盖了全部形式的推导式(列表,集合,字典)以及生成表达式。若是你对推导式还不是特别熟悉,我建议你先阅读这篇文章 或者这个演讲(这个演讲对生成器表达式挖掘的比较深)。ios

编写拥挤的推导式

列表推导式的批评者老是抱怨它们的可读性太差。他们是对的,不少推导式很难读。一些时候,让这些推导式变的更易读的方法仅仅是多一点间隔git

观察一下这个函数中的推导式:github

def get_factors(dividend):
    """返回所给数值的全部因子做为一个列表。"""
    return [n for n in range(1, dividend+1) if dividend % n == 0]
复制代码

咱们能够经过添加一些合适的换行来让这个推导式更易读:express

def get_factors(dividend):
    """返回所给数值的全部因子做为一个列表。"""
    return [
        n
        for n in range(1, dividend+1)
        if dividend % n == 0
    ]
复制代码

代码越少意味着越好的可读性,但并不老是这样。空白符是你的好朋友,尤为是在你使用推导式的时候编程

一般来讲,我跟倾向于使用上面的缩进格式来写个人推导式并利用多行来隔离代码。有时我也用单行来写解析式,可是我不默认使用单行。后端

编写的推导式太丑

一些循环是能够被写成推导式的形式,可是若是循环里面有太多逻辑,那他们可能不该该被这样改写。

观察一下这个推导式:

fizzbuzz = [
    f'fizzbuzz {n}' if n % 3 == 0 and n % 5 == 0
    else f'fizz {n}' if n % 3 == 0
    else f'buzz {n}' if n % 5 == 0
    else n
    for n in range(100)
]
复制代码

这个推导式等价于这样的 for 循环:

fizzbuzz = []
for n in range(100):
    fizzbuzz.append(
        f'fizzbuzz {n}' if n % 3 == 0 and n % 5 == 0
        else f'fizz {n}' if n % 3 == 0
        else f'buzz {n}' if n % 5 == 0
        else n
    )
复制代码

推导式和 for 循环都使用了三层嵌套的 内联 if 语句 (Python 的三元操做符

这里有一个更易读的方式,使用 if-elif-else 结构:

fizzbuzz = []
for n in range(100):
    if n % 3 == 0 and n % 5 == 0:
        fizzbuzz.append(f'fizzbuzz {n}')
    elif n % 3 == 0:
        fizzbuzz.append(f'fizz {n}')
    elif n % 5 == 0:
        fizzbuzz.append(f'buzz {n}')
    else:
        fizzbuzz.append(n)
复制代码

即便这里一种用推导式书写代码的方法,可是这并不意味着你必须要这么作

在推导式里有不少复杂逻辑时,即便是单个的 内联 if 也须要谨慎。

number_things = [
    n // 2 if n % 2 == 0 else n * 3
    for n in numbers
]
复制代码

若是你倾向于在此类案例中使用推导式,那你至少须要考虑是否可使用空白符或者括号能够提升可读性

number_things = [
    (n // 2 if n % 2 == 0 else n * 3)
    for n in numbers
]
复制代码

而且,考虑一下提取你的逻辑操做到一个独立的函数是否也能够改进你的可读性(这个略傻的例子没有体现)。

number_things = [
    even_odd_number_switch(n)
    for n in numbers
]
复制代码

一个独立的函数是否能够提升可读性,取决于这个操做的重要程度、规模,以及函数名可否传达操做的含义。

假装成推导式的循环

有时你会遇到使用了推导式语法却破坏了推导式初衷的代码。

好比,这个代码好像是一个推导式:

[print(n) for n in range(1, 11)]
复制代码

可是它不像推导式同样运行。咱们使用推导式达到的目的并非它的本意。

若是咱们在 Python 中执行这个推导式,你就会明白个人意思:

>>> [print(n) for n in range(1, 11)]

[None, None, None, None, None, None, None, None, None, None]
复制代码

咱们是想打印 1 到 10 之间的全部数,同时咱们也是这么作的。可是这个推导式的语句返回了一个全是 None 值的列表给咱们,对咱们毫无心义。

你给推导式什么内容,它就会创建什么样的列表。咱们从 print 函数那里得到值去创建列表,而 print 函数的返回值就是 None

但咱们并不在乎推导式创建的列表,咱们只关心它的反作用。

咱们能够用下面的代码替代以前的代码:

for n in range(1, 11):
    print(n)
复制代码

列表推导式会循环一个迭代器而且创建一个新的列表,for 循环是用来遍历一个迭代器同时完成你想作的任何操做

当我在代码中看到推导式时,我当即会假设咱们建立了一个新的列表(由于这个就是它的做用)。若是你用一个推导式完成建立列表以外的目的,它会给其余读你代码的人带来困扰。

若是你不是为了建立一个新的列表,那就不要使用推导式。

当存在更特定工具时,使用推导式

在不少问题中,更特定的工具比通用目的的 for 循环更有意义。但推导式并不老是最适合手头工做的专用工具。

我见过而且写过一堆像这样的代码:

import csv

with open('populations.csv') as csv_file:
    lines = [
        row
        for row in csv.reader(csv_file)
    ]
复制代码

这种推导式会对惟一性的值进行排序。它的目的就是循环咱们提供的迭代器( csv.reader(csv_file) )而且建立一个列表。

可是,在 Python 中,咱们为这个任务提供了一个更特定的工具:list 的构造函数。Python 的 list 构造函数能够为咱们完成循环并建立列表的工做。

import csv

with open('populations.csv') as csv_file:
    lines = list(csv.reader(csv_file))
复制代码

推导式是一种特殊用途的工具,用于在迭代器上循环,以便在修改每一个元素的同时建立一个新列表,并/或过滤掉一些元素。list 构造函数是一个特定目的工具,用来遍历推导式并建立列表,同时不会改变任何的东西。

若是在创建列表时你不须要过滤元素或将它们映射到新元素中,你不须要使用推导式,你只须要使用 list 构造函数

这个推导式转换了从 zip 中获得的 row 元组并放入列表:

def transpose(matrix):
    """返回给定列表的转置版本。"""
    return [
        [n for n in row]
        for row in zip(*matrix)
    ]
复制代码

咱们一样也可使用 list 构造函数:

def transpose(matrix):
    """返回给定列表的转置版本。"""
    return [
        list(row)
        for row in zip(*matrix)
    ]
复制代码

每当你看到以下的推导式时:

my_list = [x for x in some_iterable]
复制代码

你能够用这种写法替代:

my_list = list(some_iterable)
复制代码

这一样适用于 dictset 的推导式。

这个是我过去常常会写的东西:

states = [
    ('AL', 'Alabama'),
    ('AK', 'Alaska'),
    ('AZ', 'Arizona'),
    ('AR', 'Arkansas'),
    ('CA', 'California'),
    # ...
]

abbreviations_to_names = {
    abbreviation: name
    for abbreviation, name in states
}
复制代码

咱们遍历一个有两项元组构成的列表,并以今生成一个字典。

这个任务实际上已经被 dict的构造函数完成了:

abbreviations_to_names = dict(states)
复制代码

listdict 的构造函数不是惟一的推导式替代工具。标准库和第三方库中包含了不少工具,在有的时候,他们比推导式更适合于你的循环要求。

下面这个是一个生成器表达式,目的是对嵌套迭代器求和:

def sum_all(number_lists):
    """返回二维列表中全部元素的和。"""
    return sum(
        n
        for numbers in number_lists
        for n in numbers
    )
复制代码

使用 itertools.chain 能够达到一样的目的:

from itertools import chain

def sum_all(number_lists):
    """返回二维列表中全部元素的和。"""
    return sum(chain.from_iterable(number_lists))
复制代码

何时使用推导式何时使用替代品,这个的界定没有那么清晰。

我也常常纠结使用 itertools.chain 仍是推导式。我一般会把两种都写出来而后使用更清晰的那个。

可读性在编程结构中老是针对于特定问题的,这个在推导式上也适用。

无效的工做

有时候你会发现,推导式不该该被另外一个构造函数所替代,而应该被彻底删除,只留下须要遍历的迭代器。

这段代码打开了一个单词构成的文件(每行一个单词),存储这个文件,同时计数每一个单词出现的次数:

from collections import Counter

word_counts = Counter(
    word
    for word in open('word_list.txt').read().splitlines()
)
复制代码

咱们使用了一个生成器表达式,但咱们并不须要如此。能够直接这样写:

from collections import Counter

word_counts = Counter(open('word_list.txt').read().splitlines())
复制代码

咱们在传给 Counter 类以前遍历了整个列表并转换为一个生成器。彻底是无用功。Counter 类是接受任何迭代器,不论它是列表,生成器,元组或者是其它结构

这是另一个无效的推导式:

with open('word_list.txt') as words_file:
    lines = [line for line in words_file]
    for line in lines:
        if 'z' in line:
            print('z word', line, end='')
复制代码

咱们遍历了 words_file,转化为列表 lines,再去遍历 lines 一次。整个对于列表的转换是没必要要的。

咱们能够直接遍历 words_file

with open('word_list.txt') as words_file:
    for line in words_file:
        if 'z' in line:
            print('z word', line, end='')
复制代码

没有任何理由将咱们只须要遍历一次的迭代器转换为列表。

在 Python 中,咱们更关注它是否是一个迭代器而不是它是否是一个列表

在不须要的时候,不要去建立一个新的迭代器。若是你只是为了遍历这个迭代器一次,你能够直接使用它

何时应该使用推导式?

那么,何时确实应该使用推导式呢?

一个简单可是不许确的回答是,当你须要写以下文复制-粘贴推导式格式中所提到的代码,同时你没有其余的工具可让你的代码更精简,你就应该考虑使用列表推导式了。

new_things = []
for ITEM in old_things:
    if condition_based_on(ITEM):
        new_things.append(some_operation_on(ITEM))
复制代码

循环能够用这样的推导式重写:

new_things = [
    some_operation_on(ITEM)
    for ITEM in old_things
    if condition_based_on(ITEM)
]
复制代码

更复杂的回答是,当推导式有意义时,你就应该考虑它。这实际上不算是一个回答,但确实没人回答“何时该使用推导式”这个问题。

这里有一个 for 循环看起来的确不像是能够用推导式重写:

def is_prime(candidate):
    for n in range(2, candidate):
        if candidate % n == 0:
            return False
    return True
复制代码

但实际上,若是咱们知道怎么使用 all 函数,咱们能够用生成器表达式来重写它:

def is_prime(candidate):
    return all(
        candidate % n != 0
        for n in range(2, candidate)
    )
复制代码

我写过一篇文章叫 anyall 函数的文章来描述这对操做和生成器表达式是多么搭配。可是 any 和 all 并非惟一与生成器表达式有关联的。

还有一个类似场景的代码:

def sum_of_squares(numbers):
    total = 0
    for n in numbers:
        total += n**2
    return total
复制代码

这里没有 append 同时也没有迭代器被创建。可是,若是咱们建立一个平方的生成器,咱们可使用内置的 sum 函数去获得同样的结果。

def sum_of_squares(numbers):
    return sum(n**2 for n in numbers)
复制代码

因此,除了要考虑检查“我是否能够从一个循环复制-粘贴到推导式”以外,咱们还须要考虑:咱们是否能够经过结合生成器表达式与接受迭代器的函数或者类来加强咱们的代码?

那些能够接受迭代器做为参数的函数或者类,多是与生成器表达式组合的优秀组件。

深思熟虑后使用列表推导式

列表推导式可使你的代码更可读(若是你不相信我,能够看个人演讲可理解的推导式中的例子),可是它确实被滥用。

列表推导式是被用来解决特定问题的专用工具。listdict 的构造函数是被用来解决更具体问题的更专用的工具。

循环是更通用的工具,适用于当你遇到的问题不适合推导式或其它专用循环工具领域的场景。

anyallsum 这样的函数,以及像 Counterchain 这样的类都是接受迭代器的工具,它们与推导式很是匹配,有时彻底取代了推导式

请记住,推导式只有一个目的:从旧的迭代器中建立一个新的迭代器,同时在此过程当中稍微调整值和/或过滤不匹配条件的值。推导式是一个可爱的工具,可是它们不是你惟一的工具。当你的推导式不能胜任时,不要忘记 listdict 构造函数,以及 for 循环。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索