Python 串列生成式:一行取代 for 迴圈的進階語法

Python 串列生成式(List Comprehension)是用一行簡潔語法取代傳統 for 迴圈建立新串列的進階寫法,不僅程式碼更精簡,執行效能也優於等效的 for 迴圈。本文完整解析串列生成式的基本語法、加入條件篩選的寫法、巢狀生成式,以及延伸的字典生成式(Dict Comprehension)與集合生成式(Set Comprehension),並附上量化資料轉換與篩選的實戰範例。

Cinematic dark alchemy forge with a single glowing amber production line compressing a complex multi-step process into one streamlined beam of light, mystical runes transforming raw elements into refined crystals in one pass, deep dramatic shadows on stone walls, cinematic lighting, 16:9

什麼是串列生成式?

串列生成式(List Comprehension)是 Python 特有的語法糖,讓你用一行程式碼完成「迭代 → 轉換 → 收集」三步驟,直接產生一個新的 List,不需要先建立空 List 再逐一 append()。它的語法結構是 [運算式 for 變數 in 序列],讀起來幾乎像自然語言:「把序列中每個元素套用某個運算式,收集成一個新 List」。

可以把串列生成式想像成煉金坊的一體式精煉爐:傳統做法要先準備空容器、逐一投入原料、逐一煉製、逐一收集;生成式則把整條流水線壓縮成一道閃光,原料進去,精煉品直接出來。效能更好,程式碼也更優雅。

核心語法:從 for 迴圈到生成式

# === 對比:傳統 for 迴圈 vs 串列生成式 ===

# 傳統寫法:建立 1~10 的平方數串列(4 行)
squares = []
for x in range(1, 11):
    squares.append(x ** 2)
print(squares)   # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# ✅ 串列生成式(1 行,完全等效)
squares = [x ** 2 for x in range(1, 11)]
print(squares)   # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# 語法結構:[運算式  for 變數 in 序列]
#            ↑做什麼  ↑每次取出  ↑從哪裡取
串列生成式的語法順序是 「先寫你想要的結果,再寫從哪裡取、怎麼取」,與一般閱讀習慣相近。整個表達式包在中括號 [ ] 內,Python 看到這個結構就知道要建立一個新的 List。
# === 加入條件篩選(if 過濾)===

# 傳統寫法:取 1~20 中的偶數
evens = []
for x in range(1, 21):
    if x % 2 == 0:
        evens.append(x)

# ✅ 串列生成式加條件(if 寫在最後)
evens = [x for x in range(1, 21) if x % 2 == 0]
print(evens)   # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

# 語法結構:[運算式  for 變數 in 序列  if 條件]
#                                     ↑篩選條件放最後

# 字串串列的大小寫轉換與過濾
tickers = ['AAPL', 'tsla', 'GOOGL', '', 'msft', 'NVDA']

# 取出非空字串並統一轉大寫
clean = [t.upper() for t in tickers if t]
print(clean)   # ['AAPL', 'TSLA', 'GOOGL', 'MSFT', 'NVDA']
條件篩選的 if 放在生成式最後面for 子句之後),意思是「只有符合條件的元素才納入結果」。這與三元運算式的 if-else 位置不同,不要混淆——三元 if-else 是寫在運算式位置(最前面),用來決定元素的值;篩選 if 是寫在最後面,用來決定元素是否納入。

規則與注意事項

規則 正確範例 錯誤範例 / 陷阱
篩選 if 放最後,三元 if-else 放最前 [x for x in lst if x > 0](篩選) 混淆位置導致 SyntaxError 或邏輯錯誤
生成式應保持一行可讀,避免過度複雜 簡單轉換與篩選適合生成式 三層巢狀 + 複雜條件 → 改回 for 迴圈
生成式建立的是全新 List,不修改原序列 new = [x*2 for x in old] 誤以為會修改 old 本身
生成式內的變數不會洩漏到外部作用域 生成式內的 x 在外部不可見 for 迴圈的變數則會保留在外部
需要副作用(如 print)時用 for,不用生成式 for x in lst: print(x) [print(x) for x in lst](不推薦,產生無用的 None 串列)

常見錯誤與防呆

錯誤一:混淆篩選 if 與三元 if-else 的位置

# 情境:把正數保留,負數轉成 0

# ❌ 錯誤寫法(篩選 if 放前面,語法錯誤)
nums = [3, -1, 5, -2, 8]
result = [x if x > 0 for x in nums]   # SyntaxError!

# ✅ 寫法一:三元 if-else 放最前(保留所有元素,負數變 0)
result = [x if x > 0 else 0 for x in nums]
print(result)   # [3, 0, 5, 0, 8]

# ✅ 寫法二:篩選 if 放最後(只保留正數,負數直接排除)
result = [x for x in nums if x > 0]
print(result)   # [3, 5, 8]

錯誤二:誤用生成式產生副作用

# ❌ 不推薦寫法(用生成式呼叫 print,產生 [None, None, None])
nums = [1, 2, 3]
result = [print(x) for x in nums]   # 會印出,但 result = [None, None, None]
print(result)   # [None, None, None]

# ✅ 有副作用的操作用 for 迴圈
for x in nums:
    print(x)

錯誤三:生成式過度巢狀,可讀性崩潰

# ❌ 過度複雜的生成式(難以閱讀與維護)
matrix = [[1,2,3],[4,5,6],[7,8,9]]
result = [cell*2 for row in matrix for cell in row if cell % 2 != 0]

# ✅ 改用 for 迴圈,加上有意義的變數名與縮排
result = []
for row in matrix:
    for cell in row:
        if cell % 2 != 0:     # 只取奇數
            result.append(cell * 2)
print(result)   # [2, 6, 10, 14, 18]

進階用法

三元運算式:根據條件決定元素值

在生成式的運算式位置使用三元 if-else,可以對每個元素做「二選一的值轉換」,同時保留所有元素。

# 情境:將收盤價序列標記為漲('UP')或跌('DOWN')
closes = [100, 105, 102, 108, 99, 111]
prev   = closes[0]
labels = []

# 傳統寫法
for c in closes[1:]:
    labels.append('UP' if c > prev else 'DOWN')
    prev = c

# ✅ 生成式(需另外處理 prev,此處用 zip 配對相鄰兩根)
labels = ['UP' if c2 > c1 else 'DOWN'
          for c1, c2 in zip(closes, closes[1:])]
print(labels)   # ['UP', 'DOWN', 'UP', 'DOWN', 'UP']

Dict Comprehension 與 Set Comprehension

生成式的概念可以延伸到字典(使用大括號 + 冒號)與集合(使用大括號,無冒號),語法結構完全相同,只是外層括號與格式不同。

# === Dict Comprehension:建立字典 ===
tickers = ['AAPL', 'TSLA', 'GOOGL']
prices  = [185.2, 248.7, 175.9]

# 傳統做法
price_dict = {}
for t, p in zip(tickers, prices):
    price_dict[t] = p

# ✅ Dict Comprehension
price_dict = {t: p for t, p in zip(tickers, prices)}
print(price_dict)
# {'AAPL': 185.2, 'TSLA': 248.7, 'GOOGL': 175.9}

# 反轉字典 Key-Value
reversed_dict = {v: k for k, v in price_dict.items()}
print(reversed_dict)
# {185.2: 'AAPL', 248.7: 'TSLA', 175.9: 'GOOGL'}

# === Set Comprehension:建立集合(自動去重)===
raw_data = [1, 2, 2, 3, 4, 4, 5]
unique_squares = {x ** 2 for x in raw_data}
print(unique_squares)   # {1, 4, 9, 16, 25}(已去重)

量化實戰:收盤價資料批次處理

串列生成式在量化開發中最常用於批次轉換與篩選歷史價格資料,一行程式碼取代多行迴圈,讓程式碼更簡潔。

closes = [98.5, 102.3, 115.7, 88.2, 121.0, 95.6, 110.4, 78.9, 130.2]

# 篩選:只保留高於 100 的收盤價
above_100 = [c for c in closes if c >= 100]
print(above_100)   # [102.3, 115.7, 121.0, 110.4, 130.2]

# 轉換:計算每根 K 線對第一根的漲跌幅(%)
base = closes[0]
pct_changes = [round((c - base) / base * 100, 2) for c in closes]
print(pct_changes)
# [0.0, 3.86, 17.46, -10.46, 22.84, -2.95, 12.08, -19.9, 32.18]

# 組合篩選與轉換:只取正報酬,並標記為字串
winners = [f"+{round((c-base)/base*100,1)}%"
           for c in closes if c > base]
print(winners)   # ['+3.9%', '+17.5%', '+22.8%', '+12.1%', '+32.2%']

煉金坊小叮嚀

串列生成式是 Python 最具代表性的優雅語法之一,我在量化開發中大量使用它來處理 K 線資料的批次轉換與篩選。但有一個原則我始終堅守:生成式只用在「一眼能看懂」的情況——邏輯簡單、條件清晰;一旦需要巢狀超過兩層或條件複雜,立刻改回 for 迴圈。可讀性永遠優先於簡潔性。另外,篩選 if 放最後、三元 if-else 放最前,這個位置規則剛開始會搞混,口訣記住:「要什麼值」寫最前,「從哪裡取」寫中間,「要不要納入」寫最後——對應三元 if-else、for、篩選 if 的排列順序。

張貼留言

較新的 較舊