Pytania rekrutacyjne Python
102 pytań — 20 odpowiedzi dostępnych od razu
Podstawy Pythona
Czym jest Python i jakie są jego najważniejsze cechy jako języka programowania?
Odpowiedź w 30 sekund: Python to wysokopoziomowy, ogólnego przeznaczenia język programowania, który stawia na czytelność kodu i zwięzłość składni. Jest interpretowany, dynamicznie typowany i wspiera wiele paradygmatów (obiektowy, proceduralny, funkcyjny). Jego siłą jest ogromny ekosystem bibliotek i niska bariera wejścia.
Odpowiedź w 2 minuty: Python został stworzony przez Guido van Rossuma na przełomie lat 80. i 90. z naciskiem na czytelność — kod ma być łatwy do napisania, ale przede wszystkim łatwy do późniejszego przeczytania przez człowieka. Stąd charakterystyczne wcięcia (ang. significant whitespace), które zamiast nawiasów klamrowych wyznaczają bloki kodu. Język jest wysokopoziomowy, co oznacza, że abstrahuje od zarządzania pamięcią i niskopoziomowych szczegółów sprzętu.
Najważniejsze cechy to: dynamiczne typowanie (typy ustalane są w trakcie wykonania), automatyczne zarządzanie pamięcią (zliczanie referencji plus garbage collector), oraz wieloparadygmatowość — można pisać proceduralnie, obiektowo lub funkcyjnie. Python jest też interpretowany (a dokładniej kompilowany do bytecode'u uruchamianego przez maszynę wirtualną), co przyspiesza cykl pisz-uruchom-popraw.
Cechą decydującą o popularności jest „baterie w zestawie" (batteries included) — bogata biblioteka standardowa pokrywająca zadania od obsługi sieci po przetwarzanie danych. Do tego dochodzi ogromny ekosystem zewnętrzny (PyPI): NumPy, pandas, Django, FastAPI, PyTorch. Dzięki temu Python dominuje w analizie danych, uczeniu maszynowym, automatyzacji, skryptowaniu i backendzie webowym.
Warto wspomnieć o powszechnym nieporozumieniu: Python bywa nazywany „wolnym". To prawda dla czystych obliczeń CPU-bound w pętlach, ale w praktyce wąskie gardła przenosi się do bibliotek napisanych w C (NumPy), a o produktywności programisty często decyduje czas pisania kodu, nie czas jego wykonania.
Materiały
Czym różni się język interpretowany od kompilowanego i do której grupy należy Python?
Odpowiedź w 30 sekund: Język kompilowany tłumaczy cały kod źródłowy na kod maszynowy przed uruchomieniem (np. C, Rust), a język interpretowany wykonuje kod instrukcja po instrukcji w trakcie działania. Python należy do grupy interpretowanych, choć w praktyce najpierw kompiluje kod do bytecode'u, który następnie wykonuje maszyna wirtualna.
Odpowiedź w 2 minuty:
W klasycznym ujęciu kompilacja to jednorazowe przetłumaczenie całego programu na kod maszynowy zrozumiały dla procesora — efektem jest plik wykonywalny (np. .exe), który działa bez dalszego udziału kompilatora. Daje to wysoką wydajność, ale dłuższy cykl budowania i kod zależny od platformy. Interpretacja polega na analizowaniu i wykonywaniu kodu źródłowego „na bieżąco" przez program zwany interpreterem — kosztem szybkości zyskujemy przenośność i szybki feedback.
Podział ten jest jednak uproszczeniem, bo dotyczy implementacji, nie samego języka. CPython (referencyjna implementacja Pythona) działa dwuetapowo: najpierw kompiluje kod źródłowy do pośredniego bytecode'u, a następnie interpretuje ten bytecode w pętli ewaluacyjnej maszyny wirtualnej (PVM). Dlatego Python klasyfikujemy jako interpretowany — z perspektywy użytkownika nie ma osobnego kroku kompilacji, uruchamia się go bezpośrednio.
W praktyce ten model ma konsekwencje. Błędy składniowe wykrywane są podczas kompilacji do bytecode'u (przed wykonaniem), ale błędy typów czy logiczne ujawniają się dopiero w trakcie działania, gdy interpreter dotrze do problematycznej linii. Granica się zaciera również w innych implementacjach — np. PyPy używa kompilacji JIT (Just-In-Time), tłumacząc gorące fragmenty kodu na kod maszynowy w trakcie działania, co zbliża go wydajnościowo do języków kompilowanych.
| Cecha | Kompilowany (C) | Interpretowany (Python) |
|---|---|---|
| Moment tłumaczenia | Przed uruchomieniem | W trakcie działania |
| Artefakt | Plik wykonywalny | Brak (lub .pyc bytecode) |
| Szybkość wykonania | Wysoka | Niższa |
| Cykl pisz–uruchom | Wolniejszy (build) | Natychmiastowy |
| Przenośność | Zależna od platformy | Wysoka |
Materiały
Czym jest CPython i czym różni się od innych implementacji, takich jak PyPy, Jython czy IronPython?
Odpowiedź w 30 sekund: CPython to referencyjna i najpopularniejsza implementacja Pythona, napisana w języku C — to ją pobiera się z python.org. Alternatywy różnią się platformą uruchomieniową lub strategią wykonania: PyPy używa kompilacji JIT dla szybkości, Jython działa na JVM, a IronPython na platformie .NET.
Odpowiedź w 2 minuty: Należy odróżnić język Python (specyfikację) od jego implementacji (programu, który ten język wykonuje). CPython jest implementacją odniesienia: kompiluje kod do bytecode'u i interpretuje go w maszynie wirtualnej napisanej w C. Gdy mówimy „Python" bez kontekstu, niemal zawsze chodzi o CPython. Jego dużą zaletą jest doskonała integracja z bibliotekami w C (rozszerzenia CPython API), dlatego dominuje w ekosystemie naukowym (NumPy, SciPy). Jego ograniczeniem jest GIL (Global Interpreter Lock).
PyPy to implementacja napisana w RPython, której kluczową cechą jest kompilator JIT — analizuje kod w trakcie działania i tłumaczy często wykonywane fragmenty bezpośrednio na kod maszynowy. Dla długo działającego kodu CPU-bound potrafi być wielokrotnie szybszy od CPython. Cena to gorsza kompatybilność z rozszerzeniami C i wyższe zużycie pamięci na starcie.
Jython kompiluje Pythona do bytecode'u JVM, dzięki czemu kod Pythona może bezpośrednio korzystać z bibliotek Javy i działać w środowisku JVM. IronPython robi analogiczną rzecz dla platformy .NET (CLR), umożliwiając integrację z bibliotekami C#/.NET. Obie te implementacje zwykle pozostają w tyle za najnowszą wersją języka i nie obsługują rozszerzeń napisanych w C. Warto też wspomnieć o MicroPython — wersji dla mikrokontrolerów i systemów wbudowanych.
| Implementacja | Platforma / technologia | Główna zaleta | Główne ograniczenie |
|---|---|---|---|
| CPython | C (referencyjna) | Kompatybilność, ekosystem C | GIL, szybkość CPU-bound |
| PyPy | RPython + JIT | Szybkość wykonania | Słabsza zgodność z C-API |
| Jython | JVM | Integracja z Javą | Opóźnione wersje, brak C-ext |
| IronPython | .NET (CLR) | Integracja z .NET | Opóźnione wersje, brak C-ext |
Materiały
Czym są pliki .pyc i jaką rolę pełni bytecode w działaniu Pythona?
Odpowiedź w 30 sekund:
Pliki .pyc to skompilowany bytecode — pośrednia, zoptymalizowana reprezentacja kodu źródłowego, którą wykonuje maszyna wirtualna Pythona (PVM). CPython buforuje je w katalogu __pycache__, aby przy kolejnych uruchomieniach pominąć etap kompilacji i przyspieszyć start importowanych modułów.
Odpowiedź w 2 minuty: Gdy uruchamiasz program w Pythonie, CPython nie wykonuje kodu źródłowego bezpośrednio. Najpierw kompiluje go do bytecode'u — niskopoziomowego zestawu instrukcji niezależnych od platformy, lecz zależnych od wersji interpretera. Ten bytecode jest następnie wykonywany w pętli ewaluacyjnej maszyny wirtualnej Pythona (PVM). Bytecode to warstwa pośrednia między czytelnym dla człowieka kodem a operacjami, które rozumie interpreter.
Pliki .pyc to zapisany na dysku, zbuforowany bytecode. Co ważne, są one tworzone tylko dla importowanych modułów, a nie dla skryptu uruchamianego bezpośrednio (python main.py). Trafiają do katalogu __pycache__ z nazwą zawierającą wersję interpretera, np. module.cpython-312.pyc. Dzięki temu przy ponownym imporcie CPython sprawdza, czy źródło się nie zmieniło (porównując znacznik czasu lub hash), i jeśli nie — wczytuje gotowy bytecode, oszczędzając czas kompilacji. To optymalizacja czasu startu, a nie czasu wykonania — sam działający kod nie staje się szybszy.
Bytecode można podejrzeć modułem dis z biblioteki standardowej, co bywa przydatne przy zrozumieniu, jak Python interpretuje dany fragment kodu i przy mikrooptymalizacjach. Warto pamiętać o dwóch pułapkach: .pyc nie jest formą zabezpieczenia kodu (łatwo go zdekompilować) ani nie jest przenośny między wersjami Pythona — bytecode zmienia się między wydaniami.
Przykład kodu:
import dis
def dodaj(a, b):
# Prosta funkcja, której bytecode podejrzymy
return a + b
# dis pokazuje instrukcje bytecode'u tej funkcji
dis.dis(dodaj)
# Przykładowy wynik:
# LOAD_FAST a # załaduj argument a na stos
# LOAD_FAST b # załaduj argument b na stos
# BINARY_OP + (add) # wykonaj dodawanie
# RETURN_VALUE # zwróć wynik ze szczytu stosu
Diagram:
flowchart LR
A["kod źródłowy (.py)"] --> B["kompilator CPython"]
B --> C["bytecode (.pyc w __pycache__)"]
C --> D["maszyna wirtualna (PVM)"]
D --> E["wykonanie programu"]
Materiały
Czym jest PEP i dlaczego PEP 8 ma tak duże znaczenie?
Odpowiedź w 30 sekund: PEP (Python Enhancement Proposal) to ustandaryzowany dokument projektowy opisujący nową funkcję języka, proces lub wytyczne dla społeczności Pythona. PEP 8 to przewodnik stylu kodowania, który ma kluczowe znaczenie, bo ujednolica wygląd kodu, dzięki czemu jest on spójny i czytelny niezależnie od autora.
Odpowiedź w 2 minuty: PEP (Python Enhancement Proposal) to formalny mechanizm proponowania zmian i dokumentowania decyzji w ekosystemie Pythona — odpowiednik specyfikacji RFC. Każdy PEP ma numer i kategorię. Wyróżniamy Standards Track (nowe funkcje języka lub biblioteki, np. PEP 484 o adnotacjach typów), Informational (wytyczne i informacje, np. PEP 8) oraz Process (zmiany w procesach społeczności). To właśnie poprzez PEP-y język ewoluuje w sposób przejrzysty i udokumentowany — można prześledzić, dlaczego dana decyzja została podjęta.
PEP 8 to oficjalny przewodnik stylu dla kodu w Pythonie, napisany m.in. przez samego Guido van Rossuma. Określa konwencje: 4 spacje na wcięcie (nie taby), limit długości linii (zwyczajowo 79 znaków), nazewnictwo (snake_case dla funkcji i zmiennych, PascalCase dla klas, UPPER_CASE dla stałych), zasady rozmieszczania spacji wokół operatorów i kolejność importów.
Jego znaczenie wynika z filozofii Pythona: kod czyta się znacznie częściej, niż się go pisze. Spójny styl w całym projekcie redukuje obciążenie poznawcze — czytelnik nie traci uwagi na różnice formatowania i może skupić się na logice. W zespole eliminuje to jałowe dyskusje o styl podczas code review. PEP 8 zawiera jednak ważne zastrzeżenie: „spójność jest ważna, ale wiedz, kiedy złamać regułę" — czytelność konkretnego fragmentu jest nadrzędna wobec dosłownego trzymania się litery przewodnika.
W praktyce zgodności z PEP 8 pilnują automatyczne narzędzia: Black (formater), flake8 oraz nowoczesny, bardzo szybki Ruff (linter), które integruje się z edytorem i CI/CD, dzięki czemu styl utrzymuje się bez ręcznej pracy.
Materiały
Na czym polega dynamiczne typowanie i jak różni się od typowania statycznego?
Odpowiedź w 30 sekund: W typowaniu dynamicznym typ jest związany z wartością (obiektem), a nie ze zmienną, i jest sprawdzany dopiero w trakcie wykonania — ta sama zmienna może raz wskazywać na liczbę, a chwilę później na tekst. W typowaniu statycznym typy zmiennych są znane i weryfikowane przez kompilator przed uruchomieniem programu. Python jest dynamicznie typowany.
Odpowiedź w 2 minuty:
W Pythonie zmienna to tak naprawdę nazwa (etykieta) wskazująca na obiekt w pamięci, a typ należy do obiektu, nie do nazwy. Dlatego można bez problemu napisać x = 10, a potem x = "tekst" — nazwa x po prostu zaczyna wskazywać na inny obiekt. Interpreter sprawdza poprawność operacji (np. czy można dodać dwa obiekty) dopiero w momencie ich wykonania. To właśnie typowanie dynamiczne: błędy typów (TypeError) ujawniają się w czasie działania programu.
W językach statycznie typowanych (Java, C++, Go) typ zmiennej jest ustalony i sprawdzany przez kompilator zanim program się uruchomi. Wymaga to często deklarowania typów, ale pozwala wychwycić całą klasę błędów wcześnie i daje lepsze podpowiedzi w narzędziach (IDE).
Trzeba odróżnić dwie niezależne osie. Pierwsza to dynamiczne kontra statyczne (kiedy sprawdzany jest typ). Druga to silne kontra słabe typowanie (jak chętnie język wykonuje niejawne konwersje). Python jest dynamicznie, ale silnie typowany — nie pozwoli np. dodać liczby do napisu (1 + "2" rzuca TypeError), w przeciwieństwie do JavaScriptu, który dokonałby niejawnej konwersji. To częste źródło nieporozumień na rozmowach.
Zaletą dynamicznego typowania jest szybkość prototypowania, zwięzłość kodu i elastyczność (np. duck typing — „jeśli chodzi jak kaczka i kwacze jak kaczka, traktuj to jak kaczkę"). Wadą jest przesunięcie wykrywania błędów na czas wykonania. Dlatego nowoczesny Python łączy oba światy: pozostaje dynamiczny w runtime, ale wprowadza adnotacje typów (PEP 484), które narzędzia jak mypy czy pyright weryfikują statycznie — bez wpływu na samo działanie programu.
Przykład kodu:
# Typowanie dynamiczne: ta sama nazwa wskazuje na różne typy
x = 10 # x wskazuje na obiekt int
print(type(x)) # <class 'int'>
x = "tekst" # teraz ta sama nazwa wskazuje na str
print(type(x)) # <class 'str'>
# Silne typowanie: brak niejawnej konwersji
try:
wynik = 1 + "2" # Python NIE skonwertuje napisu na liczbę
except TypeError as e:
print("Błąd:", e) # unsupported operand type(s) for +: 'int' and 'str'
# Adnotacje typów są opcjonalne i nie zmieniają działania w runtime
def powitanie(imie: str) -> str:
return f"Cześć, {imie}!"
Materiały
Jakie są najważniejsze różnice między Python 2 a Python 3 i dlaczego dziś używa się wyłącznie Python 3?
Odpowiedź w 30 sekund:
Python 3 wprowadził niekompatybilne wstecz, lecz porządkujące zmiany: print stał się funkcją, dzielenie / zwraca liczbę zmiennoprzecinkową, a obsługa tekstu opiera się na Unicode (str) i bajtach (bytes). Używa się dziś wyłącznie Pythona 3, ponieważ Python 2 osiągnął koniec wsparcia 1 stycznia 2020 i nie otrzymuje już poprawek, w tym bezpieczeństwa.
Odpowiedź w 2 minuty:
Python 3 (wydany w 2008 r.) był świadomą decyzją o zerwaniu kompatybilności wstecz, aby naprawić nagromadzone niedoskonałości języka. Najbardziej widoczną zmianą jest print jako funkcja — print("x") zamiast instrukcji print "x". Dzięki temu zyskał spójność z resztą języka i konfigurowalne argumenty (sep, end, file).
Najistotniejszą i najbardziej kłopotliwą przy migracji zmianą jest obsługa tekstu. W Pythonie 2 str był ciągiem bajtów, a unicode osobnym typem. W Pythonie 3 str to zawsze tekst Unicode, a dane binarne reprezentuje osobny typ bytes. To rozdzielenie eliminuje całą klasę błędów kodowania znaków, ale wymaga jawnego kodowania/dekodowania na styku z plikami i siecią. Kolejna częsta pułapka: dzielenie — 3 / 2 daje w Pythonie 3 wynik 1.5 (true division), podczas gdy w Pythonie 2 dawało 1. Dzielenie całkowite uzyskujemy operatorem //.
Inne zmiany: range() zwraca leniwy obiekt zamiast budować całą listę w pamięci (podobnie dict.keys(), map(), zip() zwracają widoki/iteratory), poprawiono składnię obsługi wyjątków (except E as e), a od Pythona 3.7 słowniki gwarantują zachowanie kolejności wstawiania. Python 3 stał się też domem dla nowoczesnych funkcji, których nigdy nie przeniesiono do „dwójki": f-stringi, adnotacje typów, async/await, operator morsa czy strukturalne dopasowanie wzorców.
Powód, dla którego używa się wyłącznie Pythona 3, jest praktyczny: 1 stycznia 2020 r. Python 2 osiągnął oficjalny koniec życia (EOL). Nie powstają już żadne poprawki — w tym łatki bezpieczeństwa. Cały współczesny ekosystem (Django, NumPy, pandas, requests) dawno porzucił wsparcie dla „dwójki". Utrzymywanie kodu w Pythonie 2 to dziś dług technologiczny i zagrożenie bezpieczeństwa.
| Cecha | Python 2 | Python 3 |
|---|---|---|
print |
Instrukcja: print "x" |
Funkcja: print("x") |
| Tekst domyślnie | str = bajty |
str = Unicode, bytes osobno |
Dzielenie / |
3 / 2 == 1 |
3 / 2 == 1.5 |
range() |
Buduje listę | Leniwy obiekt |
Kolejność dict |
Niegwarantowana | Gwarantowana (od 3.7) |
| Wsparcie | EOL 1.01.2020 | Aktywnie rozwijany |
Materiały
Czym jest „Zen of Pythona" (PEP 20) i jak wpływa na sposób pisania kodu?
Odpowiedź w 30 sekund:
„Zen of Pythona" (PEP 20) to zbiór 19 aforyzmów ujmujących filozofię projektową języka, takich jak „Czytelność się liczy" czy „Proste jest lepsze niż złożone". Można go wyświetlić poleceniem import this. Stanowi nieformalny kompas, który zachęca do pisania kodu prostego, jawnego i czytelnego.
Odpowiedź w 2 minuty: „Zen of Pythona", autorstwa Tima Petersa, to żartobliwy, ale traktowany poważnie zbiór wytycznych opisujących ducha języka. Zawiera 19 maksym (mimo że tekst mówi o 20). Najczęściej cytowane to: „Beautiful is better than ugly", „Explicit is better than implicit" (jawne lepsze niż domniemane), „Simple is better than complex", „Readability counts" (czytelność się liczy) oraz „There should be one — and preferably only one — obvious way to do it" (powinien istnieć jeden oczywisty sposób zrobienia czegoś). Ten ostatni punkt to świadomy kontrast wobec filozofii Perla („jest wiele sposobów") i tłumaczy, dlaczego Python preferuje konwencje i idiomy.
Wpływ na codzienne pisanie kodu jest realny, nawet jeśli „Zen" nie jest sztywnym standardem jak PEP 8. Zasada „explicit is better than implicit" skłania do unikania ukrytej magii — np. jawnego importowania nazw zamiast from module import *. „Flat is better than nested" zachęca do spłaszczania głębokich zagnieżdżeń (np. przez wczesne return lub strażników zamiast piramidy if). „Errors should never pass silently" przestrzega przed pustym except:, który połyka błędy. „If the implementation is hard to explain, it's a bad idea" to praktyczny test prostoty projektu.
W kontekście rozmowy rekrutacyjnej warto pokazać, że „Zen" to nie ozdobnik, lecz kompas decyzyjny: gdy istnieje kilka możliwych rozwiązań, wybiera się to bardziej czytelne, jawne i proste — bo to one przekładają się na łatwiejszą długoterminową konserwację kodu, co jest sednem „pythonowości" (pythonic code). Ciekawostką jest, że tekst można wywołać w dowolnym interpreterze poleceniem import this.
Przykład kodu:
# Wyświetlenie pełnego tekstu Zen of Python
import this
# "Explicit is better than implicit" w praktyce:
# źle — ukryty import wszystkiego, niejasne pochodzenie nazw
from math import *
print(sqrt(16))
# dobrze — jawnie wiadomo, skąd pochodzi nazwa
import math
print(math.sqrt(16))
# "Flat is better than nested" — strażnik zamiast głębokiego zagnieżdżenia
def przetworz(dane):
if not dane:
return None # wczesny powrót spłaszcza strukturę
return [x * 2 for x in dane]
Materiały
Typy danych i struktury danych
Jakie są wbudowane typy danych w Pythonie?
Odpowiedź w 30 sekund:
Python ma kilka kategorii typów wbudowanych: liczbowe (int, float, complex, bool), tekstowe (str), sekwencje (list, tuple, range), mapowanie (dict), zbiory (set, frozenset), binarne (bytes, bytearray, memoryview) oraz typ specjalny NoneType. Wszystkie są obiektami, a każdy obiekt ma swój typ, który sprawdzisz funkcją type().
Odpowiedź w 2 minuty:
W Pythonie wszystko jest obiektem, a każdy obiekt ma typ. Wbudowane typy dzielimy na kilka grup. Typy liczbowe to int (liczby całkowite o dowolnej precyzji), float (liczby zmiennoprzecinkowe podwójnej precyzji, IEEE 754), complex (liczby zespolone) oraz bool (wartości logiczne True/False, będące podtypem int).
Sekwencje to uporządkowane kolekcje: str (niemutowalny ciąg znaków Unicode), list (mutowalna lista), tuple (niemutowalna krotka) oraz range (niemutowalny ciąg liczb). Typy mapowania reprezentuje dict (słownik klucz-wartość). Typy zbiorów to set (mutowalny zbiór) i frozenset (niemutowalny zbiór).
Typy binarne obejmują bytes (niemutowalna sekwencja bajtów), bytearray (mutowalna sekwencja bajtów) oraz memoryview (widok na bufor pamięci bez kopiowania danych). Wreszcie istnieje typ specjalny NoneType, którego jedyną instancją jest None — używany do reprezentowania braku wartości.
Warto pamiętać, że Python jest językiem o typowaniu dynamicznym (typ jest związany z obiektem, nie ze zmienną) i silnym (nie wykonuje niejawnych, ryzykownych konwersji, np. "1" + 1 zgłasza błąd).
Przykład kodu:
# Typy liczbowe
print(type(42)) # <class 'int'>
print(type(3.14)) # <class 'float'>
print(type(2 + 3j)) # <class 'complex'>
print(type(True)) # <class 'bool'>
# Sekwencje
print(type("tekst")) # <class 'str'>
print(type([1, 2, 3])) # <class 'list'>
print(type((1, 2, 3))) # <class 'tuple'>
print(type(range(5))) # <class 'range'>
# Mapowanie, zbiory, typy binarne, None
print(type({"a": 1})) # <class 'dict'>
print(type({1, 2, 3})) # <class 'set'>
print(type(frozenset())) # <class 'frozenset'>
print(type(b"bajty")) # <class 'bytes'>
print(type(None)) # <class 'NoneType'>
# bool jest podtypem int
print(isinstance(True, int)) # True
print(True + True) # 2
Materiały
Jaka jest różnica między typami mutowalnymi a niemutowalnymi? Podaj przykłady.
Odpowiedź w 30 sekund:
Obiekt mutowalny można zmienić w miejscu po utworzeniu (zmienia się jego zawartość, ale tożsamość id() pozostaje ta sama), a niemutowalny nie — każda „zmiana" tworzy nowy obiekt. Mutowalne to m.in. list, dict, set, bytearray; niemutowalne to int, float, str, tuple, frozenset, bytes.
Odpowiedź w 2 minuty:
Mutowalność określa, czy stan obiektu może się zmienić po jego utworzeniu. Obiekt mutowalny (np. list) pozwala modyfikować swoją zawartość w miejscu — możesz dodać element, a id() obiektu się nie zmieni. Obiekt niemutowalny (np. str, int, tuple) nie pozwala na zmianę zawartości; operacje, które wyglądają jak modyfikacja (np. x += 1 dla int albo s.upper() dla str), w rzeczywistości tworzą zupełnie nowy obiekt i przypinają do niego zmienną.
To rozróżnienie ma istotne konsekwencje praktyczne. Po pierwsze, tylko obiekty haszowalne (a niemutowalne typy wbudowane są haszowalne) mogą być kluczami słownika i elementami zbioru. Po drugie, niemutowalność wpływa na semantykę przekazywania argumentów: Python przekazuje referencje do obiektów, więc funkcja może zmodyfikować przekazaną listę „u dzwoniącego", ale nie może zmienić wartości przekazanego int.
Klasyczna pułapka to mutowalny argument domyślny — wartość domyślna jest tworzona raz, przy definicji funkcji, i współdzielona między wywołaniami. Drugą pułapką jest płytkie kopiowanie: skopiowanie listy zawierającej zagnieżdżone listy kopiuje tylko referencje do wewnętrznych obiektów. Warto też wiedzieć, że krotka jest niemutowalna, ale jeśli zawiera element mutowalny (np. listę), zawartość tej listy nadal można zmienić.
Przykład kodu:
# Lista jest mutowalna — id się nie zmienia po modyfikacji
lista = [1, 2, 3]
print(id(lista))
lista.append(4)
print(id(lista)) # ten sam id — zmiana w miejscu
# str jest niemutowalny — powstaje nowy obiekt
s = "abc"
print(id(s))
s = s + "d"
print(id(s)) # inny id — nowy obiekt
# Pułapka: mutowalny argument domyślny
def dodaj(element, kosz=[]): # ZŁY wzorzec
kosz.append(element)
return kosz
print(dodaj(1)) # [1]
print(dodaj(2)) # [1, 2] — niespodzianka! lista współdzielona
# Poprawny wzorzec ze strażnikiem None
def dodaj_ok(element, kosz=None):
if kosz is None:
kosz = []
kosz.append(element)
return kosz
# Krotka niemutowalna, ale jej mutowalny element już nie
krotka = (1, [2, 3])
krotka[1].append(4)
print(krotka) # (1, [2, 3, 4])
Diagram:
flowchart TD
A["Typy wbudowane"] --> B["Mutowalne"]
A --> C["Niemutowalne"]
B --> B1["list"]
B --> B2["dict"]
B --> B3["set"]
B --> B4["bytearray"]
C --> C1["int / float / bool"]
C --> C2["str"]
C --> C3["tuple"]
C --> C4["frozenset"]
C --> C5["bytes"]
C -. "haszowalne -> klucze dict / elementy set" .-> D["Haszowalność"]
Materiały
Czym różni się lista (list) od krotki (tuple) i kiedy użyłbyś której?
Odpowiedź w 30 sekund: Lista jest mutowalna (można ją modyfikować w miejscu), krotka niemutowalna (po utworzeniu nie zmienisz jej zawartości). Listy używasz dla jednorodnych, zmieniających się kolekcji, a krotki dla stałych, często heterogenicznych rekordów — krotka może też być kluczem słownika, bo jest haszowalna.
Odpowiedź w 2 minuty:
Najważniejsza różnica to mutowalność. Lista pozwala dodawać, usuwać i zmieniać elementy w miejscu (append, pop, przypisanie do indeksu), natomiast krotka jest „zamrożona" po utworzeniu. Z tego wynika kolejna różnica: krotka jest haszowalna (jeśli wszystkie jej elementy są haszowalne), więc może być kluczem słownika lub elementem zbioru; lista nie może.
Konwencjonalnie listy stosuje się do jednorodnych sekwencji o zmiennej długości, po których iterujemy (np. lista użytkowników, lista wyników). Krotki zwykle reprezentują heterogeniczne rekordy o stałej strukturze, gdzie pozycja ma znaczenie (np. współrzędne (x, y), krotka (imię, wiek)). Krotki są też zwracane przez funkcje przy wielu wartościach i używane w rozpakowywaniu.
Krotki mają drobne przewagi wydajnościowe: zajmują mniej pamięci i ich tworzenie z literałów jest minimalnie szybsze, ponieważ CPython może je zbudować jednym bajtkodem (BUILD_TUPLE) i cache'ować małe stałe krotki. Niemutowalność daje też bezpieczeństwo — przekazując krotkę masz pewność, że odbiorca jej nie zmodyfikuje.
W praktyce, jeśli potrzebujesz nazwanych pól rekordu, zamiast zwykłej krotki rozważ collections.namedtuple albo typing.NamedTuple — zachowują niemutowalność, a dają czytelny dostęp przez nazwy.
Przykład kodu:
# Lista — mutowalna kolekcja
lista = [1, 2, 3]
lista.append(4)
lista[0] = 99
print(lista) # [99, 2, 3, 4]
# Krotka — niemutowalna
krotka = (1, 2, 3)
# krotka[0] = 99 # TypeError: 'tuple' object does not support item assignment
# Krotka jako klucz słownika (lista by się nie nadała)
ceny = {("Warszawa", "Kraków"): 299}
print(ceny[("Warszawa", "Kraków")]) # 299
# Rozpakowywanie i zwracanie wielu wartości
def minmax(dane):
return min(dane), max(dane) # zwraca krotkę
najm, najw = minmax([3, 1, 4, 1, 5])
print(najm, najw) # 1 5
# Porównanie pamięci
import sys
print(sys.getsizeof([1, 2, 3])) # więcej bajtów
print(sys.getsizeof((1, 2, 3))) # mniej bajtów
Diagram: (tabela porównawcza poniżej)
| Cecha | list |
tuple |
|---|---|---|
| Mutowalność | mutowalna | niemutowalna |
| Składnia | [1, 2, 3] |
(1, 2, 3) |
| Haszowalna (klucz dict / element set) | nie | tak (gdy elementy haszowalne) |
| Zużycie pamięci | większe | mniejsze |
| Typowe zastosowanie | jednorodna, zmienna kolekcja | stały, heterogeniczny rekord |
| Metody modyfikujące | append, pop, sort... |
brak (tylko count, index) |
Materiały
Czym jest słownik (dict) i jak działa wewnętrznie (haszowanie, kolejność kluczy)?
Odpowiedź w 30 sekund:
dict to mapowanie kluczy na wartości oparte na tablicy haszującej, dające średnio stały czas O(1) dla wyszukiwania, wstawiania i usuwania. Klucze muszą być haszowalne (niemutowalne). Od Pythona 3.7 słownik gwarantuje zachowanie kolejności wstawiania kluczy.
Odpowiedź w 2 minuty:
Słownik to kolekcja par klucz-wartość zaimplementowana jako tablica haszująca (hash table). Gdy dodajesz parę, Python oblicza hash(klucz), na tej podstawie wyznacza pozycję w wewnętrznej tablicy i tam zapisuje wpis. Przy odczycie ponownie liczy hash i trafia bezpośrednio do właściwego miejsca, dzięki czemu operacje mają średnią złożoność O(1) (w pesymistycznym przypadku, przy wielu kolizjach, O(n)).
Kluczem może być tylko obiekt haszowalny — taki, który implementuje __hash__ i __eq__, a jego hash nie zmienia się w czasie życia. Z definicji niemutowalne typy wbudowane (int, str, tuple z haszowalnymi elementami) są haszowalne, a mutowalne (list, dict, set) nie. Dwa klucze równe wg == muszą mieć ten sam hash; kolizje (różne klucze z tym samym hashem lub tą samą pozycją) Python rozwiązuje przez sondowanie (open addressing).
Od Pythona 3.7 zachowanie kolejności wstawiania jest gwarancją językową (w CPythonie 3.6 było szczegółem implementacji). Wynika to z tzw. „compact dict" — wpisy trzymane są w zwartej, uporządkowanej tablicy, a osobna rzadka tablica haszująca przechowuje jedynie indeksy do nich. To rozwiązanie zmniejsza zużycie pamięci i naturalnie utrwala kolejność. Uwaga: kolejność to kolejność wstawiania, a nie posortowanie — do sortowania użyj sorted().
Praktyczne wnioski: nie używaj jako kluczy obiektów, których stan (i hash) może się zmienić; pamiętaj o metodach takich jak .get() (bezpieczny odczyt z wartością domyślną), .setdefault(), słownikach defaultdict i Counter z modułu collections, a od 3.9 o operatorze łączenia |.
Przykład kodu:
# Podstawy
osoba = {"imię": "Anna", "wiek": 30}
print(osoba["imię"]) # Anna
print(osoba.get("email", "brak")) # brak — bezpieczny odczyt
# Kolejność wstawiania jest zachowana (Python 3.7+)
d = {}
d["c"] = 1
d["a"] = 2
d["b"] = 3
print(list(d)) # ['c', 'a', 'b']
# Klucz musi być haszowalny
poprawny = {(1, 2): "krotka ok"}
# bledny = {[1, 2]: "x"} # TypeError: unhashable type: 'list'
# Równość kluczy a hash: 1 == 1.0 == True -> ten sam wpis
mieszany = {1: "a"}
mieszany[1.0] = "b"
mieszany[True] = "c"
print(mieszany) # {1: 'c'} — jeden klucz!
# Łączenie słowników (Python 3.9+)
a = {"x": 1}
b = {"y": 2}
print(a | b) # {'x': 1, 'y': 2}
Diagram:
flowchart LR
K["klucz 'imię'"] --> H["hash(klucz)"]
H --> I["indeks w tablicy<br/>haszujacej"]
I --> S["rzadka tablica<br/>indeksow"]
S --> E["zwarta tablica wpisow<br/>(kolejnosc wstawiania)"]
E --> V["wartosc 'Anna'"]
H -. "kolizja" .-> P["sondowanie<br/>kolejnej pozycji"]
Materiały
Czym różni się set od frozenset i do czego służą zbiory?
Odpowiedź w 30 sekund:
set to mutowalny zbiór unikalnych, haszowalnych elementów, a frozenset to jego niemutowalna i haszowalna wersja. Zbiory służą do usuwania duplikatów, szybkiego testu przynależności O(1) oraz operacji teoriomnogościowych (suma, część wspólna, różnica). Bo frozenset jest haszowalny, można go użyć jako klucza słownika lub elementu innego zbioru.
Odpowiedź w 2 minuty:
Zbiór (set) to nieuporządkowana kolekcja unikalnych elementów oparta — podobnie jak słownik — na tablicy haszującej. Daje to średnio stały czas O(1) dla testu przynależności (x in s), dodawania i usuwania, co czyni zbiory dużo szybszymi od list przy sprawdzaniu „czy element istnieje". Elementy muszą być haszowalne, więc nie wrzucisz do zbioru listy ani innego słownika.
Różnica między set a frozenset jest analogiczna do różnicy między list a tuple: set jest mutowalny (add, discard, update), natomiast frozenset jest niemutowalny i przez to haszowalny. Dlatego frozenset może być elementem innego zbioru lub kluczem słownika, a zwykły set nie. Jeśli potrzebujesz „zbioru zbiorów", elementy muszą być frozenset.
Zbiory są idealne do: usuwania duplikatów (list(set(dane))), szybkiego członkostwa oraz operacji teoriomnogościowych. Wspierają sumę (| / union), część wspólną (& / intersection), różnicę (- / difference) i różnicę symetryczną (^ / symmetric_difference), a także testy podzbioru i nadzbioru (<=, >=). Operatory wymagają, by oba operandy były zbiorami; metody akceptują dowolny iterowalny.
Ważne ograniczenia: zbiory są nieuporządkowane (kolejność iteracji nie jest gwarantowana, choć w danym uruchomieniu bywa stabilna), więc nie indeksujesz ich s[0]. Literał {} tworzy pusty słownik, nie zbiór — pusty zbiór tworzysz przez set().
Przykład kodu:
# Usuwanie duplikatów i szybki test przynależności
liczby = [1, 2, 2, 3, 3, 3]
unikalne = set(liczby)
print(unikalne) # {1, 2, 3}
print(3 in unikalne) # True — średnio O(1)
# Operacje teoriomnogościowe
a = {1, 2, 3}
b = {2, 3, 4}
print(a | b) # {1, 2, 3, 4} suma
print(a & b) # {2, 3} część wspólna
print(a - b) # {1} różnica
print(a ^ b) # {1, 4} różnica symetryczna
# set jest mutowalny
a.add(5)
print(a) # {1, 2, 3, 5}
# frozenset jest niemutowalny i haszowalny
fs = frozenset({1, 2, 3})
# fs.add(4) # AttributeError: 'frozenset' has no attribute 'add'
zbior_zbiorow = {frozenset({1, 2}), frozenset({3, 4})}
print(zbior_zbiorow) # działa, bo frozenset jest haszowalny
# Pusty zbiór: set(), nie {}
print(type({})) # <class 'dict'>
print(type(set())) # <class 'set'>
Diagram: (tabela porównawcza poniżej)
| Cecha | set |
frozenset |
|---|---|---|
| Mutowalność | mutowalny | niemutowalny |
| Haszowalny (klucz dict / element set) | nie | tak |
| Metody modyfikujące | add, discard, update... |
brak |
| Operacje teoriomnogościowe | tak | tak |
| Tworzenie | {1, 2} lub set(...) |
frozenset(...) |
Materiały
Jak działa slicing (wycinanie) sekwencji i czym jest składnia [start:stop:step]?
Odpowiedź w 30 sekund:
Slicing tworzy nowy fragment sekwencji za pomocą seq[start:stop:step], gdzie start jest włączany, stop wyłączany, a step to krok (może być ujemny, by iść od końca). Wszystkie trzy parametry są opcjonalne i mają sensowne wartości domyślne, więc seq[:] zwraca kopię całości, a seq[::-1] odwraca sekwencję.
Odpowiedź w 2 minuty:
Slicing pozwala wyciąć podsekwencję z dowolnej sekwencji (list, str, tuple, range, bytes). Składnia to seq[start:stop:step]. start to indeks pierwszego elementu (włącznie, domyślnie 0 lub koniec przy ujemnym kroku), stop to indeks, przy którym wycinanie się zatrzymuje (wyłącznie, domyślnie długość sekwencji), a step to krok między kolejnymi elementami (domyślnie 1).
Indeksy mogą być ujemne — liczą się od końca, gdzie -1 to ostatni element. Ujemny step odwraca kierunek iteracji, dlatego seq[::-1] to idiomatyczne odwrócenie sekwencji. Slicing jest bezpieczny względem zakresu: indeksy wykraczające poza sekwencję są obcinane bez zgłaszania IndexError, więc [1, 2, 3][10:20] zwraca po prostu pustą listę.
Dla list i innych sekwencji slicing zwraca nowy obiekt (płytka kopia wybranego fragmentu), nie referencję — to wygodny sposób na kopiowanie listy przez lista[:]. Co istotne, dla list slice można też przypisać (lista[1:3] = [...]) lub usunąć (del lista[1:3]), co modyfikuje listę w miejscu i może nawet zmienić jej długość.
Pod spodem seq[start:stop:step] jest cukrem składniowym dla obiektu slice(start, stop, step). We własnych klasach możesz obsłużyć wycinanie, implementując __getitem__, gdzie argument będzie obiektem typu slice — pomocna jest wtedy metoda slice.indices(len), normalizująca granice do konkretnej długości.
Przykład kodu:
dane = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(dane[2:5]) # [2, 3, 4] — start włącznie, stop wyłącznie
print(dane[:3]) # [0, 1, 2] — od początku
print(dane[7:]) # [7, 8, 9] — do końca
print(dane[:]) # cała kopia listy
print(dane[::2]) # [0, 2, 4, 6, 8] — co drugi
print(dane[::-1]) # [9, 8, ..., 0] — odwrócenie
print(dane[-3:]) # [7, 8, 9] — trzy ostatnie
# Bezpieczne względem zakresu — brak IndexError
print(dane[5:100]) # [5, 6, 7, 8, 9]
# Przypisanie i usuwanie przez slice (modyfikacja w miejscu)
kopia = dane[:]
kopia[1:4] = [99]
print(kopia) # [0, 99, 4, 5, 6, 7, 8, 9]
del kopia[:2]
print(kopia) # [4, 5, 6, 7, 8, 9]
# Slicing działa też na łańcuchach znaków
print("Python"[::-1]) # nohtyP
# Pod spodem to obiekt slice
print(dane[slice(2, 5)]) # [2, 3, 4]
Materiały
Czym są list, dict i set comprehensions oraz jakie dają korzyści?
Odpowiedź w 30 sekund:
Comprehensions to zwięzła składnia tworzenia kolekcji z iterowalnego źródła w jednym wyrażeniu, np. [x*x for x in zakres]. Istnieją wersje dla listy ([...]), słownika ({k: v ...}), zbioru ({...}) oraz wyrażenie generatorowe ((...)). Są czytelniejsze i zwykle szybsze od ręcznej pętli z append.
Odpowiedź w 2 minuty:
Comprehension to deklaratywny sposób budowania kolekcji: opisujesz, co ma się znaleźć w wyniku, zamiast krok po kroku budować ją pętlą. List comprehension [wyrażenie for element in iterowalne if warunek] tworzy listę, set comprehension {...} zbiór, a dict comprehension {klucz: wartość for ...} słownik. Otoczenie nawiasami okrągłymi (...) daje wyrażenie generatorowe, które nie buduje całej kolekcji w pamięci, lecz produkuje elementy leniwie — idealne dla dużych zbiorów danych.
Korzyści to przede wszystkim czytelność i zwięzłość: jedna linia zamiast czterech (inicjalizacja, pętla, warunek, append). Comprehensions są też zwykle szybsze od równoważnej pętli, bo dodawanie elementów odbywa się na poziomie zoptymalizowanego bajtkodu, bez powtarzalnego wyszukiwania metody append. Pozwalają na filtrowanie (if na końcu), transformację oraz wyrażenia warunkowe (x if c else y przed for) i zagnieżdżanie wielu klauzul for.
Trzeba jednak zachować umiar. Zbyt złożone, wielopoziomowe comprehensions stają się nieczytelne — w takich przypadkach lepsza jest zwykła pętla. Od Pythona 3 zmienna pętli w comprehension ma własny zakres i nie „wycieka" do otaczającego kodu (inaczej niż w Pythonie 2).
Praktyczna wskazówka: jeśli i tak będziesz iterować po wyniku tylko raz (np. w sum() czy any()), użyj wyrażenia generatorowego zamiast list comprehension — oszczędzisz pamięć, bo nie zmaterializujesz pośredniej listy.
Przykład kodu:
# List comprehension — kwadraty liczb parzystych
kwadraty = [x * x for x in range(10) if x % 2 == 0]
print(kwadraty) # [0, 4, 16, 36, 64]
# Z wyrażeniem warunkowym (przed for)
etykiety = ["parzysta" if x % 2 == 0 else "nieparzysta" for x in range(4)]
print(etykiety) # ['parzysta', 'nieparzysta', 'parzysta', 'nieparzysta']
# Dict comprehension — odwrócenie mapowania
ceny = {"chleb": 5, "mleko": 3}
odwrocone = {wartość: klucz for klucz, wartość in ceny.items()}
print(odwrocone) # {5: 'chleb', 3: 'mleko'}
# Set comprehension — unikalne długości słów
słowa = ["kot", "pies", "ryba", "lew"]
długości = {len(s) for s in słowa}
print(długości) # {3, 4}
# Wyrażenie generatorowe — leniwe, oszczędne pamięciowo
suma_kwadratów = sum(x * x for x in range(1_000_000))
print(suma_kwadratów)
# Zagnieżdżenie: spłaszczenie macierzy
macierz = [[1, 2], [3, 4], [5, 6]]
płaska = [el for wiersz in macierz for el in wiersz]
print(płaska) # [1, 2, 3, 4, 5, 6]
Materiały
Jak Python obsługuje liczby całkowite o dowolnej precyzji (arbitrary precision integers)?
Odpowiedź w 30 sekund:
Typ int w Pythonie 3 nie ma stałego rozmiaru — automatycznie rośnie, by pomieścić dowolnie dużą liczbę, ograniczoną jedynie dostępną pamięcią. Nie ma więc przepełnienia (overflow) jak w językach z 32/64-bitowymi liczbami; obliczenia takie jak 2 ** 1000 po prostu działają.
Odpowiedź w 2 minuty:
W przeciwieństwie do C czy Javy, gdzie int ma ustalony rozmiar (np. 64 bity) i może się przepełnić, Python 3 implementuje liczby całkowite jako liczby o dowolnej precyzji (bignum). CPython przechowuje int jako tablicę „cyfr" o dużej podstawie (zwykle 30-bitowych na platformach 64-bitowych) wraz ze znakiem. Gdy wynik operacji przekracza pojemność bieżącej reprezentacji, Python automatycznie alokuje więcej pamięci — programista nie musi nic robić, a jedynym praktycznym limitem jest ilość dostępnej pamięci.
Dzięki temu nie istnieje pojęcie przepełnienia liczb całkowitych. Operacje takie jak silnia dużej liczby, potęgowanie kryptograficzne czy liczby Fibonacciego o setkach cyfr działają poprawnie bez żadnych specjalnych bibliotek. Ceną jest to, że arytmetyka na bardzo dużych liczbach jest wolniejsza niż na natywnych liczbach maszynowych i zużywa więcej pamięci.
Dla małych liczb CPython stosuje optymalizację cache'owania: obiekty int z zakresu od -5 do 256 są tworzone raz przy starcie interpretera i współdzielone, dlatego a is b dla a = 256; b = 256 zwraca True, ale dla 257 już niekoniecznie. Tej cechy nie należy traktować jako gwarancji języka — do porównywania wartości zawsze używaj ==, a is rezerwuj dla None.
Warto wiedzieć o dwóch niuansach. Po pierwsze, dzielenie / zawsze zwraca float, więc na bardzo dużych liczbach może utracić precyzję — dla dokładnego wyniku całkowitego użyj //. Po drugie, od Pythona 3.11 istnieje limit konwersji dużych int na/z str (domyślnie 4300 cyfr) jako zabezpieczenie przed atakami DoS; można go zmienić przez sys.set_int_max_str_digits().
Przykład kodu:
# Brak przepełnienia — dowolnie duże liczby
duza = 2 ** 1000
print(duza) # liczba o ponad 300 cyfrach
print(len(str(duza))) # 302
import math
print(math.factorial(50)) # ogromna liczba, działa bez problemu
# Liczba liczona iteracyjnie nadal jest dokładna
def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
print(fib(200)) # dokładny wynik, setki cyfr
# Cache małych intów (szczegół implementacji CPythona, NIE gwarancja!)
a = 256
b = 256
print(a is b) # True (zazwyczaj)
x = 257
y = 257
print(x is y) # może być False
print(x == y) # True — do wartości zawsze ==
# Dzielenie: / daje float (możliwa utrata precyzji), // daje int
print(10 ** 20 // 3) # dokładny int
print(10 ** 20 / 3) # float — przybliżenie
Materiały
Jaka jest różnica między operatorami is a ==?
Odpowiedź w 30 sekund:
== porównuje wartości (czy obiekty są równe, wywołując __eq__), a is porównuje tożsamość (czy to dokładnie ten sam obiekt w pamięci, czyli ta sama wartość id()). Operatora is używaj przede wszystkim do porównań z None, True, False; do porównywania wartości zawsze używaj ==.
Odpowiedź w 2 minuty:
== to operator równości — sprawdza, czy dwa obiekty mają równą wartość, delegując do metody __eq__. is to operator tożsamości — sprawdza, czy dwie nazwy wskazują na ten sam obiekt w pamięci, co jest równoważne porównaniu id(a) == id(b). Dwa różne obiekty mogą być równe (== zwraca True), ale nie być tym samym obiektem (is zwraca False).
Najczęstsza pułapka wynika z cache'owania małych obiektów przez CPython. Małe liczby całkowite (-5 do 256) i niektóre krótkie/„internowane" stringi są współdzielone, więc a is b bywa True dla małych wartości, a False dla większych — mimo że == zawsze zwraca True. To szczegół implementacji, nie cecha języka, dlatego porównywanie wartości operatorem is jest błędem, który potrafi działać „przypadkiem" i zawieść w innym miejscu. Nowsze wersje Pythona ostrzegają o tym przez SyntaxWarning: "is" with a literal.
Operatora is używaj świadomie tam, gdzie naprawdę chodzi o tożsamość: porównania z singletonami None, True, False (if x is None:), sprawdzanie czy zmienna to ten sam obiekt po przypisaniu, albo strażniki typu _SENTINEL = object(). Do wszystkiego innego — liczby, łańcuchy, listy, własne obiekty — używaj ==.
Warto pamiętać, że zachowanie == możesz dostosować, nadpisując __eq__ w klasie (a w parze z nim __hash__, jeśli obiekt ma być haszowalny). Natomiast is jest nieprzeciążalne — zawsze porównuje czystą tożsamość obiektu i nie da się tego zmienić.
Przykład kodu:
# Listy o tej samej zawartości: równe, ale to różne obiekty
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — taka sama wartość
print(a is b) # False — różne obiekty
print(a is a) # True — ten sam obiekt
# Pułapka cache'owania małych intów
x = 256
y = 256
print(x is y) # True (cache) — NIE polegaj na tym
m = 1000
n = 1000
print(m is n) # zwykle False
print(m == n) # True — poprawny sposób
# Idiomatyczne użycie is dla None
def funkcja(arg=None):
if arg is None: # poprawnie
arg = []
return arg
# Nadpisanie __eq__ wpływa na ==, ale nie na is
class Punkt:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
p1 = Punkt(1, 2)
p2 = Punkt(1, 2)
print(p1 == p2) # True — wg __eq__
print(p1 is p2) # False — różne obiekty
Diagram: (tabela porównawcza poniżej)
| Aspekt | == |
is |
|---|---|---|
| Co porównuje | wartość (równość) | tożsamość (ten sam obiekt) |
| Mechanizm | wywołuje __eq__ |
porównuje id() |
| Przeciążalny | tak (__eq__) |
nie |
| Typowe użycie | porównywanie wartości | x is None, singletony, sentinel |
| Pułapka | — | cache małych int / internowane str |
Materiały
Funkcje, zakresy i domknięcia
Jak definiuje się funkcję w Pythonie i co oznacza, że funkcje są „pierwszej klasy" (first-class)?
Odpowiedź w 30 sekund:
Funkcję definiuje się słowem kluczowym def, po którym następuje nazwa, lista parametrów w nawiasach i wcięty blok ciała. To, że funkcje są „pierwszej klasy" oznacza, że Python traktuje je jak każdą inną wartość (obiekt): można je przypisać do zmiennej, przekazać jako argument, zwrócić z innej funkcji i przechowywać w strukturach danych.
Odpowiedź w 2 minuty:
Definicja funkcji w Pythonie ma postać def nazwa(parametry): zakończoną dwukropkiem, a jej ciało to wcięty blok kodu. Funkcja może zwracać wartość przez return (gdy go brakuje, zwraca None). Sama instrukcja def jest wykonywana w czasie działania programu — tworzy obiekt funkcji i wiąże go z nazwą w bieżącym zakresie. Dlatego funkcja istnieje dopiero po wykonaniu jej definicji.
Określenie „funkcje pierwszej klasy" (ang. first-class functions) oznacza, że funkcja jest pełnoprawnym obiektem (instancją typu function). Wynika z tego kilka praktycznych konsekwencji: funkcję można przypisać do zmiennej (f = moja_funkcja), umieścić na liście lub w słowniku, przekazać do innej funkcji jako argument oraz zwrócić jako wynik. Funkcje przyjmujące lub zwracające inne funkcje nazywamy funkcjami wyższego rzędu (np. map, filter, sorted z argumentem key).
Ponieważ funkcja jest obiektem, posiada też atrybuty: __name__ (nazwa), __doc__ (docstring) czy __defaults__ (wartości domyślne). Warto rozróżnić odwołanie do funkcji (f) od jej wywołania (f()) — to częsta pomyłka początkujących, którzy zapominają o nawiasach albo dodają je tam, gdzie chcą przekazać samą funkcję.
Przykład kodu:
def powitaj(imie: str) -> str:
"""Zwraca powitanie dla podanego imienia."""
return f"Cześć, {imie}!"
# Przypisanie funkcji do zmiennej (bez nawiasów = sama funkcja)
f = powitaj
print(f("Ania")) # Cześć, Ania!
# Przekazanie funkcji jako argumentu (funkcja wyższego rzędu)
def zastosuj(funkcja, wartosc):
return funkcja(wartosc)
print(zastosuj(powitaj, "Bartek")) # Cześć, Bartek!
# Przechowywanie funkcji w strukturach danych
operacje = {"powitanie": powitaj, "wielkie": str.upper}
print(operacje["wielkie"]("python")) # PYTHON
# Funkcja jest obiektem z atrybutami
print(powitaj.__name__) # powitaj
print(powitaj.__doc__) # Zwraca powitanie dla podanego imienia.
Materiały
Czym są argumenty *args i **kwargs i kiedy się ich używa?
Odpowiedź w 30 sekund:
*args zbiera dodatkowe argumenty pozycyjne do krotki (tuple), a **kwargs zbiera dodatkowe argumenty nazwane do słownika (dict). Używa się ich, gdy funkcja ma przyjmować zmienną liczbę argumentów lub gdy chcemy przekazać argumenty dalej do innej funkcji bez ich wymieniania.
Odpowiedź w 2 minuty:
Nazwy args i kwargs to tylko konwencja — istotne są gwiazdki. Pojedyncza gwiazdka * przed parametrem powoduje, że Python pakuje wszystkie nadmiarowe argumenty pozycyjne do krotki. Podwójna gwiazdka ** pakuje nadmiarowe argumenty nazwane (klucz=wartość) do słownika. W definicji funkcji obowiązuje kolejność: najpierw zwykłe parametry, potem *args, potem parametry keyword-only, na końcu **kwargs.
Gwiazdki działają też w drugą stronę — przy wywołaniu funkcji służą do rozpakowania (unpacking) kolekcji. *lista rozbija sekwencję na osobne argumenty pozycyjne, a **slownik rozbija słownik na argumenty nazwane. To bardzo wygodne przy przekazywaniu argumentów „w przelocie", np. w dekoratorach i funkcjach opakowujących (wrapper(*args, **kwargs)), które nie powinny znać konkretnej sygnatury opakowywanej funkcji.
Typowe zastosowania to: funkcje o nieokreślonej liczbie argumentów (jak print czy max), przekazywanie argumentów do funkcji nadrzędnej przez super().__init__(*args, **kwargs), oraz budowa elastycznych API i dekoratorów. Pułapka: nadużywanie *args/**kwargs w publicznym API pogarsza czytelność i podpowiedzi IDE — gdy zestaw parametrów jest znany, lepiej wypisać je jawnie.
Przykład kodu:
def suma(*args):
# args to krotka wszystkich argumentów pozycyjnych
return sum(args)
print(suma(1, 2, 3, 4)) # 10
def opis(**kwargs):
# kwargs to słownik argumentów nazwanych
for klucz, wartosc in kwargs.items():
print(f"{klucz} = {wartosc}")
opis(imie="Ola", wiek=30) # imie = Ola \n wiek = 30
# Łączenie wszystkich rodzajów parametrów
def zarejestruj(nazwa, *tagi, aktywny=True, **metadane):
print(nazwa, tagi, aktywny, metadane)
zarejestruj("plik", "wazne", "pilne", aktywny=False, autor="Kuba")
# plik ('wazne', 'pilne') False {'autor': 'Kuba'}
# Rozpakowanie przy WYWOŁANIU (operacja odwrotna)
liczby = [1, 2, 3]
dane = {"imie": "Ewa", "wiek": 25}
print(suma(*liczby)) # 6 — rozpakowanie listy
opis(**dane) # imie = Ewa \n wiek = 25 — rozpakowanie słownika
Materiały
Jak działają argumenty domyślne i na czym polega pułapka mutowalnego argumentu domyślnego?
Odpowiedź w 30 sekund:
Wartości domyślne są obliczane RAZ, w momencie definicji funkcji, a nie przy każdym wywołaniu. Jeśli domyślną wartością jest obiekt mutowalny (lista, słownik, zbiór), to wszystkie wywołania bez tego argumentu współdzielą ten sam obiekt — zmiany kumulują się między wywołaniami. Poprawnym wzorcem jest domyślne None i utworzenie nowego obiektu w ciele funkcji.
Odpowiedź w 2 minuty:
Argument domyślny pozwala pominąć wartość przy wywołaniu — wtedy użyta zostaje wartość z definicji. Kluczowy szczegół: wyrażenie definiujące wartość domyślną jest ewaluowane jednokrotnie, kiedy interpreter wykonuje instrukcję def. Wynik zostaje zapisany w atrybucie __defaults__ obiektu funkcji i ten sam obiekt jest używany przy każdym kolejnym wywołaniu, w którym argument pominięto.
Dla wartości niemutowalnych (liczby, łańcuchy, None, krotki) nie ma to znaczenia. Problem pojawia się przy obiektach mutowalnych. Klasyczna pułapka to def dodaj(element, lista=[]): pusta lista powstaje raz i jest współdzielona przez wszystkie wywołania bez podanej listy. Pierwsze wywołanie doda element do listy, ale drugie zobaczy listę z poprzednim elementem — wynik „pamięta" historię, co prawie nigdy nie jest zamierzone i prowadzi do trudnych do wykrycia błędów.
Rozwiązaniem jest idiom sentinel z None: jako wartość domyślną podajemy None, a w ciele funkcji sprawdzamy if lista is None: lista = []. Dzięki temu nowy obiekt powstaje przy każdym wywołaniu. Jeśli None jest poprawną wartością biznesową, można użyć dedykowanego znacznika, np. _SENTINEL = object(). Narzędzia takie jak pylint (W0102 dangerous-default-value) wykrywają tę pułapkę automatycznie.
Przykład kodu:
# ŹLE — mutowalna lista współdzielona między wywołaniami
def dodaj_zle(element, lista=[]):
lista.append(element)
return lista
print(dodaj_zle(1)) # [1]
print(dodaj_zle(2)) # [1, 2] <- niespodzianka! pamięta poprzedni stan
print(dodaj_zle(3)) # [1, 2, 3]
# DOBRZE — idiom z None tworzy nową listę przy każdym wywołaniu
def dodaj_dobrze(element, lista=None):
if lista is None:
lista = []
lista.append(element)
return lista
print(dodaj_dobrze(1)) # [1]
print(dodaj_dobrze(2)) # [2] <- za każdym razem świeża lista
print(dodaj_dobrze(3)) # [3]
# Dowód: wartość domyślna powstaje RAZ i jest pamiętana w obiekcie funkcji
print(dodaj_zle.__defaults__) # ([1, 2, 3],) — ta sama lista
Diagram:
flowchart TD
A["def f(x, lst=[])"] -->|"ewaluacja RAZ przy definicji"| B["[] zapisane w f.__defaults__"]
B --> C["wywołanie 1: f(1)"]
B --> D["wywołanie 2: f(2)"]
B --> E["wywołanie 3: f(3)"]
C --> F["ten sam obiekt listy"]
D --> F
E --> F
F --> G["stan kumuluje się: [1, 2, 3]"]
Materiały
lambda i kiedy warto jej używać, a kiedy nie?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziglobal i nonlocal?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziProgramowanie obiektowe
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziself i dlaczego musi być przekazywane jawnie?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi__init__ i czym różni się od __new__?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi@classmethod, @staticmethod a zwykłą metodą instancji?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi@property i do czego służy?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzidataclasses i jakie problemy rozwiązują?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi_nazwa i __nazwa (name mangling)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziabc?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziIteratory i generatory
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi__iter__ i __next__)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziyield zmienia działanie funkcji?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziyield from?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziitertools w praktyce? Podaj przykłady.
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzirange jest pamięciowo wydajny mimo reprezentowania dużych zakresów?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziDekoratory i programowanie funkcyjne
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzifunctools.wraps i dlaczego warto go stosować?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzifunctools.lru_cache i kiedy warto go używać?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzimap, filter i reduce?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzifunctools.partial i w jakich sytuacjach się przydaje?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziObsługa wyjątków i context managery
try / except / else / finally?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziBaseException, Exception)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziwith i czym jest context manager?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi__enter__/__exit__ oraz contextlib)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziraise ... from ... i jak działa łańcuchowanie wyjątków (exception chaining)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziexcept:?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziModuły, pakiety i zarządzanie zależnościami
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi__init__.py?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziif __name__ == "__main__": i kiedy się jej używa?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzivenv) i dlaczego warto go używać?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzipip i pliku requirements.txt?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzipip, pipenv, poetry oraz conda?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzisys.path) i czym jest zmienna PYTHONPATH?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziZarządzanie pamięcią i GIL
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi__slots__ i jak wpływają na zużycie pamięci oraz wydajność?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzicProfile, timeit, memory_profiler)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziWspółbieżność i programowanie asynchroniczne
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzithreading, a kiedy multiprocessing?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziasyncio i jak działa pętla zdarzeń (event loop)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziasync i await?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziconcurrent.futures (ThreadPoolExecutor, ProcessPoolExecutor)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziNowoczesny Python: typowanie i nowe funkcje
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzimypy i jak pomaga w statycznej analizie typów?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi%, str.format() i konkatenacją?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi:=) i kiedy go używać?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzimatch / case) wprowadzone w Pythonie 3.10?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzityping (np. Optional, Union, list[int], Callable, generyki)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziProtocol i typowanie strukturalne (statyczny duck typing)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziTestowanie i dobre praktyki
unittest a pytest?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzipytest i jak je uruchamiać?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziunittest.mock oraz monkeypatch?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzipytest i jak zarządzają cyklem życia danych testowych?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziblack, flake8, ruff)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzipdb, breakpoint(), logowanie)?
Odpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 82 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi