[譯] 列表推導式與表達式生成器在 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索