「Python 很慢」是程式設計社群中最常見的說法之一。本章將探討這個說法的真相、何時效能真的重要,以及如何有效地優化 Python 程式。

Python「慢」的真相

直譯語言 vs 編譯語言

Python 是直譯語言,程式碼在執行時才被轉換成機器碼:

1編譯語言(C/C++/Rust):
2原始碼 → 編譯器 → 機器碼 → 執行
34              一次編譯,多次執行
5
6直譯語言(Python):
7原始碼 → 直譯器 → 逐行執行
89         每次執行都要解釋

這意味著 Python 在純計算任務上確實比編譯語言慢,通常是 10-100 倍的差距。

但這重要嗎?

讓我們看一個來自 Reddit 社群的經典回答:

「如果你要問 Python 是不是太慢,那就不關你的事。」 — Reddit 用戶 scandii

這聽起來很直接,但背後有深刻的道理:

 1# 情境 1:網頁後端
 2# Python 處理請求:50ms
 3# 網路延遲:200ms
 4# 資料庫查詢:100ms
 5# 總計:350ms
 6#
 7# 就算 Python 快 10 倍(5ms),總時間也只變成 305ms
 8# 用戶感受差異:幾乎沒有
 9
10# 情境 2:命令列工具
11# 執行時間:0.5 秒
12# 用戶可接受?當然可以

設計哲學的取捨

Python 的設計哲學是「開發速度 > 執行速度」:

面向PythonC++
開發時間
執行速度
程式碼可讀性
除錯難度
學習曲線

對於大多數應用來說,開發效率和維護成本遠比執行速度重要。

真正的瓶頸在哪裡?

在優化之前,你需要先找出真正的瓶頸。以下是常見的效能瓶頸排名:

1. I/O 操作

 1import time
 2import requests
 3
 4# 網路請求:通常是最大的瓶頸
 5start = time.perf_counter()
 6response = requests.get("https://api.example.com/data")  # 50-500ms
 7print(f"網路請求: {time.perf_counter() - start:.3f}s")
 8
 9# 檔案讀寫
10start = time.perf_counter()
11with open("large_file.txt", "r") as f:
12    content = f.read()  # 取決於檔案大小和硬碟速度
13print(f"檔案讀取: {time.perf_counter() - start:.3f}s")

2. 資料庫查詢

 1# 一個沒有索引的查詢可能需要幾秒鐘
 2# SELECT * FROM users WHERE email = '...'  # 無索引:慢
 3# SELECT * FROM users WHERE id = 123       # 有索引:快
 4
 5# N+1 查詢問題
 6for user in users:
 7    orders = get_orders(user.id)  # 每個用戶一次查詢 → 很慢
 8
 9# 應該改成
10orders = get_orders_for_users([u.id for u in users])  # 一次查詢

3. 演算法複雜度

 1# O(n²) vs O(n) 的差異遠大於語言差異
 2
 3# O(n²) - 10000 個元素需要 100,000,000 次操作
 4def find_duplicates_slow(items):
 5    duplicates = []
 6    for i, item in enumerate(items):
 7        for j, other in enumerate(items):
 8            if i != j and item == other:
 9                duplicates.append(item)
10    return duplicates
11
12# O(n) - 10000 個元素只需要 10000 次操作
13def find_duplicates_fast(items):
14    seen = set()
15    duplicates = []
16    for item in items:
17        if item in seen:
18            duplicates.append(item)
19        seen.add(item)
20    return duplicates

瓶頸排名

1通常的效能瓶頸(由大到小):
21. 網路延遲         100-1000ms
32. 資料庫查詢        10-1000ms
43. 檔案 I/O          1-100ms
54. 演算法複雜度      視情況
65. Python 本身        0.001-1ms

優化方案總覽

方案適用場景學習成本效果
演算法優化通用最高
NumPy/Pandas數值計算
concurrent.futures並行任務中-高
Free-threadingCPU 並行
Cython熱點程式碼
PyPy通用加速
asyncioI/O 並發中-高

1. 演算法優化

永遠是第一優先:

1# 用合適的資料結構
2items_list = [1, 2, 3, ...]    # 查找 O(n)
3items_set = {1, 2, 3, ...}     # 查找 O(1)
4
5# 用合適的演算法
6sorted(items)                   # O(n log n)
7items.sort()                    # O(n log n),但原地排序更省記憶體

2. 使用 NumPy/Pandas

把計算交給 C 實現的函式庫:

 1import numpy as np
 2
 3# 純 Python:慢
 4def sum_squares_python(n):
 5    return sum(i * i for i in range(n))
 6
 7# NumPy:快 10-100 倍
 8def sum_squares_numpy(n):
 9    arr = np.arange(n)
10    return np.sum(arr * arr)
11
12# 向量化操作是關鍵
13# 不好:Python 迴圈
14result = []
15for x in data:
16    result.append(x * 2 + 1)
17
18# 好:NumPy 向量化
19result = data * 2 + 1

3. 並行處理

3.7 並行處理3.8 Free-Threading

1from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
2
3# I/O 密集:使用執行緒
4with ThreadPoolExecutor(max_workers=10) as executor:
5    results = executor.map(fetch_url, urls)
6
7# CPU 密集:使用進程(或 Free-threading)
8with ProcessPoolExecutor() as executor:
9    results = executor.map(compute_heavy, data_chunks)

4. PyPy

PyPy 是 Python 的另一個實現,使用 JIT 編譯:

1# 安裝 PyPy
2# macOS: brew install pypy3
3# Ubuntu: apt install pypy3
4
5# 執行
6pypy3 your_script.py

PyPy 對於迴圈密集的程式碼特別有效:

1# 這種程式碼在 PyPy 上可能快 10-50 倍
2def compute():
3    total = 0
4    for i in range(10_000_000):
5        total += i * i
6    return total

Python 3.13-3.14 效能改進

新的直譯器

Python 3.14 引入了使用尾調用的新直譯器,在支援的編譯器上快 3-5%:

1# 需要使用 Clang 19+ 編譯,並啟用配置選項
2./configure --with-tail-call-interp

增量垃圾回收

循環垃圾回收現在是增量式的,減少了長時間停頓:

1import gc
2
3# 舊版:可能造成明顯停頓
4gc.collect()
5
6# 3.14:增量回收,影響更平滑
7gc.collect(1)

Free-Threading

詳見 3.8 Free-Threading

什麼時候該優化?

「過早優化是萬惡之源」

Donald Knuth 的這句名言經常被誤解。完整的引言是:

「程式設計師花費了大量時間思考或擔心程式非關鍵部分的速度,而當考慮到除錯和維護時,這些效率的嘗試實際上會產生強烈的負面影響。我們應該忘記小的效率問題,比如說 97% 的時間:過早優化是萬惡之源。然而,我們不應該放棄那關鍵的 3% 的機會。」

優化的正確流程

 11. 讓程式正確運作
 2 32. 讓程式碼可讀、可維護
 4 53. 測量效能(profiling)
 6 74. 找出瓶頸(通常是 20% 的程式碼佔 80% 的時間)
 8 95. 只優化瓶頸
10116. 再次測量,確認改善

80/20 法則

在大多數程式中:

  • 20% 的程式碼佔用 80% 的執行時間
  • 優化錯誤的地方不會有任何效果

效能測量工具

簡單計時

 1import time
 2
 3def measure_time(func, *args, **kwargs):
 4    """測量函式執行時間"""
 5    start = time.perf_counter()
 6    result = func(*args, **kwargs)
 7    elapsed = time.perf_counter() - start
 8    print(f"{func.__name__}: {elapsed:.6f}s")
 9    return result
10
11# 使用
12result = measure_time(my_function, arg1, arg2)

使用 timeit

 1import timeit
 2
 3# 測量小段程式碼
 4time_taken = timeit.timeit(
 5    'sum(range(1000))',
 6    number=10000
 7)
 8print(f"平均執行時間: {time_taken / 10000:.6f}s")
 9
10# 比較兩種實現
11setup = "data = list(range(1000))"
12
13time1 = timeit.timeit('sum(data)', setup=setup, number=10000)
14time2 = timeit.timeit('sum(x for x in data)', setup=setup, number=10000)
15
16print(f"直接 sum: {time1:.4f}s")
17print(f"生成器 sum: {time2:.4f}s")

使用 cProfile

 1import cProfile
 2import pstats
 3
 4# 基本用法
 5cProfile.run('my_function()')
 6
 7# 詳細分析
 8profiler = cProfile.Profile()
 9profiler.enable()
10
11# 執行你的程式碼
12result = my_function()
13
14profiler.disable()
15stats = pstats.Stats(profiler)
16stats.sort_stats('cumulative')
17stats.print_stats(20)  # 顯示前 20 個

使用 line_profiler(逐行分析)

1pip install line_profiler
1# 在函式上加上 @profile 裝飾器
2@profile
3def slow_function():
4    total = 0
5    for i in range(1000):
6        total += i * i
7    return total
1kernprof -l -v your_script.py

使用 memory_profiler(記憶體分析)

1pip install memory_profiler
1from memory_profiler import profile
2
3@profile
4def memory_hungry_function():
5    big_list = [i for i in range(1000000)]
6    return sum(big_list)

實際案例

案例 1:優化資料處理

 1# 原始版本:慢
 2def process_data_slow(data):
 3    result = []
 4    for item in data:
 5        if item > 0:
 6            result.append(item * 2)
 7    return result
 8
 9# 優化版本 1:列表推導式(快 20-30%)
10def process_data_v1(data):
11    return [item * 2 for item in data if item > 0]
12
13# 優化版本 2:NumPy(大數據時快 10-100 倍)
14import numpy as np
15
16def process_data_v2(data):
17    arr = np.array(data)
18    return arr[arr > 0] * 2

案例 2:快取昂貴的計算

 1from functools import lru_cache
 2
 3# 沒有快取:每次都重新計算
 4def fibonacci_slow(n):
 5    if n < 2:
 6        return n
 7    return fibonacci_slow(n - 1) + fibonacci_slow(n - 2)
 8
 9# 有快取:已計算的結果會被記住
10@lru_cache(maxsize=None)
11def fibonacci_fast(n):
12    if n < 2:
13        return n
14    return fibonacci_fast(n - 1) + fibonacci_fast(n - 2)
15
16# fibonacci_slow(35) 需要幾秒鐘
17# fibonacci_fast(35) 幾乎瞬間完成

案例 3:選擇正確的資料結構

 1import time
 2
 3# 用 list 查找(O(n))
 4def find_in_list(items, target):
 5    return target in items
 6
 7# 用 set 查找(O(1))
 8def find_in_set(items, target):
 9    return target in items
10
11# 測試
12data_list = list(range(1_000_000))
13data_set = set(range(1_000_000))
14target = 999_999
15
16start = time.perf_counter()
17find_in_list(data_list, target)
18print(f"List 查找: {time.perf_counter() - start:.6f}s")
19
20start = time.perf_counter()
21find_in_set(data_set, target)
22print(f"Set 查找: {time.perf_counter() - start:.6f}s")
23
24# List 查找: 0.015000s(取決於位置)
25# Set 查找:  0.000001s(幾乎瞬間)

思考題

  1. 為什麼「過早優化是萬惡之源」?什麼時候優化才是適當的?
  2. 在什麼情況下,Python 的「慢」確實是個問題?
  3. NumPy 為什麼比純 Python 迴圈快這麼多?

實作練習

  1. 使用 cProfile 分析一個現有的 Python 程式,找出效能瓶頸
  2. 將一個使用 Python 迴圈的數值計算程式改寫成 NumPy 版本,比較效能差異
  3. 實作一個帶有快取的 API 客戶端,避免重複請求相同的資料

延伸閱讀

延伸閱讀(進階系列)


上一章:並行處理