Profilowanie wydajności w Pythonie – cProfile, timeit, line_profiler i optymalizacja

Kacper Sieradziński
Kacper Sieradziński
27 lipca 2025Edukacja4 min czytania

"Premature optimization is the root of all evil" — te słowa Donalda Knutha są prawdziwe, ale po zidentyfikowaniu problemu wydajnościowego trzeba wiedzieć, jak go znaleźć i naprawić. Profilowanie kodu to proces analizowania, gdzie program spędza najwięcej czasu i zużywa najwięcej zasobów. W Pythonie mamy potężne narzędzia do profilowania: cProfile, timeit, line_profiler i wiele innych. W tym artykule poznasz, jak używać tych narzędzi do znajdowania wąskich gardeł, optymalizowania kodu i mierzenia postępów.

Obraz główny Profilowanie wydajności w Pythonie – cProfile, timeit, line_profiler i optymalizacja

Dlaczego profilowanie jest ważne?

Profilowanie to proces zbierania statystyk o wykonaniu programu — które funkcje są wywoływane najczęściej, ile czasu zajmują, ile pamięci zużywają. Korzyści:

  • 🎯 Identyfikacja problemów – znajdowanie rzeczywistych wąskich gardeł
  • 📊 Mierzenie postępów – weryfikacja, czy optymalizacje działają
  • 💡 Świadome decyzje – optymalizujesz tylko to, co naprawdę trzeba
  • 🚀 Lepsza wydajność – aplikacje działają szybciej i zużywają mniej zasobów

Zasada: "Measure, don't guess" — zawsze mierz wydajność przed i po optymalizacji.

Jeśli chcesz poznać ogólne techniki optymalizacji, sprawdź Zaawansowane techniki w Pythonie.

timeit – mierzenie czasu wykonania

timeit to najprostsze narzędzie do mierzenia czasu wykonania małych fragmentów kodu.

Podstawowe użycie

Python
1 2 3 4 5 import timeit # Mierzenie czasu wykonania pojedynczej operacji czas = timeit.timeit('sum(range(1000))', number=10000) print(f"Czas: {czas:.4f} sekundy dla 10000 wywołań")

timeit w Jupyter Notebook

W Jupyter można użyć magic command:

Python
1 2 %%timeit sum(range(1000))

timeit.Timer dla bardziej złożonych przykładów

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import timeit # Porównanie dwóch implementacji setup = """ def funkcja_a(): return sum(range(1000)) def funkcja_b(): total = 0 for i in range(1000): total += i return total """ czas_a = timeit.timeit('funkcja_a()', setup=setup, number=10000) czas_b = timeit.timeit('funkcja_b()', setup=setup, number=10000) print(f"Funkcja A: {czas_a:.4f}s") print(f"Funkcja B: {czas_b:.4f}s") print(f"Różnica: {(czas_b/czas_a - 1)*100:.1f}%")

Zalety timeit

  • ✅ Automatyczne uruchamianie wiele razy dla dokładności
  • ✅ Automatyczne wyłączenie garbage collectora dla lepszej precyzji
  • ✅ Łatwe porównywanie różnych implementacji

Wady timeit

  • ❌ Tylko całkowity czas wykonania
  • ❌ Nie pokazuje, gdzie kod spędza czas
  • ❌ Nie nadaje się do analizy całych aplikacji

cProfile – statystyczne profilowanie

cProfile to standardowe narzędzie do profilowania całych programów Python. Pokazuje, które funkcje są wywoływane, ile razy i ile czasu zajmują.

Użycie z linii poleceń

Bash
1 python -m cProfile -o output.prof script.py

Plik output.prof można analizować za pomocą pstats:

Python
1 2 3 4 5 import pstats stats = pstats.Stats('output.prof') stats.sort_stats('cumulative')# Sortuj według czasu kumulacyjnego stats.print_stats(20)# Pokaż top 20 funkcji

Użycie w kodzie

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import cProfile import pstats def moja_funkcja(): # Twój kod do profilowania total = 0 for i in range(10000): total += i * 2 return total # Profilowanie profiler = cProfile.Profile() profiler.enable() # Wykonaj kod for _ in range(100): moja_funkcja() profiler.disable() # Analiza wyników stats = pstats.Stats(profiler) stats.sort_stats('cumulative') stats.print_stats(20)

Interpretacja wyników cProfile

Typowy output cProfile:

Bash
1 2 3 4 5 6 7 8 100006 function calls in 0.250 seconds Ordered by: cumulative time ncallstottimepercallcumtimepercall filename:lineno(function) 10.0000.0000.2500.250 script.py:1(<module>) 1000.1500.0020.2500.003 script.py:3(moja_funkcja) 1000000.1000.0000.1000.000 {built-in method range}
  • ncalls – liczba wywołań funkcji
  • tottime – całkowity czas spędzony w funkcji (bez funkcji wywoływanych)
  • percall – średni czas na wywołanie (tottime/ncalls)
  • cumtime – czas kumulacyjny (włączając funkcje wywoływane)
  • filename:lineno(function) – lokalizacja funkcji

SnakeViz – wizualizacja wyników

SnakeViz to narzędzie do wizualizacji profilów cProfile:

Bash
1 2 pip install snakeviz snakeviz output.prof

Otworzy interaktywną wizualizację w przeglądarce.

line_profiler – profilowanie linia po linii

line_profiler pokazuje, ile czasu zajmuje każda linia kodu. To kluczowe narzędzie do znajdowania wąskich gardeł.

Instalacja line_profiler

Bash
1 pip install line_profiler

Użycie z dekoratorem

Python
1 2 3 4 5 6 7 8 9 10 11 12 @profile def przetwarzaj_dane(data): wynik = [] for item in data: # Linia 1 if item > 0: # Linia 2 wynik.append(item * 2) # Linia 3 else: # Linia 4 wynik.append(0) # Linia 5 return wynik # Linia 6 # Uruchom profilowanie: # kernprof -l -v script.py

Uruchomienie

Bash
1 kernprof -l -v script.py

Output pokazuje czas dla każdej linii:

Line #Hits TimePer Hit % TimeLine Contents
==============================================================
 1 @profile
 2 def przetwarzaj_dane(data):
 3 122.00.0wynik = []
 4 10000 50000.5 50.0for item in data:
 58000 30000.4 30.0if item > 0:
 68000 20000.3 20.0wynik.append(item * 2)
 720005000.35.0else:
 820003000.23.0wynik.append(0)
 9 111.00.0return wynik

line_profiler programowo

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from line_profiler import LineProfiler def moja_funkcja(data): wynik = [] for item in data: wynik.append(item * 2) return wynik profiler = LineProfiler() profiler.add_function(moja_funkcja) profiler.enable() data = list(range(10000)) moja_funkcja(data) profiler.disable() profiler.print_stats()

memory_profiler – profilowanie pamięci

memory_profiler pokazuje zużycie pamięci linia po linii. To szczególnie przydatne do znajdowania problemów z alokacją pamięci. Jeśli chcesz zrozumieć, jak Python zarządza pamięcią, sprawdź Zaawansowane zarządzanie pamięcią i garbage collector.

Instalacja memory_profiler

Bash
1 2 pip install memory-profiler pip install psutil# Dla lepszej precyzji

Użycie memory_profiler

Python
1 2 3 4 5 6 7 8 @profile def przetwarzaj_dane(data): wynik = [] # Linia 1: 50 MiB for item in data: # Linia 2: 50 MiB wynik.append(item * 2) # Linia 3: 100 MiB return wynik # Linia 4: 100 MiB # Uruchom: python -m memory_profiler script.py

Output:

Bash
1 2 3 4 5 6 7 8 Line #Mem usageIncrement Line Contents ================================================ 1 50.0 MiB0.0 MiB @profile 2 50.0 MiB0.0 MiB def przetwarzaj_dane(data): 3 50.0 MiB0.0 MiB wynik = [] 4 52.0 MiB2.0 MiB for item in data: 5 75.0 MiB 23.0 MiB wynik.append(item * 2) 6 75.0 MiB0.0 MiB return wynik

py-spy – profilowanie działających procesów

py-spy pozwala profilować działający proces Python bez modyfikacji kodu.

Instalacja py-spy

Bash
1 pip install py-spy

Użycie py-spy

Bash
1 2 3 4 5 6 7 8 # Profilowanie działającego procesu py-spy top --pid 12345 # Zapisz profil do pliku py-spy record -o profile.svg --pid 12345 # Real-time profilowanie py-spy top --pid 12345 --rate 100

To szczególnie przydatne do profilowania aplikacji produkcyjnych bez restartowania.

Profilowanie asynchronicznego kodu

Profilowanie kodu asynchronicznego wymaga specjalnego podejścia. Więcej o asynchroniczności i jej optymalizacji znajdziesz w artykule Asynchroniczność w Pythonie z asyncio.

cProfile z asyncio

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import asyncio import cProfile import pstats async def async_task(): await asyncio.sleep(0.1) return sum(range(1000)) async def main(): profiler = cProfile.Profile() profiler.enable() tasks = [async_task() for _ in range(100)] await asyncio.gather(*tasks) profiler.disable() stats = pstats.Stats(profiler) stats.sort_stats('cumulative') stats.print_stats() asyncio.run(main())

Więcej o asynchroniczności w Asynchroniczność w Pythonie z asyncio.

Najlepsze praktyki profilowania

1. Profiluj, nie zgaduj

Zawsze mierz wydajność przed optymalizacją. Intuicja często jest błędna.

2. Profiluj realistyczne dane

Używaj danych podobnych do produkcyjnych. Małe dane testowe mogą nie pokazać problemów.

3. Profiluj w odpowiednim środowisku

  • Development – dla szybkiego feedbacku
  • Staging – dla bardziej realistycznych wyników
  • Production – tylko jeśli bezpieczne (np. py-spy)

4. Szukaj wzorców

Nie tylko pojedyncze wolne funkcje, ale też:

  • Funkcje wywoływane bardzo często (nawet jeśli szybkie)
  • Duże alokacje pamięci
  • N+1 queries (w przypadku baz danych)

5. Mierz przed i po

Zawsze mierz wydajność przed i po optymalizacji, aby potwierdzić poprawę. Profilowanie jest szczególnie przydatne przy optymalizacji wielowątkowych i wieloprocesowych aplikacji — więcej w Wielowątkowość a wieloprocesowość w Pythonie.

6. Uważaj na overhead profilowania

Profilowanie spowalnia kod (zwykle 2-10x). To normalne, ale pamiętaj o tym przy interpretacji wyników.

Przykład praktyczny: optymalizacja funkcji

Załóżmy, że mamy funkcję do przetwarzania danych:

Python
1 2 3 4 5 6 7 8 def przetwarzaj_dane(data): wynik = [] for item in data: if item > 0: wynik.append(item * 2) else: wynik.append(0) return wynik

Krok 1: Profilowanie

Python
1 2 3 4 5 6 7 8 9 10 11 from line_profiler import LineProfiler profiler = LineProfiler() profiler.add_function(przetwarzaj_dane) profiler.enable() data = list(range(100000)) wynik = przetwarzaj_dane(data) profiler.disable() profiler.print_stats()

Krok 2: Analiza wyników

Widzimy, że pętla i append są wąskim gardłem.

Krok 3: Optymalizacja

Python
1 2 def przetwarzaj_dane_opt(data): return [item * 2 if item > 0 else 0 for item in data]

Krok 4: Porównanie

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import timeit setup = """ data = list(range(100000)) def przetwarzaj_dane(data): wynik = [] for item in data: if item > 0: wynik.append(item * 2) else: wynik.append(0) return wynik def przetwarzaj_dane_opt(data): return [item * 2 if item > 0 else 0 for item in data] """ czas_stary = timeit.timeit('przetwarzaj_dane(data)', setup=setup, number=100) czas_nowy = timeit.timeit('przetwarzaj_dane_opt(data)', setup=setup, number=100) print(f"Stary: {czas_stary:.4f}s") print(f"Nowy: {czas_nowy:.4f}s") print(f"Poprawa: {(1 - czas_nowy/czas_stary)*100:.1f}%")

Integracja z CI/CD

Możesz automatycznie profilować kod w CI/CD:

YAML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # .github/workflows/profiling.yml name: Performance Profiling on: [pull_request] jobs: profile: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies run: pip install -r requirements.txt pytest cProfile - name: Run profiling run: | python -m cProfile -o profile.prof -m pytest tests/ - name: Analyze profile run: | python -c "import pstats; s = pstats.Stats('profile.prof'); s.sort_stats('cumulative'); s.print_stats(20)"

Podsumowanie

Profilowanie to kluczowa umiejętność dla każdego programisty Pythona. Kluczowe narzędzia:

  • timeit – szybkie pomiary czasu wykonania
  • cProfile – statystyczne profilowanie całych programów
  • line_profiler – profilowanie linia po linii
  • memory_profiler – analiza zużycia pamięci
  • py-spy – profilowanie działających procesów

Pamiętaj: zawsze mierz przed optymalizacją, profiluj na realistycznych danych i mierz postępy po optymalizacji.

Co dalej?

Rozwijaj umiejętności optymalizacji:

Pamiętaj: profilowanie to proces iteracyjny. Znajdź problem, napraw go, zmierz poprawę i powtórz!