還記得昨天我們完成了羅馬數字轉換器的完整功能嗎?今天,我們要深入探討一個開發者最關心的議題:性能優化!想像一下,如果你的轉換器每天要處理數百萬次的轉換請求,該如何確保它能快速回應?讓我們用 TDD 的方式來解決這個挑戰 ⚡。
今天是第 16 天,我們已經:
在進行性能優化時,我們遵循以下原則:
在 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 ⚡ |
性能測量 📊:學會建立基準測試和性能分析
快取策略 🔄:實作雙層快取系統(記憶體 + functools.lru_cache)
TDD 優化 ✅:在優化過程中保持測試驅動方法
性能監控 ⚡:學會測量執行時間和快取命中率
map()
, filter()
, sum()
通常比迴圈更快functools.lru_cache
:適用於純函數time.perf_counter()
:高精度時間測量cProfile
:Python 內建的性能分析器lru_cache
不生效
記憶體洩漏
試著為你的專案加入以下優化:
透過今天的學習,我們學會了一套系統性的性能優化方法。記住,優化是一個持續的過程,需要不斷地測量、分析和改進。這些技巧在實際專案開發中非常有用,特別是當你的應用需要處理大量請求時 🧪。
明天我們將進入新的階段,探索更多 Python 框架特有的測試技巧!