for Comprehension

April 14, 2022

如果使用者輸入的命令列引數是數字,想將這些數字全部進行平方運算,該怎麼做呢?現在的你,也許會想出這樣的寫法:

import sys

squares = []
for arg in sys.argv[1:]:
    squares.append(int(arg) ** 2)

print(squares)

map 映射

將一個 list 轉為另一個 list,是很常見的操作,Python 針對這類需求,提供了for Comprehension 語法,你可以如下實現需求:

import sys

squares = [int(arg) ** 2 for arg in sys.argv[1:]]
print(squares)

對於 for arg in sys.argv[1:] 這部份,其作用是逐一迭代出命令列引數指定給 arg 變數,之後執行 for 左方的 int(arg) ** 2 運算,使用[]含括起來,表示每次迭代的運算結果,會被收集為一個 list。一個執行結果如下:

>python square.py 10 20 30
[100, 400, 900]

filter 過濾

for Comprehension 也可以與條件式結合,這可以構成一個過濾的功能。例如想收集某個 list 中的奇數元素至另一 list,在不使用 for Comprehension下,可以如下撰寫:

import sys

odds = []
for arg in sys.argv[1:]:
    if int(arg) % 2:
        odds.append(arg)

print(odds)

若使用 for Comprehension 的話,可以改寫為以下的程式碼:

import sys

odds = [arg for arg in sys.argv[1:] if int(arg) % 2]
print(odds)

在這個例子中,只有在 if 條件式成立時,for 左邊的運算式才會被執行,並收集為最後結果 list 中的元素。一個執行結果如下:

>python odds.py 11 8 9 5 4 6 3 2
['11', '9', '5', '3']

map 映射、filter 過濾,其實是具有函數式風格的模式,有興趣可以看看〈Map〉、〈Filter〉。

for Comprehension 如果要形成巢狀結構也是可行的,不過建議別太過火,不然可讀性會迅速降低。簡單地將矩陣表示為一維的 list 倒還不錯:

>>> matrix = [
...     [1, 2, 3],
...     [4, 5, 6],
...     [7, 8, 9]
... ]
>>> array = [element for row in matrix for element in row]
>>> array
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>

另一個例子是,使用 for Comprehension 來取得兩個序列的排列組合:

>>> [letter1 + letter2 for letter1 in 'Justin' for letter2 in 'momor']
['Jm', 'Jo', 'Jm', 'Jo', 'Jr', 'um', 'uo', 'um', 'uo', 'ur', 'sm', 'so', 'sm', 'so', 'sr', 'tm', 'to', 'tm', 'to', 'tr', 'im', 'io', 'im', 'io', 'ir', 'nm', 'no', 'nm', 'no', 'nr']
>>>

惰性求值

在 for Comprehension 兩旁放上 [],表示會產生 list,如果資料來源很長,或者資料來源本身,是個有惰性求值特性的產生器時,直接產生 list 顯得沒有效率,這時可以在 for Comprehension兩旁放上 (),這樣的話就會建立一個 generator 物件,具有惰性求值(lazy evaluation)特性。

舉個例子來說,Python 中有個 sum 函式,可以計算指定序列的數字加總值,像是若傳遞 sum([1, 2, 3]) 的話,結果會是 6。如果想計算 1 到 10000 的加總值呢?使用 sum([n for n in range(1, 10001)]) 是可以達到目的,不過,這會先產生具有 10000 個元素的 list,然後再交給 sum 函式運算,此時可以寫成 sum(n for n in range(1, 10001)),就不會有產生 list 的負擔。

這邊其實也在說明,只要寫 n for n in range(1, 10001) 就是個產生器運算式了,因此在傳給 sum 函式時,不必再寫成 sum((n for n in range(1, 10001))),需要加上括號的情況,是在需要直接參考一個產生器的時候,例如 g = (n for n in range(1, 10001)) 的情況。

應用於 set/dict/tuple

for Comprehension 也可用來建立 set,只要在 for Comprehension兩旁放上 {}。例如,建立一個 set,其中包括了來源字串中不重複的大寫字母。

>>> text = 'Your Right brain has nothing Left. Your Left brain has nothing Right'
>>> {c for c in text if c.isupper()}
{'Y', 'R', 'L'}
>>>

若是想使用 for Comprehension 來建立 dict 實例,也是可行的。例如:

>>> names = ['Justin', 'Monica', 'Irene']
>>> passwds = [123456, 654321, 13579]
>>> {name : passwd for name, passwd in zip(names, passwds)}
{'Justin': 123456, 'Irene': 13579, 'Monica': 654321}
>>>

上面的 zip,將 names 與 passwds 兩兩相扣在一起成為 tuple,每個 tuple 中的一對元素,會在 for Comprehension 中拆解指定給 name 與 passwd,最後 name 與 passwd 組成 dict 的每一對鍵值。

那麼,可以使用 for Comprehension建立 tuple嗎?可以的,不過不是在 for Comprehension兩旁放上 (),這樣的話就會建立一個 generator 物件,而不是 tuple,想要用 for Comprehension 建立 tuple 的話,可以將 for Comprehension 產生器運算式傳給 tuple。例如:

>>> tuple(n for n in range(10))
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
>>>

分享到 LinkedIn 分享到 Facebook 分享到 Twitter