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ą.
- Testy jednostkowe i integracyjne uruchamiane jednym poleceniem.
- Pomiar pokrycia, by wiedzieć, co jest chronione.
- Statyczna analiza i formatowanie, by uprościć diff.
- Małe commity i feature toggle dla większych zmian.
- CI, które blokuje merge, gdy testy lub jakość spadają.
Przykładowy baseline narzędzi:
Bash1 2 3 4 5 6pytest -q coverage run -m pytest coverage report ruff check . black --check . mypy .
Workflow bezpiecznej refaktoryzacji
- Wybierz mały zakres. Jedna funkcja, jeden moduł, jedna odpowiedzialność.
- Zabezpiecz zachowanie testami charakterystycznymi.
- Ustal metryki startowe: złożoność, długość funkcji, liczba zależności.
- Refaktoruj w krokach atomowych. Po każdej zmianie uruchamiaj testy.
- 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:
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18def 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:
Python1 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 31from 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:
Python1 2 3 4 5 6 7 8 9def 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):
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16def _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:
Python1 2 3 4 5 6 7 8 9def 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:
Python1 2 3 4 5 6 7import 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:
Python1 2 3 4def render(data, pretty=False): if pretty: return format_pretty(data) return format_compact(data)
Po:
Python1 2 3 4 5def 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:
Python1 2 3def add(x, acc=[]): acc.append(x) return acc
Po:
Python1 2 3 4def 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.
Python1 2def net_price(brutto, vat): return brutto / (1 + vat)
Rename Symbol z konsekwencją typów
Nazwy powinny oddawać zamiar. Wspieraj się typami.
Python1 2 3 4from typing import Iterable def sum_weights(values: Iterable[float]) -> float: return sum(values)
Replace Magic Numbers with Named Constants
Python1 2 3 4EARLY_BIRD_DISCOUNT = 0.15 def apply_early_bird(value): return value * (1 - EARLY_BIRD_DISCOUNT)
Introduce Dataclass zamiast luźnych dictów
Python1 2 3 4 5 6from dataclasses import dataclass @dataclass(frozen=True) class Money: amount: float currency: str
Encapsulate Field przez właściwości
Python1 2 3 4 5 6 7class 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:
Python1 2 3 4import requests def fetch(url): return requests.get(url).json()
Po:
Python1 2def fetch(url, http_get): return http_get(url).json()
Wydzielanie interfejsów przez Protocol
Python1 2 3 4 5from 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
mypylubpyrightograniczają ryzyko łamania kontraktów.- Stopniowe typowanie: zacznij od new code only, potem krytyczne moduły.
- Konwencja:
from __future__ import annotationsw większych projektach.
Przykład dodania typów podczas refaktoryzacji:
Python1 2 3 4 5from 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:
Bash1 2 3radon cc -s -a . ruff check . black .
Automaty, które utrzymują porządek
- Formatowanie:
black, sort importów:isort - Linting:
ruff - Typy:
mypylubpyright - Pokrycie:
coverage, raport HTML - Pre-commit: automatyczne porządki przed każdym commitem
Konfiguracja pre-commit:
YAML1 2 3 4 5 6 7 8 9 10 11 12 13 14 15repos: - 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:
Python1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16import 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.
Python1 2 3 4 5 6 7 8def 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.
Python1 2 3 4 5def 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.
Python1 2 3 4 5def valid_row(row): return row and len(row) >= 3 def valid_email(email): return "@" in email
Krok 4: Główna funkcja jako pipeline.
Python1 2 3 4 5 6 7 8 9import 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:
Python1 2 3 4 5 6from 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
- Testy zielone i coverage raport dostępny.
- Mały zakres zmian, commit po każdej atomowej operacji.
- Usuwanie duplikacji przed optymalizacją.
- Nazwy odzwierciedlają zamiar.
- Funkcje krótkie, jedna odpowiedzialność.
- Typy dodane w publicznych interfejsach.
- Lint i format przechodzą bez wyjątków.
- 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.



