Refaktoryzacja kodu w Pythonie – jak poprawiać bez psucia

Kacper Sieradziński
Kacper Sieradziński
23 marca 2025Edukacja4 min czytania

Refaktoryzacja to systematyczne ulepszanie struktury kodu bez zmiany jego zachowania. Celem jest czytelność, mniejsza złożoność, łatwiejsze testowanie i tańsze wprowadzanie zmian. Jeśli poprawiasz bez testów i bez planu, ryzykujesz regresje. Jeśli robisz to metodycznie, podnosisz jakość bez psucia logiki.

Obraz główny Refaktoryzacja kodu w Pythonie – jak poprawiać bez psucia

Dlaczego refaktoryzacja w Pythonie ma sens

  • Python sprzyja szybkim iteracjom, co zwykle kończy się długimi funkcjami, zduplikowaną logiką i nielogiczną strukturą modułów.
  • Język oferuje proste mechanizmy rozbijania kodu: funkcje, moduły, klasy, dataclasses, właściwości, protokoły, typy.
  • Ekosystem ma dojrzałe narzędzia automatyzujące porządki: formatowanie, linting, analiza złożoności, sprawdzanie typów.

Zasada numer 1: siatka bezpieczeństwa

Bezpieczeństwo zmian zapewnia zestaw praktyk, które uruchamiasz przed refaktoryzacją.

  1. Testy jednostkowe i integracyjne uruchamiane jednym poleceniem.
  2. Pomiar pokrycia, by wiedzieć, co jest chronione.
  3. Statyczna analiza i formatowanie, by uprościć diff.
  4. Małe commity i feature toggle dla większych zmian.
  5. CI, które blokuje merge, gdy testy lub jakość spadają.

Przykładowy baseline narzędzi:

Bash
1 2 3 4 5 6 pytest -q coverage run -m pytest coverage report ruff check . black --check . mypy .

Workflow bezpiecznej refaktoryzacji

  1. Wybierz mały zakres. Jedna funkcja, jeden moduł, jedna odpowiedzialność.
  2. Zabezpiecz zachowanie testami charakterystycznymi.
  3. Ustal metryki startowe: złożoność, długość funkcji, liczba zależności.
  4. Refaktoruj w krokach atomowych. Po każdej zmianie uruchamiaj testy.
  5. Zatrzymaj się, gdy metryki i czytelność są wystarczające.

Code smells w Pythonie i jak je usuwać

Długa funkcja

Smell: zbyt wiele gałęzi, zagnieżdżenia, zmienne o niejasnym zakresie.

Techniki: Extract Function, Extract Parameter Object, Decompose Conditional.

Przed:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def place_order(user, items, inventory, notifier): if not user.is_active: raise ValueError("inactive") total = 0 discounts = 0 for it in items: if it.sku not in inventory: raise ValueError("missing") stock = inventory[it.sku] if stock < it.qty: raise ValueError("stock") price = it.price * it.qty if it.qty >= 3: discounts += price * 0.1 total += price payable = total - discounts notifier.send(user.email, f"pay {payable}") return payable

Po:

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 25 26 27 28 29 30 31 from dataclasses import dataclass @dataclass(frozen=True) class Line: sku: str qty: int price: float def assert_active(user): if not user.is_active: raise ValueError("inactive") def assert_stock(items, inventory): for it in items: if it.sku not in inventory: raise ValueError("missing") if inventory[it.sku] < it.qty: raise ValueError("stock") def total(items): return sum(it.price * it.qty for it in items) def discounts(items): return sum(it.price * it.qty * 0.1 for it in items if it.qty >= 3) def place_order(user, items, inventory, notifier): assert_active(user) assert_stock(items, inventory) payable = total(items) - discounts(items) notifier.send(user.email, f"pay {payable}") return payable

Duży if-elif (dispatch na typ/rodzaj)

Smell: długi łańcuch warunków sterujących zachowaniem.

Techniki: Strategy, dict dispatch, klasy polimorficzne.

Przed:

Python
1 2 3 4 5 6 7 8 9 def price(kind, value): if kind == "standard": return value elif kind == "vip": return value * 0.9 elif kind == "staff": return 0 else: raise ValueError("kind")

Po (dict dispatch):

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def _standard(v): return v def _vip(v): return v * 0.9 def _staff(v): return 0 DISPATCH = {"standard": _standard, "vip": _vip, "staff": _staff} def price(kind, value): try: return DISPATCH[kind](value) except KeyError: raise ValueError("kind")

Zduplikowany kod

Smell: te same bloki w wielu miejscach.

Techniki: Extract Function, Template Method, deduplikacja na poziomie modułu.

Przed:

Python
1 2 3 4 5 6 7 8 9 def load_json(path): import json with open(path) as f: return json.load(f) def load_settings(path): import json with open(path) as f: return json.load(f)

Po:

Python
1 2 3 4 5 6 7 import json def load_json(path): with open(path) as f: return json.load(f) load_settings = load_json

Parametry boolowskie

Smell: parametry sterujące logiką zwiększają gałęzie i złożoność.

Techniki: Split Function, Strategy, dwa punkty wejścia.

Przed:

Python
1 2 3 4 def render(data, pretty=False): if pretty: return format_pretty(data) return format_compact(data)

Po:

Python
1 2 3 4 5 def render_pretty(data): return format_pretty(data) def render_compact(data): return format_compact(data)

Mutowalne domyślne argumenty

Smell: niespodziewane współdzielenie stanu.

Technika: None sentinel.

Przed:

Python
1 2 3 def add(x, acc=[]): acc.append(x) return acc

Po:

Python
1 2 3 4 def add(x, acc=None): acc = [] if acc is None else acc acc.append(x) return acc

Kluczowe techniki refaktoryzacji w Pythonie

Extract Function / Inline Function

Rozbijanie lub scalanie funkcji dla czytelności.

Python
1 2 def net_price(brutto, vat): return brutto / (1 + vat)

Rename Symbol z konsekwencją typów

Nazwy powinny oddawać zamiar. Wspieraj się typami.

Python
1 2 3 4 from typing import Iterable def sum_weights(values: Iterable[float]) -> float: return sum(values)

Replace Magic Numbers with Named Constants

Python
1 2 3 4 EARLY_BIRD_DISCOUNT = 0.15 def apply_early_bird(value): return value * (1 - EARLY_BIRD_DISCOUNT)

Introduce Dataclass zamiast luźnych dictów

Python
1 2 3 4 5 6 from dataclasses import dataclass @dataclass(frozen=True) class Money: amount: float currency: str

Encapsulate Field przez właściwości

Python
1 2 3 4 5 6 7 class User: def __init__(self, email): self._email = email @property def email(self): return self._email

Dependency Injection

Zależności przekazuj z zewnątrz, łatwiej testować.

Przed:

Python
1 2 3 4 import requests def fetch(url): return requests.get(url).json()

Po:

Python
1 2 def fetch(url, http_get): return http_get(url).json()

Wydzielanie interfejsów przez Protocol

Python
1 2 3 4 5 from typing import Protocol class Notifier(Protocol): def send(self, to: str, message: str) -> None: ...

Pure Functions i ograniczenie efektów ubocznych

Dane do środka, wynik na zewnątrz, brak modyfikacji globalnego stanu.

Typy i statyczna analiza jako dźwignia refaktoryzacji

  • mypy lub pyright ograniczają ryzyko łamania kontraktów.
  • Stopniowe typowanie: zacznij od new code only, potem krytyczne moduły.
  • Konwencja: from __future__ import annotations w większych projektach.

Przykład dodania typów podczas refaktoryzacji:

Python
1 2 3 4 5 from typing import Iterable def average(values: Iterable[float]) -> float: vals = list(values) return sum(vals) / len(vals)

Metryki jakości, które mają znaczenie

  • Złożoność cyklomatyczna i kognitywna
  • Długość funkcji i liczba parametrów
  • Coupling i liczba zależności modułu
  • Pokrycie liniowe i gałęziowe

Narzędzia:

Bash
1 2 3 radon cc -s -a . ruff check . black .

Automaty, które utrzymują porządek

  • Formatowanie: black, sort importów: isort
  • Linting: ruff
  • Typy: mypy lub pyright
  • Pokrycie: coverage, raport HTML
  • Pre-commit: automatyczne porządki przed każdym commitem

Konfiguracja pre-commit:

YAML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 repos: - repo: https://github.com/psf/black rev: 24.8.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.8 hooks: - id: ruff - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort

Przykład refaktoryzacji krok po kroku

Wejście: długi, proceduralny parser CSV.

Przed:

Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import csv def load_users(path): with open(path) as f: rows = list(csv.reader(f)) data = [] for r in rows[1:]: if not r or len(r) < 3: continue email = r[0].strip().lower() name = r[1].strip().title() active = r[2].strip().lower() == "true" if "@" not in email: continue data.append((email, name, active)) return data

Krok 1: Wydziel transformacje.

Python
1 2 3 4 5 6 7 8 def normalize_email(v): return v.strip().lower() def normalize_name(v): return v.strip().title() def to_bool(v): return v.strip().lower() == "true"

Krok 2: Uprość przepływ danymi.

Python
1 2 3 4 5 def parse_row(row): email = normalize_email(row[0]) name = normalize_name(row[1]) active = to_bool(row[2]) return email, name, active

Krok 3: Filtry jako funkcje.

Python
1 2 3 4 5 def valid_row(row): return row and len(row) >= 3 def valid_email(email): return "@" in email

Krok 4: Główna funkcja jako pipeline.

Python
1 2 3 4 5 6 7 8 9 import csv def load_users(path): with open(path) as f: rows = list(csv.reader(f)) records = (r for r in rows[1:] if valid_row(r)) parsed = (parse_row(r) for r in records) cleaned = [p for p in parsed if valid_email(p[0])] return cleaned

Efekt: prostsze testowanie, mniejsza złożoność, jawne kroki przetwarzania.

Wzorce architektoniczne wspierające refaktoryzację

  • Warstwowanie modułów: domena, aplikacja, infrastruktura.
  • Strangler Fig: nowe ścieżki obok starego kodu, stopniowe przełączanie.
  • Branch by Abstraction: interfejs wprowadzony przed wymianą implementacji.
  • Feature toggles: kontrola wdrożenia bez rozgałęziania kodu.

Ryzyka i jak je neutralizować

  • Zmiany publicznych interfejsów wymagają wersjonowania i deprecacji.
  • Ukryte sprzężenia testów integracyjnych łamiesz najczęściej. Uruchamiaj je częściej niż rzadziej.
  • Dane produkcyjne są niejednorodne. Dodaj testy property-based.

Przykład property-based:

Python
1 2 3 4 5 6 from hypothesis import given, strategies as st @given(st.lists(st.floats(allow_nan=False, allow_infinity=False), min_size=1)) def test_average_total_is_between_min_and_max(values): a = average(values) assert min(values) <= a <= max(values)

Lista kontrolna refaktoryzacji w Pythonie

  1. Testy zielone i coverage raport dostępny.
  2. Mały zakres zmian, commit po każdej atomowej operacji.
  3. Usuwanie duplikacji przed optymalizacją.
  4. Nazwy odzwierciedlają zamiar.
  5. Funkcje krótkie, jedna odpowiedzialność.
  6. Typy dodane w publicznych interfejsach.
  7. Lint i format przechodzą bez wyjątków.
  8. Dokumentacja modułu aktualna.

FAQ: refaktoryzacja w Pythonie

Kiedy przestać refaktoryzować

Gdy marginalny zysk w czytelności nie rekompensuje czasu i ryzyka. Wróć przy następnym zadaniu dotykającym ten fragment.

Czy zawsze dodawać typy

Dodawaj w modułach publicznych, domenowych i krytycznych ścieżkach. W kodzie pomocniczym wystarczy czytelna nazwa i test.

Co z wydajnością

Najpierw popraw strukturę i testowalność. Optymalizuj dopiero po pomiarze.

Podsumowanie

Refaktoryzacja w Pythonie to rzemiosło: małe kroki, powtarzalny proces, stała ochrona testami i narzędziami. Cel jest prosty: kod, który da się szybko zrozumieć, bezpiecznie zmieniać i tanio utrzymywać. Trzymaj się siatki bezpieczeństwa, refaktoruj małymi porcjami i mierz efekty. Tylko tyle i aż tyle.