Python - podstawy
Materiał przygotowany z pomocą AI
Dane w przykładach
Wartości użyte w przykładach hydrologicznych (kody stacji, stany wody, przepływy, rzędne zera wodowskazów) są ilustracyjne — wybrane tak, żeby były wiarygodne dla danej stacji, ale nie były weryfikowane i mogą różnić się od rzeczywistych. Wszystkie fragmenty kodu są kompletne i samodzielnie wykonalne na CPython 3.10+ (chyba że oznaczono inaczej).
Środowisko do uruchomienia
Bloki kodu zostały zweryfikowane w Pythonie 3.12.12 z zafiksowanymi wersjami pakietów. Pełna instrukcja odtworzenia środowiska: Środowisko Python dla przykładów.
Python to interpretowany, dynamicznie typowany język programowania ogólnego przeznaczenia, znany z czytelnej składni i bardzo szerokiego ekosystemu bibliotek. Dominuje w analizie danych, automatyzacji, uczeniu maszynowym i zastosowaniach naukowych — w tym w hydrologii, meteorologii i GIS. Większość narzędzi opisanych w pozostałych wpisach tej kategorii (Hydrograf, Hydrolog, IMGWTools, Kartograf) jest napisana właśnie w Pythonie.
Ten wpis przedstawia fundamenty: czym Python jest i jak działa, jak go zainstalować i uruchomić, jakie ma typy danych i struktury, jak pisze się funkcje i klasy, jak organizuje kod w modułach i jak zarządza zależnościami. Przykłady proste mają charakter ogólny, zaawansowane trzymają się domeny hydrologicznej — odczytu pliku CSV ze stacjami, obliczeń statystycznych, zapisu raportów.
Czym jest Python
Python został stworzony przez Guido van Rossuma na przełomie lat 80. i 90. Jego cechy:
- interpretowany — kod nie jest kompilowany do pliku wykonywalnego; uruchamia go interpreter linia po linii (de facto: bytecode wykonywany w wirtualnej maszynie);
- dynamicznie typowany — typ zmiennej ustala się w czasie wykonania, ta sama nazwa może wskazywać na obiekty różnych typów;
- wieloparadygmatowy — wspiera programowanie strukturalne, obiektowe i funkcyjne;
- z zarządzaną pamięcią — programista nie alokuje ani nie zwalnia pamięci ręcznie, robi to garbage collector.
Wersje i implementacje
Aktywnie rozwijana jest linia Python 3.x — kolejne wersje wychodzą rocznie (3.10, 3.11, 3.12, 3.13…). Każda dokłada nowe konstrukcje składniowe i optymalizacje wydajności, ale zachowuje wsteczną kompatybilność na poziomie semantyki języka.
| Implementacja | Charakterystyka |
|---|---|
| CPython | Referencyjna, napisana w C — to ją mamy na myśli mówiąc „Python” |
| PyPy | Alternatywna z JIT-em, kilkukrotnie szybsza dla długich obliczeń numerycznych |
| MicroPython | Okrojony Python na mikrokontrolery (ESP32, Raspberry Pi Pico) |
| Jython | Python na JVM (rzadziej używany) |
| IronPython | Python na .NET (rzadziej używany) |
Python 2 — koniec wsparcia
Python 2 oficjalnie zakończył życie 1 stycznia 2020 roku. Nowy kod piszemy wyłącznie w Pythonie 3. Jeśli trafiasz na poradnik z
print "x"(bez nawiasów) — to Python 2, pomijaj.
Jak Python wykonuje kod
Po uruchomieniu skryptu CPython kompiluje go do bytecode’u (pliki .pyc w katalogu __pycache__/), a potem wykonuje go w wewnętrznej maszynie wirtualnej. Dla użytkownika cały proces wygląda jak „uruchomienie kodu źródłowego” — kompilacja jest niewidoczna i w 99% przypadków nie ma znaczenia.
Tryby uruchomienia:
- REPL (Read-Eval-Print Loop) — interaktywna konsola, świetna do eksperymentów; uruchamiana komendą
python3bez argumentów; - skrypt —
python3 nazwa_skryptu.py; - moduł —
python3 -m nazwa_modułu(np.python3 -m http.server); - inline —
python3 -c "print('test')"dla jednolinijkowca.
Zen of Python (PEP 20)
Filozofia języka — wpisana w sam Python jako ukryty moduł:
import thisKluczowe zasady (skrót): czytelne jest lepsze niż sprytne, proste lepsze niż złożone, jawne lepsze niż domniemane, jest jeden — najlepiej tylko jeden — oczywisty sposób żeby coś zrobić. Te zasady są często cytowane podczas dyskusji o stylu kodu.
Instalacja i uruchamianie
Instalacja: Linux / macOS / Windows
Większość dystrybucji Linuksa ma Pythona w pakietach systemowych:
# Debian / Ubuntu
sudo apt install python3 python3-pip python3-venv
# Fedora / RHEL
sudo dnf install python3 python3-pipmacOS: najprościej przez Homebrew (instalacja Homebrew opisana w Komendy terminala - Linux i macOS):
brew install [email protected]Windows: pobierz instalator z python.org/downloads. Podczas instalacji zaznacz „Add Python to PATH” — bez tego komenda python z konsoli nie zadziała.
Sprawdzenie, czy Python jest zainstalowany:
python3 --versionWynik dla świeżej instalacji 3.12 wygląda tak:
Python 3.12.5
pyenv — zarządzanie wieloma wersjami Pythona
pyenv to zewnętrzne narzędzie, które pozwala zainstalować na jednej maszynie wiele wersji Pythona obok siebie i przełączać się między nimi per-projekt. Bez niego utykamy z wersją systemową, a różne projekty mogą wymagać różnych wersji (np. starszy moduł działa tylko na 3.10, nowy projekt na 3.12). Szczegóły w sekcji Wersje, środowiska wirtualne i zależności.
REPL i pierwszy skrypt
REPL uruchamiamy bez argumentów:
$ python3
Python 3.12.5 (main, Aug 6 2024, 19:08:49)
>>> 2 + 2
4
>>> print("Witaj")
Witaj
>>> exit()Skrypt to plik z rozszerzeniem .py. Najprostszy skrypt:
# plik: 01_hello_world.py
print("Witaj, Świecie!")Uruchomienie:
python3 hello.pyShebang i python3 vs python
Jeśli chcemy uruchamiać skrypt bezpośrednio (./hello.py), pierwsza linia pliku musi zawierać tzw. shebang wskazujący interpreter:
#!/usr/bin/env python3
print("Witaj")Plik trzeba dodatkowo oznaczyć jako wykonywalny komendą chmod +x hello.py (szczegóły w Komendy terminala - Linux i macOS).
Na większości systemów python (bez trójki) wskazuje na Pythona 2 lub w ogóle nie istnieje. Bezpieczne jest pisać python3 — i tego konwencjonalnie używamy w shebangach.
Składnia i konwencje
Wcięcia jako składnia
W Pythonie wcięcia są częścią składni — definiują bloki kodu. W innych językach mamy nawiasy klamrowe { }, tu mamy spacje:
x = 10
if x > 5:
print("duża wartość")
print("nadal w bloku if")
print("już poza if")Standard (PEP 8) to 4 spacje na poziom. Mieszanie spacji i tabulatorów to klasyczne źródło IndentationError. Większość edytorów konfiguruje się tak, by tab wstawiał 4 spacje — ustawienie warto zweryfikować od razu.
Komentarze i docstring
# Komentarz jednoliniowy
x = 5 # komentarz inline
"""
Wieloliniowy string używany jako komentarz.
W Pythonie nie ma osobnej składni dla komentarzy blokowych
- używa się stringów wieloliniowych.
"""
def policz_srednia(wartosci):
"""Zwraca średnią arytmetyczną listy liczb.
Pierwsza linia to skrótowy opis. Po pustej linii - szczegóły.
Docstring jest dostępny w runtime jako `policz_srednia.__doc__`.
"""
return sum(wartosci) / len(wartosci)
# Zwraca pierwszą linię docstringa
print(policz_srednia.__doc__.splitlines()[0])Docstring (string jako pierwsza instrukcja w funkcji, klasie lub module) jest standardową formą dokumentacji — czytany przez help(), IDE i generatory dokumentacji. Konwencje opisuje PEP 257. Bardziej rozbudowany format z sekcjami parametrów, wartości zwracanych i przykładów (styl NumPy używany w narzędziach hydrologicznych) pokazuje sekcja Docstring jako dokumentacja niżej.
Identyfikatory i konwencje nazewnictwa
| Element | Konwencja | Przykład |
|---|---|---|
| Funkcje, zmienne, moduły | snake_case | policz_srednia, stan_wody |
| Klasy | PascalCase | StacjaPomiarowa, Pomiar |
| Stałe | UPPER_SNAKE_CASE | MAX_PRZEPLYW, API_URL |
| Prywatne (konwencja) | prefiks _ | _pomocnicza, _cache |
| Magiczne (Python sam) | dunder __x__ | __init__, __str__ |
Identyfikatory nie mogą zaczynać się od cyfry, zawierać myślnika ani spacji. Mogą zawierać polskie znaki, ale w praktyce ich unikamy — są źródłem problemów z encoding-iem między systemami.
Typowanie dynamiczne i type hints
Python jest dynamicznie typowany — typ wynika z aktualnej zawartości:
x = 10
print(type(x))
x = 10.0
print(type(x))
x = "tekst"
print(type(x))
x = [1, 2, 3]
print(type(x))Od wersji 3.5 można dodawać podpowiedzi typów (PEP 484). Nie wpływają na działanie programu, ale dokumentują intencję i są weryfikowane przez statyczne narzędzia (mypy, pyright):
def policz_srednia(wartosci: list[float]) -> float:
return sum(wartosci) / len(wartosci)
print(policz_srednia([1.5, 2.5, 3.0]))Type hints w runtime
CPython wczytuje adnotacje, ale nie sprawdza ich w czasie działania — to nie jest TypeScript ani Java. Funkcja zadeklarowana jako zwracająca
floatzwróci nawet stringa, jeśli tak ją napiszesz, i nie dostaniesz błędu od interpretera. Walidację daje dopiero zewnętrzny linter typów (mypy).
Kodowanie pliku źródłowego
Python 3 traktuje pliki .py jako UTF-8 domyślnie — można swobodnie używać polskich znaków w komentarzach, stringach i (technicznie) w identyfikatorach. W kodzie z czasów Pythona 2 oraz w niektórych szablonach do dziś można spotkać deklarację kodowania w pierwszej lub drugiej linii pliku — tzw. coding cookie opisany w PEP 263:
# -*- coding: utf-8 -*-Równoważnie:
# coding: utf-8Jeśli plik zaczyna się od shebanga, deklaracja kodowania trafia do drugiej linii:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-W nowym kodzie pisanym wyłącznie w Pythonie 3 deklaracja jest niepotrzebna — interpreter zakłada UTF-8. Nadal pojawia się w starszych projektach i w niektórych przewodnikach stylu (np. Google Python Style Guide) „dla jasności”.
To nie to samo, co
encoding=przyopen()Domyślne kodowanie pliku źródłowego (UTF-8 w Pythonie 3) i domyślne kodowanie używane przy otwieraniu plików danych funkcją
open()to dwie różne sprawy. To drugie zależy od ustawień systemu (locale) i bywa różne między Linuxem, macOS a Windowsem — dlatego w funkcjiopen()zawsze podajemy parametrencoding="utf-8"jawnie. Patrz sekcja Operacje na plikach.
Typy podstawowe
Liczby
| Typ | Co reprezentuje | Przykład |
|---|---|---|
int | Liczba całkowita (dowolna długość) | 42, -7, 10**100 |
float | Liczba zmiennoprzecinkowa (IEEE 754 double) | 3.14, 1e-3, inf |
complex | Liczba zespolona | 2+3j |
a = 7
b = 2
print(a + b) # 9
print(a - b) # 5
print(a * b) # 14
print(a / b) # 3.5 - dzielenie zawsze daje float
print(a // b) # 3 - dzielenie całkowite (zaokrąglenie w dół)
print(a % b) # 1 - reszta z dzielenia
print(a ** b) # 49 - potęgowanie
floatma ograniczoną precyzję
0.1 + 0.2w Pythonie nie daje dokładnie0.3— to konsekwencja standardu IEEE 754 (taka sama jak w SQL, Javie czy JavaScripcie). Dla wartości z domeny finansowej używaj klasydecimal.Decimal. Dla pomiarów fizycznych z kontrolowaną niepewnościąfloatzwykle wystarcza, bo już sam pomiar ma większy błąd niż precyzja typu.
Łańcuchy znaków (str)
imie = "Anna" # apostrofy lub cudzysłowy
opis = 'tekst z "cytatem" w środku'
wieloliniowy = """linia 1
linia 2"""
# konkatenacja
print("Witaj, " + imie)
# Cytaty
print(opis)
# wieloliniowy
print(wieloliniowy)
# powielenie
print("-" * 20)
# długość
print(len(imie)) # 4
# indeksowanie i slicing
slowo = "hydrologia"
print(slowo[0]) # h
print(slowo[-1]) # a (ostatni)
print(slowo[0:5]) # hydro
print(slowo[::-1]) # aigolordyh (od końca)f-stringi (od Pythona 3.6) — zalecany sposób formatowania:
stacja = "Poznań-Most Rocha"
przeplyw = 64.2
print(f"Stacja {stacja}: przepływ {przeplyw:.1f} m³/s"){wyrażenie:format} — po dwukropku idzie specyfikator: .1f (jedno miejsce po przecinku), 04d (liczba uzupełniona zerami do 4 znaków), >10 (wyrównanie do prawej w polu szerokości 10). Stringi w Pythonie są niemutowalne — nie da się zmienić znaku w istniejącym stringu, można tylko utworzyć nowy.
Wartości logiczne i None
prawda = True
falsz = False
brak = None
print(prawda and falsz) # False
print(prawda or falsz) # True
print(not prawda) # False
# typowy idiom - sprawdzenie None
wynik = None
if wynik is None:
print("brak wyniku")None to specjalna wartość oznaczająca „brak wartości” (analogicznie do NULL w SQL). Porównujemy z nią przez is None / is not None, nie przez == None.
W kontekstach logicznych wartości „fałsz” są: False, None, 0, 0.0, "" (pusty string), [] (pusta lista), {}, set(). Wszystko inne traktowane jest jako „prawda”.
Konwersje typów
# string -> liczba
x = int("42")
y = float("3.14")
# liczba -> string
napis = str(42)
# float -> int (obcięcie, nie zaokrąglenie!)
print(int(3.9)) # 3
print(int(-3.9)) # -3
# zaokrąglenie
print(round(3.5)) # 4
print(round(3.14159, 2)) # 3.14Próba konwersji nieprzystającego stringa rzuca ValueError:
try:
x = int("abc")
except ValueError as e:
print(f"nie udało się: {e}")Operatory
Arytmetyczne, porównania, logiczne
| Operator | Znaczenie |
|---|---|
+, -, *, / | Dodawanie, odejmowanie, mnożenie, dzielenie zwykłe |
// | Dzielenie całkowite |
% | Modulo (reszta) |
** | Potęgowanie |
==, != | Równe, różne |
<, >, <=, >= | Porównania |
and, or, not | Operatory logiczne |
in, not in | Należy / nie należy do kolekcji |
is, is not | Identyczność obiektów (ten sam obiekt w pamięci) |
in / not in, is / is not
in działa na każdej kolekcji wspierającej iterację:
print(3 in [1, 2, 3]) # True
print("Warta" in ("Warta", "Wisła")) # True
print("h" in "hydrologia") # True
print("brak" not in {"a", "b"}). # Trueis sprawdza tożsamość obiektów — czy dwie nazwy wskazują na ten sam obiekt w pamięci, nie czy ich wartości są równe:
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True - wartości są równe
print(a is b) # False - to różne obiekty w pamięci
print(a is c) # True - c i a wskazują na ten sam obiekt
isna liczbach i stringach — pułapkaCPython cache’uje małe liczby (-5 do 256) oraz krótkie stringi. Dlatego
1000 is 1000może wyjśćTruew REPL-u, aleFalsew skrypcie albo w innej wersji interpretera. Nigdy nie używajisdo porównywania wartości — tylko dois None,is True,is False(te trzy są singletonami i porównanie przezisjest jedyną poprawną formą).
Struktury danych
Wbudowane kolekcje to lista, krotka, słownik i zbiór. Każda ma inną charakterystykę pod kątem mutowalności, kolejności i indeksowania.
| Struktura | Składnia | Mutowalna? | Uporządkowana? | Duplikaty? | Indeksowanie |
|---|---|---|---|---|---|
list | [1, 2, 3] | tak | tak | tak | po pozycji |
tuple | (1, 2, 3) | nie | tak | tak | po pozycji |
dict | {"a": 1} | tak | tak (od 3.7) | klucze nie | po kluczu |
set | {1, 2, 3} | tak | nie | nie | brak |
frozenset | frozenset([1, 2]) | nie | nie | nie | brak |
Lista (list)
stacje = ["Poznań", "Warszawa", "Kalisz"]
stacje.append("Wrocław") # dodanie na koniec
stacje.insert(1, "Kraków") # wstawienie na pozycję 1
stacje.remove("Kalisz") # usunięcie po wartości
ostatnia = stacje.pop() # pobranie i usunięcie ostatniej
print(stacje)
print(f"ostatnia: {ostatnia}")
# slicing
print(stacje[0:2]) # pierwsze dwa
print(stacje[::-1]) # odwrócona kopia
# sortowanie
liczby = [3, 1, 4, 1, 5, 9, 2, 6]
liczby.sort() # sortuje w miejscu
print(liczby)
posortowane = sorted([3, 1, 2]) # zwraca nową listę, nie modyfikuje
print(posortowane)Krotka (tuple)
Krotka to niemutowalna lista. Przydaje się, gdy chcemy mieć pewność, że dane się nie zmienią — np. współrzędne stacji albo wynik funkcji zwracającej wiele wartości.
poznan_most_rocha = (52.41, 16.95) # (lat, lon)
print(poznan_most_rocha[0])
# rozpakowanie
lat, lon = poznan_most_rocha
print(f"lat={lat}, lon={lon}")
# krotka z jednym elementem - przecinek konieczny
jednoelementowa = (5,)
print(type(jednoelementowa))Funkcje często zwracają krotkę — najprościej rozpakować ją od razu:
def min_max(wartosci):
return min(wartosci), max(wartosci)
a, b = min_max([10, 5, 7, 12, 3])
print(f"min={a}, max={b}")Słownik (dict)
Mapuje klucze na wartości. Kluczem może być dowolna wartość niemutowalna (string, liczba, krotka).
stacja = {
"kod_imgw": "152160140",
"nazwa": "Poznań-Most Rocha",
"rzeka": "Warta",
"rzedna_zera": 49.54,
}
# odczyt
print(stacja["nazwa"])
print(stacja.get("brak_klucza")) # None zamiast KeyError
print(stacja.get("brak_klucza", "n/d")) # wartość domyślna
# modyfikacja
stacja["dorzecze"] = "Odra"
del stacja["rzedna_zera"]
# iteracja
for klucz, wartosc in stacja.items():
print(f"{klucz}: {wartosc}")Zbiór (set, frozenset)
Nieuporządkowana kolekcja unikalnych wartości — naturalnie modeluje „zbiór” w sensie matematycznym.
rzeki = {"Warta", "Wisła", "Odra"}
rzeki.add("Prosna")
rzeki.add("Warta") # duplikat - ignorowany
print(rzeki)
# operacje matematyczne
a = {1, 2, 3}
b = {2, 3, 4}
print(a | b) # suma: {1, 2, 3, 4}
print(a & b) # przecięcie: {2, 3}
print(a - b) # różnica: {1}
print(a ^ b) # różnica symetryczna: {1, 4}Typowe zastosowanie: usuwanie duplikatów z listy.
lista_z_powtorzeniami = ["Warta", "Wisła", "Warta", "Odra", "Wisła"]
unikalne = list(set(lista_z_powtorzeniami))
print(unikalne) # kolejność niegwarantowanafrozenset to niemutowalna wersja zbioru — można go użyć jako klucza w słowniku albo elementu innego zbioru.
Comprehensions
Zwarta składnia tworzenia listy/słownika/zbioru z istniejącej kolekcji. Czytelniejsza i zwykle szybsza niż jawna pętla.
# list comprehension
przeplywy_cm = [245, 240, 312, 238, 156]
przeplywy_m = [x / 100 for x in przeplywy_cm]
print(przeplywy_m)
# z warunkiem (filter)
duze = [x for x in przeplywy_cm if x > 200]
print(duze)
# dict comprehension
stacje = ["Poznań", "Warszawa", "Kalisz"]
indeksy = {nazwa: i for i, nazwa in enumerate(stacje)}
print(indeksy)
# set comprehension
slowo = "hydrologia"
unikalne_litery = {ch for ch in slowo}
print(unikalne_litery)Comprehension czy pętla
Comprehension jest świetne dla prostych przekształceń jeden-do-jednego z opcjonalnym filtrem. Gdy logika rośnie (zagnieżdżone warunki, wiele etapów, side-effects) — pisz zwykłą pętlę
for. Czytelność > sprytność.
Instrukcje sterujące
if / elif / else
przeplyw = 45.5
if przeplyw > 100:
kategoria = "wezbranie"
elif przeplyw > 30:
kategoria = "stan średni"
elif przeplyw > 10:
kategoria = "niżówka"
else:
kategoria = "stan ekstremalnie niski"
print(kategoria)for
Iteracja po elementach kolekcji:
stacje = ["Poznań", "Warszawa", "Kalisz"]
for s in stacje:
print(s)Pomocnicze funkcje:
# range - sekwencja liczb
for i in range(5): # 0, 1, 2, 3, 4
print(i)
for i in range(2, 8): # 2..7
print(i)
for i in range(0, 10, 2): # co 2: 0, 2, 4, 6, 8
print(i)
# enumerate - indeks + wartość
for i, s in enumerate(stacje):
print(f"{i}: {s}")
# zip - równoległa iteracja
nazwy = ["Poznań", "Warszawa"]
przeplywy = [64.2, 542.0]
for nazwa, q in zip(nazwy, przeplywy):
print(f"{nazwa}: {q} m³/s")while
x = 0
while x < 5:
print(x)
x += 1while używamy, gdy nie znamy z góry liczby iteracji — np. czytamy z czujnika dopóki nie pojawi się wartość spełniająca warunek.
break, continue i else w pętlach
# break - wyjście z pętli
for x in range(10):
if x == 5:
break
print(x) # 0, 1, 2, 3, 4
# continue - pomija resztę iteracji
for x in range(5):
if x % 2 == 0:
continue
print(x) # 1, 3
# else po pętli - wykonuje się, gdy NIE było break
for x in range(3):
print(x)
else:
print("pętla się zakończyła normalnie")else po pętli to konstrukcja rzadka i często myląca — wielu programistów czyta ją jak „else dla if w pętli”. Używaj świadomie.
match / case (Python 3.10+)
Wzorzec dopasowywania, dodany w 3.10. Bardziej rozbudowany niż switch z C czy Javy — pozwala dopasowywać też strukturę.
def opis_stanu(kategoria: str) -> str:
match kategoria:
case "wezbranie":
return "powyżej stanu ostrzegawczego"
case "stan średni" | "niżówka":
return "stan codzienny"
case _:
return "nieznana kategoria"
print(opis_stanu("wezbranie"))
print(opis_stanu("nieznane"))_ to wildcard — pasuje do wszystkiego (analog default z switch).
Funkcje
Definicja, parametry, wartości zwracane
def stan_w_metrach(stan_cm, rzedna_zera):
return rzedna_zera + stan_cm / 100
h = stan_w_metrach(245, 49.54)
print(f"{h:.2f} m n.p.m.")Funkcja bez return zwraca None.
Argumenty pozycyjne, nazwane, domyślne
def opis_stacji(nazwa, rzeka, dorzecze="Wisła"):
return f"{nazwa} ({rzeka}, dorzecze {dorzecze})"
# pozycyjnie
print(opis_stacji("Warszawa-Bulwary", "Wisła"))
# nazwane (kolejność dowolna)
print(opis_stacji(rzeka="Warta", nazwa="Poznań", dorzecze="Odra"))
# z wartością domyślną
print(opis_stacji("Kraków-Bielany", "Wisła"))Mutowalna wartość domyślna — pułapka
def f(x, y=[]):wygląda niewinnie, ale lista[]jest tworzona raz przy definicji funkcji i współdzielona między wszystkimi wywołaniami. Każdey.append(...)modyfikuje tę samą listę — zachowanie często mylnie diagnozowane jako „pamiętanie stanu między wywołaniami”. Poprawnie:def f(x, y=None):i wewnątrzif y is None: y = [].
*args i **kwargs
def policz_srednia(*wartosci):
"""Akceptuje dowolną liczbę argumentów pozycyjnych."""
return sum(wartosci) / len(wartosci)
print(policz_srednia(1, 2, 3))
print(policz_srednia(10, 20, 30, 40))
def loguj(**dane):
"""Akceptuje dowolne argumenty nazwane - trafiają do słownika."""
for klucz, wartosc in dane.items():
print(f"{klucz}={wartosc}")
loguj(stacja="Poznań", przeplyw=64.2, data="2024-05-10")Funkcje anonimowe (lambda)
kwadrat = lambda x: x ** 2
print(kwadrat(5))
# typowo używane jako argument funkcji wyższego rzędu
liczby = [3, 1, 4, 1, 5, 9, 2, 6]
posortowane = sorted(liczby, key=lambda x: -x) # malejąco
print(posortowane)lambda ma jedno wyrażenie i nie pozwala pisać wieloliniowych funkcji — celowo. Gdy logika rośnie, użyj def.
Type hints w sygnaturze
def stan_w_metrach(stan_cm: int, rzedna_zera: float) -> float:
return rzedna_zera + stan_cm / 100
print(stan_w_metrach(245, 49.54))Hints można zostawić tylko na ważnych granicach (publiczne API modułu) i pominąć w funkcjach pomocniczych — to częsta praktyka. mypy lub pyright weryfikują je statycznie poza interpreterem.
Docstring jako dokumentacja
W Pythonie funkcjonują trzy popularne konwencje formatowania docstringów:
- Google —
Args:,Returns:z wcięciem; - NumPy/numpydoc — sekcje z poziomą linią
----pod nagłówkiem; - Sphinx/reST — pola w stylu
:param x:,:returns:.
W bazie wiedzy oraz w narzędziach hydrologicznych (Hydrolog, IMGWTools, Kartograf) używamy stylu NumPy — jest najczęstszy w Pythonie naukowym, czytelny w surowej postaci pliku i wspierany przez generatory dokumentacji (Sphinx z rozszerzeniem napoleon, MkDocs Material). Ten sam docstring napisany w trzech stylach wygląda inaczej, a wymóg jest jeden: spójność w obrębie projektu.
def policz_charakterystyki(przeplywy):
"""Oblicza podstawowe statystyki serii przepływów.
Parameters
----------
przeplywy : list of float
Lista wartości przepływu w m³/s.
Returns
-------
dict
Słownik z kluczami ``min``, ``max``, ``srednia``, ``mediana``.
Examples
--------
>>> wynik = policz_charakterystyki([45.2, 67.1, 89.3, 12.5, 56.8])
>>> round(wynik["srednia"], 2)
54.18
"""
posortowane = sorted(przeplywy)
n = len(posortowane)
mediana = posortowane[n // 2] if n % 2 else (posortowane[n // 2 - 1] + posortowane[n // 2]) / 2
return {
"min": min(przeplywy),
"max": max(przeplywy),
"srednia": sum(przeplywy) / n,
"mediana": mediana,
}
wynik = policz_charakterystyki([45.2, 67.1, 89.3, 12.5, 56.8])
print(wynik)Najczęściej spotykane sekcje stylu NumPy: Parameters, Returns, Yields (dla generatorów), Raises, Notes, Examples, See Also. Linie ---- muszą mieć dokładnie tyle samo znaków co nagłówek nad nimi — jest to wymóg parsera. Krótkie, jednolinijkowe docstringi (np. """Stacja o podanym kodzie nie istnieje w bazie.""" w klasie wyjątku) są w pełni zgodne z NumPy — sekcje pojawiają się dopiero, gdy funkcja ma parametry albo zwracaną wartość wartą udokumentowania.
Programowanie obiektowe (OOP)
Klasa to szablon obiektu — definiuje, jakie atrybuty (dane) i metody (zachowania) mają jego egzemplarze.
Klasy i instancje
class Stacja:
def __init__(self, kod_imgw, nazwa, rzeka):
self.kod_imgw = kod_imgw
self.nazwa = nazwa
self.rzeka = rzeka
def opis(self):
return f"{self.nazwa} ({self.rzeka})"
s = Stacja("152160140", "Poznań-Most Rocha", "Warta")
print(s.opis())
print(s.kod_imgw)__init__— konstruktor; wywoływany przy tworzeniu instancjiself— pierwszy parametr każdej metody instancji; wskazuje na konkretny obiekt- atrybuty przypisujemy przez
self.<nazwa> = ...
Atrybuty i metody (instance / class / static)
class Pomiar:
JEDNOSTKA = "m³/s" # atrybut klasy - wspólny dla wszystkich instancji
def __init__(self, stacja_id, przeplyw):
self.stacja_id = stacja_id # atrybut instancji
self.przeplyw = przeplyw
def opis(self): # metoda instancji
return f"stacja {self.stacja_id}: {self.przeplyw} {self.JEDNOSTKA}"
@classmethod
def z_litrow(cls, stacja_id, ls): # metoda klasowa - alternatywny konstruktor
return cls(stacja_id, ls / 1000)
@staticmethod
def m3s_na_ls(m3s): # metoda statyczna - funkcja w przestrzeni klasy
return m3s * 1000
p1 = Pomiar(1, 64.2)
p2 = Pomiar.z_litrow(1, 64200)
print(p1.opis())
print(p2.opis())
print(Pomiar.m3s_na_ls(64.2))__init__, __str__, __repr__
Dunder-metody (od ang. double underscore, po polsku znane również jako metody specjalne lub metody magiczne) sterują zachowaniem obiektów w standardowych operacjach.
class Stacja:
def __init__(self, kod, nazwa):
self.kod = kod
self.nazwa = nazwa
def __str__(self):
return f"{self.nazwa} [{self.kod}]"
def __repr__(self):
return f"Stacja(kod={self.kod!r}, nazwa={self.nazwa!r})"
s = Stacja("152160140", "Poznań-Most Rocha")
print(str(s)) # przyjazna reprezentacja (dla użytkownika)
print(repr(s)) # techniczna (dla programisty); idealnie taka, że eval(repr(x)) == xprint(obiekt) używa __str__. Reprezentacja w REPL-u i w listach ([stacja, stacja]) używa __repr__. Dobry __repr__ znacznie ułatwia debugowanie.
Dziedziczenie
class Stacja:
def __init__(self, kod, nazwa):
self.kod = kod
self.nazwa = nazwa
def opis(self):
return f"Stacja {self.nazwa}"
class StacjaHydrologiczna(Stacja):
def __init__(self, kod, nazwa, rzeka, rzedna_zera):
super().__init__(kod, nazwa)
self.rzeka = rzeka
self.rzedna_zera = rzedna_zera
def opis(self):
bazowy = super().opis()
return f"{bazowy} na rzece {self.rzeka}"
s = StacjaHydrologiczna("152160140", "Poznań-Most Rocha", "Warta", 49.54)
print(s.opis())
print(isinstance(s, Stacja)) # Truesuper() daje dostęp do metod klasy bazowej. isinstance sprawdza, czy obiekt jest egzemplarzem klasy lub jakiejkolwiek z jej klas pochodnych.
Python wspiera dziedziczenie wielokrotne — klasa może mieć wiele klas bazowych. Kolejność wyszukiwania metod określa MRO (Method Resolution Order); dla większości codziennych zastosowań wystarczy dziedziczenie pojedyncze.
Dataclasses (3.7+)
Bardzo częsty wzorzec — klasa, która tylko trzyma dane. @dataclass generuje __init__, __repr__ i __eq__ automatycznie:
from dataclasses import dataclass
@dataclass
class Pomiar:
stacja_id: int
data_pomiaru: str
stan_wody: int
przeplyw: float
p = Pomiar(stacja_id=1, data_pomiaru="2024-05-10", stan_wody=245, przeplyw=64.2)
print(p) # Pomiar(stacja_id=1, data_pomiaru='2024-05-10', stan_wody=245, przeplyw=64.2)
print(p == Pomiar(1, "2024-05-10", 245, 64.2)) # TrueDataclasses to standard dla obiektów-rekordów. Mniej kodu boilerplate, lepsza czytelność.
Enkapsulacja (konwencja _ i __)
Python nie ma prawdziwego private — wszystko jest dostępne z zewnątrz. Konwencjonalnie:
_atrybut— „prywatny” przez konwencję; nie używaj z zewnątrz, choć technicznie możesz;__atrybut— name mangling: Python automatycznie zmienia nazwę na_NazwaKlasy__atrybut(ochrona przed kolizją w dziedziczeniu, nie przed celowym dostępem).
class Bufor:
def __init__(self):
self._wewnetrzny = [] # konwencja: nie tykać
self.__bardzo_prywatny = "x"
def dodaj(self, wartosc):
self._wewnetrzny.append(wartosc)
b = Bufor()
b.dodaj(1)
print(b._wewnetrzny) # technicznie działa, ale łamie konwencję
print(b._Bufor__bardzo_prywatny) # name mangling: można dostać się "ręcznie"Obsługa wyjątków
Wyjątek to obiekt sygnalizujący błąd — może zostać przechwycony i obsłużony albo (jeśli nie) zatrzymać program z komunikatem i tracebackiem.
try / except / else / finally
def parsuj_przeplyw(tekst):
try:
wartosc = float(tekst)
except ValueError:
print(f"nie udało się sparsować: {tekst!r}")
return None
else:
# wykonuje się TYLKO gdy try nie rzucił wyjątku
print(f"sparsowano: {wartosc}")
return wartosc
finally:
# wykonuje się ZAWSZE - i przy sukcesie, i przy wyjątku
print("zakończono parsowanie")
parsuj_przeplyw("64.2")
parsuj_przeplyw("brak danych")Wbudowane typy wyjątków
| Wyjątek | Kiedy występuje |
|---|---|
ValueError | Argument poprawnego typu, ale niewłaściwa wartość (int("abc")) |
TypeError | Niewłaściwy typ argumentu (len(5)) |
KeyError | Brak klucza w słowniku |
IndexError | Indeks spoza zakresu listy/krotki |
FileNotFoundError | Brak pliku (open("nieistnieje.txt")) |
ZeroDivisionError | Dzielenie przez zero |
AttributeError | Brak atrybutu w obiekcie |
ImportError | Brak modułu / błąd importu |
StopIteration | Koniec iteracji (rzadko łapany ręcznie) |
slownik = {"a": 1}
try:
print(slownik["b"])
except KeyError as e:
print(f"brak klucza: {e}")Bare
except:to anty-wzorzec
except:(bez podanego typu) lubexcept Exception:łapie też niezamierzone błędy — np.KeyboardInterrupt(Ctrl+C w długim skrypcie). Łap konkretne typy wyjątków, a w naprawdę szerokim łapaniu zostaw przynajmniejexcept Exception(nieBaseException).
Własne wyjątki i raise ... from
class StacjaNieZnaleziona(Exception):
"""Stacja o podanym kodzie nie istnieje w bazie."""
def znajdz_stacje(kod, baza):
try:
return baza[kod]
except KeyError as e:
raise StacjaNieZnaleziona(f"brak stacji o kodzie {kod}") from e
baza = {"152160140": "Poznań-Most Rocha"}
try:
znajdz_stacje("999999999", baza)
except StacjaNieZnaleziona as e:
print(e)raise NowyWyjatek(...) from oryginalny zachowuje pełen kontekst — w tracebacku zobaczysz oba wyjątki z adnotacją „The above exception was the direct cause”.
Operacje na plikach
open() i tryby
# zapis (tworzy lub nadpisuje)
with open("/tmp/test.txt", "w", encoding="utf-8") as f:
f.write("linia 1\n")
f.write("linia 2\n")
# odczyt
with open("/tmp/test.txt", "r", encoding="utf-8") as f:
zawartosc = f.read()
print(zawartosc)| Tryb | Co robi |
|---|---|
r | Odczyt (domyślny) |
w | Zapis (nadpisuje istniejący plik) |
a | Dopisywanie na koniec |
x | Tworzenie — błąd, jeśli plik istnieje |
b | Binarny (np. rb, wb dla obrazów, archiwów) |
t | Tekstowy (domyślny, można pominąć) |
+ | Otwarcie do odczytu i zapisu (np. r+) |
Zawsze podawaj
encodingDomyślne kodowanie różni się między systemami (UTF-8 na nowoczesnych Linuksach i macOS, czasem CP-1250 na Windowsie). Niejawne poleganie na domyślnym kodowaniu prowadzi do
UnicodeDecodeErrorprzy uruchomieniu skryptu na innej maszynie. Dobra praktyka: zawszeencoding="utf-8".
Context manager with
with open(...) as f: to context manager — gwarantuje, że plik zostanie zamknięty (f.close()) niezależnie od tego, czy w środku pojawi się wyjątek. To zalecany sposób pracy z plikami, soketami i większością zasobów wymagających sprzątania.
Czytanie linia po linii
# tworzymy testowy plik
with open("/tmp/lista.txt", "w", encoding="utf-8") as f:
f.write("Warta\nWisła\nOdra\n")
# wariant 1 - iteracja po linii (najefektywniejsze pamięciowo)
with open("/tmp/lista.txt", "r", encoding="utf-8") as f:
for linia in f:
print(linia.rstrip()) # rstrip usuwa końcowy \n
# wariant 2 - wszystkie linie do listy
with open("/tmp/lista.txt", "r", encoding="utf-8") as f:
linie = f.readlines()
print(linie)Standard library: csv i json
import csv
# zapis CSV
wiersze = [
{"kod": "152160140", "nazwa": "Poznań-Most Rocha", "rzeka": "Warta"},
{"kod": "152210170", "nazwa": "Warszawa-Bulwary", "rzeka": "Wisła"},
]
with open("/tmp/stacje.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["kod", "nazwa", "rzeka"])
writer.writeheader()
writer.writerows(wiersze)
# odczyt CSV
with open("/tmp/stacje.csv", "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for wiersz in reader:
print(wiersz["nazwa"])import json
dane = {
"stacja": "Poznań-Most Rocha",
"rzeka": "Warta",
"rzedna_zera": 49.54,
"wspolrzedne": [52.41, 16.95],
}
# do stringa
tekst = json.dumps(dane, ensure_ascii=False, indent=2)
print(tekst)
# do pliku
with open("/tmp/stacja.json", "w", encoding="utf-8") as f:
json.dump(dane, f, ensure_ascii=False, indent=2)
# odczyt z pliku
with open("/tmp/stacja.json", "r", encoding="utf-8") as f:
z_pliku = json.load(f)
print(z_pliku["stacja"])ensure_ascii=False zachowuje polskie znaki w plikach JSON (domyślnie kodowane są jako ó itp.).
Moduły i paczki
import, from, as
Moduł to plik .py. Paczka to katalog z plikiem __init__.py (lub od Pythona 3.3+ bez tego pliku — jako namespace package).
import math
print(math.pi)
print(math.sqrt(16))
from math import sqrt, pi
print(sqrt(16))
import math as m # alias modułu
print(m.pi)
from math import sqrt as pierwiastek # alias funkcji
print(pierwiastek(25))Gwiazdka from modul import * jest generalnie odradzana — zaśmieca przestrzeń nazw i utrudnia znalezienie, skąd pochodzi dana funkcja.
Tworzenie własnego modułu
Plik narzedzia.py:
# narzedzia.py
def stan_w_metrach(stan_cm: int, rzedna_zera: float) -> float:
return rzedna_zera + stan_cm / 100
PROG_OSTRZEGAWCZY_CM = 300Użycie z innego pliku w tym samym katalogu:
# main.py
import narzedzia
print(narzedzia.stan_w_metrach(245, 49.54))
print(narzedzia.PROG_OSTRZEGAWCZY_CM)Wybrane moduły standard library
| Moduł | Co oferuje |
|---|---|
os | Operacje systemowe (zmienne środowiskowe, ścieżki, procesy). |
sys | Argumenty linii komend, stdin/stdout, exit() |
pathlib | Obiektowy interfejs do ścieżek (nowoczesna alternatywa dla os.path) |
datetime | Daty i czas |
re | Wyrażenia regularne |
json | Serializacja JSON |
csv | Czytanie i pisanie CSV |
logging | Logowanie zdarzeń |
urllib | HTTP w bibliotece standardowej |
subprocess | Uruchamianie zewnętrznych procesów |
argparse. | Parsowanie argumentów linii komend |
Przykład pathlib:
from pathlib import Path
sciezka = Path("/tmp/test_pythona.txt")
sciezka.write_text("hello\n", encoding="utf-8")
print(sciezka.exists())
print(sciezka.suffix)
print(sciezka.read_text(encoding="utf-8"))
sciezka.unlink() # usunięcie
pathlibzamiastos.path
pathlib.Pathdaje bardziej czytelny, obiektowy interfejs do ścieżek niż klasyczneos.path. Operator/służy do łączenia (Path("/tmp") / "plik.txt"), metodyread_text(),write_text(),exists(),iterdir()upraszczają najczęstsze operacje. W nowym kodzie preferujemypathlib.
if __name__ == "__main__":
Idiom pozwalający odróżnić sytuację „skrypt jest uruchamiany bezpośrednio” od „moduł jest importowany przez inny skrypt”.
# narzedzia.py
def stan_w_metrach(stan_cm, rzedna_zera):
return rzedna_zera + stan_cm / 100
def main():
print(stan_w_metrach(245, 49.54))
if __name__ == "__main__":
main()Po imporcie z innego pliku (import narzedzia) blok main() się nie wykona. Po uruchomieniu python3 narzedzia.py — wykona się. Standardowy układ: definicje funkcji + if __name__ == "__main__": na końcu z entry pointem.
Wersje, środowiska wirtualne i zależności
pyenv — instalacja i przełączanie wersji Pythona
pyenv rozwiązuje problem „mam Pythona 3.10 z systemu, ale nowy projekt wymaga 3.12” — pozwala mieć obie wersje (i dowolnie wiele innych) na tej samej maszynie i przełączać się między nimi per-katalog.
Instalacja:
# macOS przez Homebrew
brew install pyenv
# Linux - skrypt instalacyjny
curl https://pyenv.run | bashPo instalacji trzeba dopisać do pliku startowego powłoki (~/.zshrc, ~/.bashrc):
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"Podstawowe komendy:
pyenv install --list # dostępne wersje
pyenv install 3.12.5 # instalacja
pyenv versions # zainstalowane lokalnie
pyenv global 3.12.5 # ustaw globalnie domyślną wersję
pyenv local 3.11.9 # ustaw dla bieżącego katalogu (tworzy plik .python-version)
python3 --version # potwierdzeniePlik .python-version w katalogu projektu jest commitowany do repo — gwarantuje, że każdy współtwórca i CI używają tej samej wersji Pythona. To analog package.json engines z Node.js czy .ruby-version.
Relacja pyenv ↔ venv
pyenvwybiera interpreter (którą wersję Pythona uruchamiamy).venvizoluje zależności (paczki pip per-projekt). To dwie ortogonalne sprawy — typowy stack topyenv+venvrazem: pyenv ustawia wersję, venv izoluje paczki dla tej wersji.
Po co venv
Bez izolacji wszystkie projekty na jednej maszynie korzystają z tej samej globalnej instalacji. pip install w jednym projekcie wpływa na inne — łatwo o konflikty wersji bibliotek. Wirtualne środowisko to lokalny katalog z własnym interpreterem Pythona (dowiązanie symboliczne) i własnym katalogiem na biblioteki — projekty są izolowane.
python -m venv + aktywacja
# tworzymy venv w katalogu projektu
cd /tmp/moj-projekt
python3 -m venv .venv # dobrą praktyką jest dodawnie kropki na początku
# aktywacja
source .venv/bin/activate # Linux / macOS
# .venv\Scripts\activate # Windows PowerShell
# w aktywnym venv każdy `python` i `pip` korzysta z lokalnego venv
which python
pip install requests
# dezaktywacja
deactivateAktywacja modyfikuje PATH — python i pip w terminalu wskazują na binaria z .venv/. Prompt powłoki zwykle pokazuje (venv) na początku, żeby było widać, że venv jest aktywny.
.venv/ nie jest commitowane do repo — tylko requirements.txt (lub pyproject.toml) z listą zależności.
pip install / freeze, requirements.txt
# instalacja konkretnej paczki
pip install requests
# instalacja konkretnej wersji
pip install "requests==2.31.0"
# instalacja z minimalną wersją
pip install "requests>=2.30"
# lista zainstalowanych paczek
pip list
# eksport do requirements.txt
pip freeze > requirements.txt
# odtworzenie środowiska na innej maszynie
pip install -r requirements.txtrequirements.txt to płaska lista paczek z wersjami:
requests==2.31.0
pandas==2.2.2
numpy==1.26.4
Jest commitowany do repo — gwarantuje, że każdy programista (i CI) zainstaluje dokładnie te same wersje paczek.
pyproject.toml — wzmianka
Nowsze projekty używają pyproject.toml (PEP 621) zamiast requirements.txt — pojedynczy plik konfiguracyjny opisujący zależności, metadane projektu i konfigurację narzędzi (black, ruff, mypy). Współpracuje z menedżerami paczek nowej generacji: poetry, uv, pdm, hatch.
uv (od Astral) zyskuje obecnie popularność — instaluje paczki kilkukrotnie szybciej niż pip i ma wbudowane zarządzanie wersjami Pythona, częściowo zastępując pyenv i venv. Dla nowych projektów warto rozważyć.
Ekosystem — popularne biblioteki
| Biblioteka | Domena |
|---|---|
numpy | Tablice numeryczne, algebra liniowa |
pandas | DataFrame’y, analiza danych tabelarycznych |
matplotlib | Wizualizacje, wykresy |
scipy | Statystyka, optymalizacja, interpolacje |
requests | HTTP klient (przyjazny interfejs nad urllib) |
pytest | Framework testowy (de facto standard) |
Django | Webowy framework full-stack |
Flask | Lekki webowy framework |
FastAPI | Nowoczesny framework REST z type hints |
scikit-learn | Klasyczne uczenie maszynowe |
PyTorch, TensorFlow | Sieci neuronowe i deep learning |
geopandas, shapely, rasterio | GIS w Pythonie |
W kontekście narzędzi hydrologicznych: IMGWTools korzysta z pandas i requests, Hydrolog z numpy i scipy, Kartograf z geopandas i rasterio. W tym wpisie nie wchodzimy głębiej w żadną z bibliotek — to tematy na osobne opracowania.
Testowanie
assert jako narzędzie debugowania
Najprostsza forma testu — sprawdzenie warunku. Jeśli fałsz, rzuca AssertionError:
def stan_w_metrach(stan_cm, rzedna_zera):
assert stan_cm >= 0, "stan wody nie może być ujemny"
return rzedna_zera + stan_cm / 100
print(stan_w_metrach(245, 49.54))
try:
stan_w_metrach(-5, 49.54)
except AssertionError as e:
print(f"asercja zadziałała: {e}")
assertznika zpython -OAsercje są usuwane przy uruchomieniu z flagą optymalizacji (
python3 -O). Nie używajassertdo walidacji danych użytkownika ani do warunków bezpieczeństwa — używaj zwykłegoif+raise.assertjest dla niezmiennych założeń o stanie programu („tu zawsze powinno być x”).
unittest — biblioteka standardowa
import unittest
def stan_w_metrach(stan_cm, rzedna_zera):
return rzedna_zera + stan_cm / 100
class TestStanWMetrach(unittest.TestCase):
def test_zero(self):
self.assertEqual(stan_w_metrach(0, 49.54), 49.54)
def test_245_cm(self):
self.assertAlmostEqual(stan_w_metrach(245, 49.54), 51.99, places=2)
def test_ujemna_rzedna(self):
# rzędna zera może być ujemna w niektórych formatach
self.assertEqual(stan_w_metrach(100, -5.0), -4.0)
if __name__ == "__main__":
unittest.main()Uruchomienie: python3 -m unittest plik.py. unittest jest w bibliotece standardowej — nie wymaga instalacji.
pytest — de facto standard
pytest to zewnętrzna biblioteka (pip install pytest), ale w praktyce używana w większości nowych projektów. Mniej boilerplate’u niż unittest:
# plik: test_narzedzia.py
def stan_w_metrach(stan_cm, rzedna_zera):
return rzedna_zera + stan_cm / 100
def test_zero():
assert stan_w_metrach(0, 49.54) == 49.54
def test_245_cm():
assert abs(stan_w_metrach(245, 49.54) - 51.99) < 0.01
def test_ujemna_rzedna():
assert stan_w_metrach(100, -5.0) == -4.0Uruchomienie: pytest test_narzedzia.py lub samo pytest (znajduje wszystkie pliki test_*.py). Każda funkcja test_* to osobny test — wynik zwraca podsumowanie (passed / failed / skipped).
pytest ma rozbudowany system fixtures (przygotowywanie danych testowych przez funkcje oznaczone @pytest.fixture) i parametryzacji (@pytest.mark.parametrize — uruchomienie tego samego testu z wieloma zestawami danych), które poznajemy zwykle po opanowaniu podstaw.
Pełny artykuł
Fixtures, parametryzacja,
conftest.py, mockowanie, TDD oraz testowanie kodu rastrowego — patrz Testowanie kodu w Pythonie.
Co testować w pierwszej kolejności
- logikę z gałęziami — funkcje, które na podstawie warunków wybierają jedną z kilku ścieżek;
- transformacje danych — funkcje czyste (input → output) bez efektów ubocznych;
- przypadki brzegowe — pusta lista, zero, wartości ujemne, brak klucza w słowniku;
- regresje — gdy znalazłeś bug, dopisz test, który go wykrywa, zanim go naprawisz.
Niekoniecznie warto testować trywialne gettery, integracje z bazą i siecią (te zwykle pokrywają testy integracyjne, nie jednostkowe), kod GUI.
Dobre praktyki
PEP 8 i automatyczne formatowanie
PEP 8 to oficjalny przewodnik stylu Pythona — definiuje wcięcia, spacje, długość linii (79 znaków klasycznie, 88 czy 100 współcześnie), nazewnictwo. Większości reguł nie trzeba pamiętać, bo wymuszają je narzędzia:
black— formatuje kod „ostatecznie” (mało konfigurowalny, świadomie);pip install black && black plik.pyruff— szybki linter i formatter (zastępujeflake8,isort, częściowoblack); de facto standard w nowych projektachisort— porządkuje importy alfabetycznie i grupowo
Pojedynczy wybór: w nowym projekcie zacznij od ruff z domyślną konfiguracją — robi 80% roboty bez tuningu.
Type hints i mypy
def policz_srednia(wartosci: list[float]) -> float:
return sum(wartosci) / len(wartosci)mypy plik.py zweryfikuje statycznie, czy typy się zgadzają. W praktyce: dodawaj hints do publicznego API (funkcje eksportowane z modułu, sygnatury klas), pomijaj w drobnych funkcjach pomocniczych jeśli generują szum.
logging zamiast print
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
def pobierz_dane(stacja_id):
logging.info(f"pobieranie danych dla stacji {stacja_id}")
# ... logika ...
logging.warning("brak pomiarów dla 2024-05-10")
return []
pobierz_dane(1)print jest OK w skryptach jednorazowych. W kodzie produkcyjnym logging daje: poziomy ważności (DEBUG/INFO/WARNING/ERROR/CRITICAL), filtrowanie, kierowanie do plików / sysloga / serwisów monitoringu, formatowanie z czasem i kontekstem.
Czytelność > sprytność
# Sprytnie, ale trudno czytać
wynik = [x for x in [y for y in dane if y > 0] if x % 2 == 0]
# Czytelnie
dodatnie = [y for y in dane if y > 0]
parzyste_dodatnie = [x for x in dodatnie if x % 2 == 0]Kod czytasz wielokrotnie więcej razy niż piszesz. Zen of Python: „Readability counts.”
Kompatybilność wersji
- Pisz nowy kod na ostatnich dwóch-trzech wersjach Pythona (np. 3.11, 3.12, 3.13). Cofanie się dalej rzadko ma sens.
- W publicznych bibliotekach deklaruj minimalną wersję w
pyproject.toml(python = ">=3.10"). - Funkcje takie jak
match/case(3.10+),tomllib(3.11+), generic typeslist[int]zamiastList[int](3.9+) są dostępne tylko od konkretnych wersji — sprawdzaj nawias w dokumentacji.
eval,exec,picklena niezaufanych danych
eval(string)iexec(string)wykonują dowolny kod Pythona — wartość ze źródła zewnętrznego (formularz, plik konfiguracyjny od użytkownika, dane z sieci) potraktowana w ten sposób to natychmiastowe RCE. Tak samopickle.loads()na danych z niezaufanego źródła pozwala odtworzyć dowolny obiekt — w tym wykonać dowolny kod podczas deserializacji. Do parsowania danych użyjjson/yaml.safe_load/tomllib. Do wywołania zewnętrznego procesu —subprocess.run([...])z listą argumentów, nigdyos.system(string)z konkatenacją.
Pełny przykład
Skrypt liczący proste statystyki przepływów dla zestawu stacji — łączy elementy ze wszystkich poprzednich sekcji:
"""Statystyki przepływów dla zestawu stacji hydrologicznych.
Skrypt:
1. Definiuje stacje i ich pomiary jako struktury w kodzie.
2. Liczy charakterystyki dla każdej stacji.
3. Zapisuje raport w formacie JSON.
"""
import json
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class Stacja:
kod_imgw: str
nazwa: str
rzeka: str
rzedna_zera: float | None = None
pomiary: list[float] = field(default_factory=list)
def charakterystyki(self) -> dict[str, float]:
if not self.pomiary:
return {"liczba_pomiarow": 0}
return {
"liczba_pomiarow": len(self.pomiary),
"min": min(self.pomiary),
"max": max(self.pomiary),
"srednia": round(sum(self.pomiary) / len(self.pomiary), 2),
}
def main() -> None:
stacje = [
Stacja(
kod_imgw="152160140",
nazwa="Poznań-Most Rocha",
rzeka="Warta",
rzedna_zera=49.54,
pomiary=[64.2, 61.5, 58.3, 67.1, 60.0],
),
Stacja(
kod_imgw="152210170",
nazwa="Warszawa-Bulwary",
rzeka="Wisła",
rzedna_zera=75.17,
pomiary=[542.0, 538.4, 551.2, 545.9],
),
Stacja(
kod_imgw="151180020",
nazwa="Piwonice",
rzeka="Prosna",
pomiary=[8.15, 7.42, 9.01, 8.55],
),
]
raport = {
s.nazwa: {
"kod_imgw": s.kod_imgw,
"rzeka": s.rzeka,
"rzedna_zera": s.rzedna_zera,
**s.charakterystyki(),
}
for s in stacje
}
sciezka = Path("/tmp/raport_stacji.json")
sciezka.write_text(
json.dumps(raport, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"Raport zapisany do: {sciezka}")
for nazwa, dane in raport.items():
print(f" {nazwa} ({dane['rzeka']}): średnia {dane.get('srednia', 'b.d.')} m³/s")
if __name__ == "__main__":
main()Po uruchomieniu (python3 raport.py) skrypt wypisuje:
Raport zapisany do: /tmp/raport_stacji.json
Poznań-Most Rocha (Warta): średnia 62.22 m³/s
Warszawa-Bulwary (Wisła): średnia 544.38 m³/s
Piwonice (Prosna): średnia 8.28 m³/s
Zawartość /tmp/raport_stacji.json:
{
"Poznań-Most Rocha": {
"kod_imgw": "152160140",
"rzeka": "Warta",
"rzedna_zera": 49.54,
"liczba_pomiarow": 5,
"min": 58.3,
"max": 67.1,
"srednia": 62.22
},
...
}W tym jednym pliku użyliśmy: dataclass, type hints, list comprehension, dict comprehension, f-stringów, pathlib, json, if __name__ == "__main__":, docstringa. To realistyczna struktura skryptu pomocniczego — od tego wzorca naturalnie wyrastają większe moduły, gdy logika rośnie.
Zobacz także
- IT — indeks narzędzi informatycznych
- Komendy terminala - Linux i macOS — uruchamianie skryptów, shebang,
chmod +x, instalacja Homebrew - Git - podstawowe operacje — wersjonowanie projektów Pythona
- SQL - podstawy — bazy danych; Python łączy się z nimi przez
sqlite3(standard library),psycopg, SQLAlchemy - Praca z rastrami w Pythonie — dane rastrowe (NMT, ortofoto, mapy pochodne) z biblioteką rasterio i xarray
- Hydrolog, IMGWTools, Kartograf, Hydrograf — własne narzędzia hydrologiczne pisane w Pythonie
- Bielik MCP — integracja LLM z Claude Code; serwer MCP w Pythonie
- Python documentation — oficjalna dokumentacja
- PEP 8 — Style Guide — standard formatowania
- PEP 20 — Zen of Python — filozofia języka
- PEP 257 — Docstring Conventions — konwencje dokumentowania
- pyenv — zarządzanie wersjami Pythona
- uv — szybki menedżer paczek i wersji (alternatywa dla pip + venv + pyenv)