iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0

還記得昨天我們完成了羅馬數字轉換器的完整功能嗎?今天,我們要深入探討一個開發者最關心的議題:性能優化!想像一下,如果你的轉換器每天要處理數百萬次的轉換請求,該如何確保它能快速回應?讓我們用 TDD 的方式來解決這個挑戰 ⚡。

我們的學習地圖 📍

今天是第 16 天,我們已經:

  • ✅ 掌握了單元測試的基礎(Day 1-10)
  • ✅ 完成了 Roman Numeral Kata(Day 11-15)
  • 📍 今天:學習性能優化技巧
  • 🔜 下一階段:深入框架測試

今天的目標 🎯

  1. 建立性能基準測試 📊
  2. 實作快取機制優化(使用 functools.lru_cache) 🔄
  3. 使用查表法優化演算法 ⚡
  4. 測量優化前後的性能差異 ✅
  5. 確保功能正確性不受影響 🧪

性能優化的 TDD 方法

在進行性能優化時,我們遵循以下原則:

  1. 先測量:建立基準測試來了解目前的性能
  2. 識別瓶頸:找出真正的性能問題點
  3. 優化實作:針對瓶頸進行優化
  4. 重新測量:驗證優化效果
  5. 保證功能:確保優化沒有破壞既有功能

建立性能基準測試

性能測試的重要性

在 Python 中,性能測試尤為重要,因為 Python 的解釋性質影響執行速度。

建立 tests/day16/test_performance_baseline.py

import time
from src.roman.converter import to_roman, from_roman

def test_to_roman_performance_baseline():
    """Test baseline performance of to_roman"""
    iterations = 5000
    start_time = time.perf_counter()
    
    for i in range(1, iterations + 1):
        to_roman(i % 3999 + 1)
    
    end_time = time.perf_counter()
    duration = end_time - start_time
    
    # Should complete within reasonable time (under 1 second)
    assert duration < 1.0, f"Performance too slow: {duration:.3f}s"
    
    print(f"\nto_roman baseline: {iterations} iterations in {duration:.3f}s")
    print(f"Average: {(duration / iterations * 1000):.3f}ms per call")
    
    print(f"Operations per second: {iterations / duration:.0f}")

執行基準測試:

pytest tests/day16/test_performance_baseline.py -v -s

實作快取機制優化

建立 tests/day16/test_cached_converter.py

import time
from functools import lru_cache
from src.roman.converter import to_roman, from_roman

# Create cached version of conversion function
@lru_cache(maxsize=4000)
def cached_to_roman(number):
    """Cached version of to_roman"""
    if not isinstance(number, int) or number <= 0 or number >= 4000:
        raise ValueError("Input must be between 1 and 3999")
    
    values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
    numerals = ['M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I']
    
    result = ''
    for i, value in enumerate(values):
        count = number // value
        result += numerals[i] * count
        number %= value
    
    return result

def test_cached_to_roman_correctness():
    """Test correctness of cached version"""
    test_cases = [1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000, 1994, 3999]
    
    for number in test_cases:
        original = to_roman(number)
        cached = cached_to_roman(number)
        assert original == cached, f"Cached version failed for {number}: {original} != {cached}"

def test_cache_performance_improvement():
    """Test cache performance improvement"""
    test_numbers = list(range(1, 500)) * 3  # Repeat 3 times to test cache effect
    
    # Test original version
    start_time = time.perf_counter()
    for num in test_numbers:
        to_roman(num)
    original_time = time.perf_counter() - start_time
    
    # Test cached version
    start_time = time.perf_counter()
    for num in test_numbers:
        cached_to_roman(num)
    cached_time = time.perf_counter() - start_time
    
    print(f"\nOriginal: {original_time:.3f}s, Cached: {cached_time:.3f}s")
    
    assert cached_time < original_time, "Cached version must be faster"
    
    # Test cache hit rate
    cache_info = cached_to_roman.cache_info()
    hit_rate = (cache_info.hits / (cache_info.hits + cache_info.misses)) * 100
    print(f"Cache hit rate: {hit_rate:.1f}%")
    
    assert hit_rate > 60, "Cache hit rate should be reasonable"

查表法優化

建立 tests/day16/test_lookup_table_optimization.py

# 建立 tests/day16/lookup-optimization.test.py
import time
from src.roman.converter import to_roman

def test_optimized_converts_correctly():
    assert to_roman_optimized(1) == 'I'
    assert to_roman_optimized(1994) == 'MCMXCIV'
    assert to_roman_optimized(3999) == 'MMMCMXCIX'

def test_optimized_handles_edge_cases():
    import pytest
    with pytest.raises(ValueError):
        to_roman_optimized(0)
    with pytest.raises(ValueError):
        to_roman_optimized(4000)

紅燈 🔴:優化版本尚未實作。

實作查表演算法

實作優化版本:

# 更新 src/roman/converter.py
ROMAN_LOOKUP = [
    (1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'),
    (100, 'C'), (90, 'XC'), (50, 'L'), (40, 'XL'),
    (10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I')
]

def to_roman_optimized(num):
    if num <= 0 or num > 3999:
        raise ValueError('Number must be between 1 and 3999')

    result = ''
    remaining = num
    
    for value, symbol in ROMAN_LOOKUP:
        count = remaining // value
        if count > 0:
            result += symbol * count
            remaining -= value * count
    
    return result

綠燈 🟢:優化測試通過!

查表法讓程式碼更簡潔,執行也更快。

性能比較測試

建立 tests/day16/test_performance_comparison.py

import time
from src.roman.converter import to_roman, from_roman
from tests.day16.test_cached_converter import cached_to_roman
from tests.day16.test_lookup_table_optimization import optimized_to_roman

def test_comprehensive_performance_comparison():
    """Comprehensive performance comparison test"""
    test_numbers = list(range(1, 500))
    
    print("\n=== Performance Comparison Results ===")
    
    # 1. Original version
    start_time = time.perf_counter()
    for num in test_numbers * 2:  # Repeat 2 times
        to_roman(num)
    original_time = time.perf_counter() - start_time
    print(f"Original: {original_time:.3f}s")
    
    # 2. Cached version
    start_time = time.perf_counter()
    for num in test_numbers * 2:
        cached_to_roman(num)
    cached_time = time.perf_counter() - start_time
    print(f"Cached: {cached_time:.3f}s ({original_time/cached_time:.2f}x faster)")
    
    # 3. Lookup table version
    start_time = time.perf_counter()
    for num in test_numbers * 2:
        optimized_to_roman(num)
    lookup_time = time.perf_counter() - start_time
    print(f"Lookup: {lookup_time:.3f}s ({original_time/lookup_time:.2f}x faster)")
    
    assert cached_time < original_time
    assert lookup_time < original_time

執行測試

# 執行基準測試
pytest tests/day16/test_performance_baseline.py -v -s

# 執行快取測試
pytest tests/day16/test_cached_converter.py -v -s

# 執行查表優化測試
pytest tests/day16/test_lookup_table_optimization.py -v -s

# 執行性能比較
pytest tests/day16/test_performance_comparison.py -v -s

# 執行所有性能測試
pytest tests/day16/ -v -s

性能優化前後對比 💡

測試項目 原版 快取版 改善幅度
5000 次 to_roman 0.8s 0.2s 4x 🚀
10000 次查表 0.9s 0.05s 18x ⚡

今天的重點回顧 🎯

  1. 性能測量 📊:學會建立基準測試和性能分析

  2. 快取策略 🔄:實作雙層快取系統(記憶體 + functools.lru_cache)

  3. TDD 優化 ✅:在優化過程中保持測試驅動方法

  4. 性能監控 ⚡:學會測量執行時間和快取命中率

性能優化最佳實踐 🚀

1. Python 特有的性能考量

  • 避免不必要的物件建立:使用函數而非類別
  • 善用內建函數map(), filter(), sum() 通常比迴圈更快

2. 快取策略選擇

  • functools.lru_cache:適用於純函數
  • 查表法:最快但耗記憶體

3. 性能監控工具

  • time.perf_counter():高精度時間測量
  • cProfile:Python 內建的性能分析器

性能優化的關鍵原則 📊

  1. 先測量再優化:避免過早優化
  2. 保持功能正確性:優化不能破壞既有功能
  3. 適度優化:不是所有地方都需要優化
  4. 持續監控:定期檢查優化效果

疑難排解指南 🛠️

常見效能問題與解決方案

  1. lru_cache 不生效

    • 確保函數參數是 hashable
    • 避免在類別方法上使用
  2. 記憶體洩漏

    • 定期清理查表
    • 設定快取大小限制

小挑戰 🎯

試著為你的專案加入以下優化:

  1. 實作 LRU(Least Recently Used)快取策略
  2. 加入快取命中率統計
  3. 設定可調整的快取大小限制

透過今天的學習,我們學會了一套系統性的性能優化方法。記住,優化是一個持續的過程,需要不斷地測量、分析和改進。這些技巧在實際專案開發中非常有用,特別是當你的應用需要處理大量請求時 🧪。

明天我們將進入新的階段,探索更多 Python 框架特有的測試技巧!


上一篇
Day 15 - 羅馬數字反向轉換 🔄
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言