Gdy list comprehensions zaskoczy, nigdy więcej nie napiszesz pętli for dołączającej do listy. To nie tylko cukier składniowy — wyraźnie sygnalizują intencję i działają szybciej niż równoważna pętla w CPython dzięki optymalizacji kodu bajtowego. Według oficjalnego samouczka Pythona comprehensions zapewniają zwięzły sposób tworzenia list na podstawie istniejących sekwencji lub innych iterowalnych obiektów. Ten artykuł najpierw buduje model mentalny, a następnie obejmuje każdy prawdziwy wzorzec: filtrowanie, zagnieżdżanie, dict i set comprehensions oraz jeden przypadek, gdy powinieneś sięgnąć po zwykłą pętlę for.

Podstawowy wzorzec

Anatomia list comprehension to [wyrażenie for element in iterowalny]. Trzy części: wyrażenie wyjściowe (czym staje się każdy element), zmienna pętli i iterowalny, z którego się czerpie. Zacznij od znajomej pętli i zredukuj ją.

python
# Plain for loop — building a list of file sizes in KB
file_sizes_bytes = [1024, 204800, 51200, 3145728, 8192]

sizes_kb = []
for size in file_sizes_bytes:
    sizes_kb.append(size / 1024)

# print(sizes_kb)  →  [1.0, 200.0, 50.0, 3072.0, 8.0]

# Same result as a list comprehension — one line, same meaning
sizes_kb = [size / 1024 for size in file_sizes_bytes]

Comprehension czyta się prawie jak angielski: „daj mi size / 1024 dla każdego size w file_sizes_bytes". Ta jasność to prawdziwa wygrana — czytelnik nie musi śledzić wywołania append, aby zrozumieć, co budujesz.

python
# Another common pattern: deriving one list from another
usernames = ["alice", "bob", "carol"]

# Build a list of display names
display_names = [name.capitalize() for name in usernames]
# ['Alice', 'Bob', 'Carol']

# Or extract a single field from a list of dicts
users = [
    {"id": 1, "name": "Alice", "role": "admin"},
    {"id": 2, "name": "Bob",   "role": "viewer"},
    {"id": 3, "name": "Carol", "role": "editor"},
]

names = [user["name"] for user in users]
# ['Alice', 'Bob', 'Carol']

Filtrowanie z if

Dodaj warunek na końcu, a tylko elementy, które go spełniają, trafią na listę wyjściową: [wyrażenie for element in iterowalny if warunek]. To wzorzec zastępujący kombinację for + if + append.

python
# Loop version — extract only active users
active_users = []
for user in users:
    if user["active"]:
        active_users.append(user["name"])

# Comprehension version — identical result
users = [
    {"name": "Alice", "active": True},
    {"name": "Bob",   "active": False},
    {"name": "Carol", "active": True},
    {"name": "Dave",  "active": False},
]

active_users = [user["name"] for user in users if user["active"]]
# ['Alice', 'Carol']
python
# Filtering a raw CSV row — drop blanks and whitespace-only values
raw_row = ["[email protected]", "", "  ", "editor", " "]

clean_row = [field.strip() for field in raw_row if field.strip()]
# ['[email protected]', 'editor']

# Filtering a list of log levels
log_lines = [
    "INFO  server started",
    "DEBUG loading config",
    "ERROR database timeout",
    "DEBUG query took 450ms",
    "ERROR disk space low",
]

errors = [line for line in log_lines if line.startswith("ERROR")]
# ['ERROR database timeout', 'ERROR disk space low']

Praca z ciągami znaków

Przetwarzanie ciągów znaków to miejsce, gdzie comprehensions naprawdę się sprawdzają. Kombinacja bogatych metod ciągów Pythona i składni comprehension utrzymuje czytelność potoków transformacji bez pośrednich zmiennych.

python
# Strip whitespace from tags coming out of a form field
raw_tags = ["  python ", "data science", " machine-learning ", "API"]

tags = [tag.strip().lower() for tag in raw_tags]
# ['python', 'data science', 'machine-learning', 'api']

# Normalise email addresses from a signup CSV
raw_emails = ["[email protected]", "  [email protected]  ", "[email protected]"]

emails = [e.strip().lower() for e in raw_emails]
# ['[email protected]', '[email protected]', '[email protected]']

# Extract file extensions from a list of uploaded filenames
filenames = ["report.pdf", "avatar.PNG", "data.CSV", "archive.tar.gz", "notes.txt"]

extensions = [name.rsplit(".", 1)[-1].lower() for name in filenames if "." in name]
# ['pdf', 'png', 'csv', 'gz', 'txt']
Wskazówka: przy normalizacji danych wejściowych od użytkownika najpierw wywołaj strip, a potem lowercase — w przeciwnym razie wartość taka jak " [email protected] " przejdzie kontrolę .lower() na zawartości, ale nadal będzie zawierać wiodące spacje, które zepsują wyszukiwanie w bazie danych lub dopasowanie klucza JSON.

Dict i set comprehensions

Ten sam pomysł rozciąga się na słowniki i zbiory. Dict comprehension używa nawiasów klamrowych z parą klucz: wartość: {klucz: wartość for element in iterowalny}. Set comprehension pomija dwukropek i tworzy zdeduplikowaną kolekcję: {wyrażenie for element in iterowalny}.

python
# Invert a dict — swap keys and values
permissions = {"alice": "admin", "bob": "viewer", "carol": "editor"}

by_role = {role: name for name, role in permissions.items()}
# {'admin': 'alice', 'viewer': 'bob', 'editor': 'carol'}

# Build a fast lookup dict from a list of user records
users = [
    {"id": 101, "name": "Alice", "active": True},
    {"id": 102, "name": "Bob",   "active": False},
    {"id": 103, "name": "Carol", "active": True},
]

# O(1) lookups by ID — much faster than scanning the list every time
user_by_id = {user["id"]: user for user in users}
# {101: {...}, 102: {...}, 103: {...}}

# Access a user directly
user_by_id[102]["name"]  # 'Bob'
python
# Set comprehension — deduplicate a list of file extensions
uploads = ["report.pdf", "data.csv", "summary.pdf", "export.CSV", "notes.txt"]

unique_extensions = {name.rsplit(".", 1)[-1].lower() for name in uploads if "." in name}
# {'pdf', 'csv', 'txt'}  — order not guaranteed

Jedno zastrzeżenie dotyczące zbiorów: kolejność nie jest zachowana. Jeśli musisz zdeduplikować listę zachowując oryginalną kolejność, set comprehension jest złym narzędziem — użyj list(dict.fromkeys(items)), które korzysta z zachowania kolejności wstawiania słowników w Pythonie 3.7+.

Zagnieżdżone comprehensions

Możesz zagnieżdżać comprehensions, aby iterować po zagnieżdżonych strukturach. Najczęstszym przypadkiem użycia jest spłaszczanie listy list — macierz z parsowania CSV, stronicowane odpowiedzi API lub pogrupowane wyniki zapytań.

python
# Flatten a 2D list (e.g. paginated API results)
pages = [
    [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}],
    [{"id": 3, "name": "Carol"}],
    [{"id": 4, "name": "Dave"}, {"id": 5, "name": "Eve"}],
]

all_users = [user for page in pages for user in page]
# [{'id': 1, ...}, {'id': 2, ...}, {'id': 3, ...}, {'id': 4, ...}, {'id': 5, ...}]

# The order mirrors what nested for loops would produce:
# for page in pages:
#     for user in page:
#         all_users.append(user)

Czytaj zagnieżdżone comprehensions od lewej do prawej — zewnętrzna pętla jest pierwsza, wewnętrzna pętla druga. To odpowiada kolejności zagnieżdżonych pętli for.

python
# Two levels — fine and readable
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [cell for row in matrix for cell in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

# Three levels — stop here. Use a plain loop or a helper function.
# This is the point where readability dies:
cube = [[[1,2],[3,4]],[[5,6],[7,8]]]

# Don't do this:
flat3 = [val for layer in cube for row in layer for val in row]

# Do this instead:
flat3 = []
for layer in cube:
    for row in layer:
        flat3.extend(row)
Wyrażenia generatorów używają tej samej składni z nawiasami zamiast nawiasów kwadratowych: (user["id"] for user in users). Są obliczane leniwie — elementy są produkowane jeden po jednym zamiast budowania pełnej listy w pamięci. Używaj ich, gdy musisz iterować tylko raz lub gdy przekazujesz wynik bezpośrednio do sum(), max(), any() lub podobnych funkcji. Zapoznaj się z referencją wyrażeń generatorów, aby uzyskać pełne szczegóły.

Trójnik if/else wewnątrz comprehension

Gdy musisz przekształcić elementy w jedną z dwóch wartości — zamiast je usuwać — użyj wyrażenia trójnikowego w pozycji wyjściowej. Pozycja ma znaczenie: trójnik należy na początku, nie po iterowalnym.

python
# Correct: ternary is the output expression
users = [
    {"name": "Alice", "active": True},
    {"name": "Bob",   "active": False},
    {"name": "Carol", "active": True},
]

status = ["active" if user["active"] else "inactive" for user in users]
# ['active', 'inactive', 'active']

# Normalise a config value — replace None with a default
raw_config = ["/var/log", None, "/tmp", None, "/etc/app"]

paths = [path if path is not None else "/var/log/default" for path in raw_config]
# ['/var/log', '/var/log/default', '/tmp', '/var/log/default', '/etc/app']
python
# Common mistake — putting the ternary after the iterable (SyntaxError)
# status = ["active" for user in users if user["active"] else "inactive"]  # WRONG

# The trailing "if" is a filter — it drops non-matching items entirely.
# The ternary ("if ... else ...") in the output expression transforms all items.
# They serve different purposes. You can combine them:

# Keep only users, but show their status
status = ["active" if user["active"] else "inactive"
          for user in users
          if user["name"] != "Bob"]
# ['active', 'active']  — Bob was filtered out entirely

Kiedy zamiast tego używać pętli for

List comprehensions służą do budowania list. Jeśli w rzeczywistości wykonujesz efekty uboczne, użyj pętli for — nie comprehension. Ma to znaczenie poza stylem: comprehension, które odrzuca wynik, marnuje pamięć budując listę, której nikt nie używa, i ukrywa intencję przed czytelnikiem.

python
# Bad — comprehension for side effects (writing to a file, printing, calling an API)
[print(f"Processing user {user['name']}") for user in users]   # don't do this
[requests.post("/api/notify", json=user) for user in users]    # definitely don't do this

# Good — plain for loop makes the intent obvious
for user in users:
    print(f"Processing user {user['name']}")

for user in users:
    requests.post("/api/notify", json=user)
python
# Bad — comprehension with logic complex enough to need comments or multiple steps
result = [
    user["name"].strip().lower()
    if user.get("active") and user.get("email_verified")
    else user["name"].strip().lower() + " (unverified)"
    for user in users
    if user.get("role") in ("admin", "editor") and user.get("last_login") is not None
]

# Good — break it out when the logic is this involved
result = []
for user in users:
    if user.get("role") not in ("admin", "editor"):
        continue
    if user.get("last_login") is None:
        continue
    name = user["name"].strip().lower()
    if not (user.get("active") and user.get("email_verified")):
        name += " (unverified)"
    result.append(name)
  • Użyj comprehension, gdy budujesz nową listę z istniejącego iterowalnego z wyraźnym wyrażeniem i opcjonalnym filtrem.
  • Użyj pętli for, gdy wykonujesz efekty uboczne — I/O, wywołania sieciowe, drukowanie, mutowanie zewnętrznego stanu.
  • Użyj pętli for, gdy logika transformacji wymaga wielu linii, pośrednich zmiennych lub komentarzy, aby ją zrozumieć.
  • Użyj wyrażenia generatora, gdy musisz iterować tylko raz lub przekazać bezpośrednio do sum(), any(), max() — żadna lista nie jest potrzebna w pamięci.

Uwaga o wydajności

List comprehensions są znacząco szybsze niż równoważna pętla for + append w CPython. Powodem jest kod bajtowy: comprehension kompiluje się do dedykowanego opcode'u LIST_APPEND, który unika wyszukiwania atrybutu na list.append przy każdej iteracji. Wiki wskazówek dotyczących wydajności Pythona omawia to, a różnica wynosi zazwyczaj 10–40% dla obciążeń czystego Pythona w zależności od rozmiaru listy.

python
import timeit

data = list(range(100_000))

# for + append
def with_loop():
    result = []
    for x in data:
        result.append(x * 2)
    return result

# list comprehension
def with_comprehension():
    return [x * 2 for x in data]

# generator expression — no list built at all
def with_generator():
    return sum(x * 2 for x in data)

# Typical results on CPython 3.12:
# with_loop():          ~7.2 ms
# with_comprehension(): ~4.8 ms  (~33% faster)
# with_generator():     ~4.1 ms  (and uses O(1) memory vs O(n))

Jeśli nie potrzebujesz zmaterializowanej listy — przekazujesz wynik do sum(), any(), max() lub iterujesz raz — użyj wyrażenia generatora. Używa ono stałej pamięci niezależnie od rozmiaru wejścia, co ma znaczenie przy przetwarzaniu dużych eksportów CSV lub ładunków JSON w ciasnej pętli.

Podsumowanie

List comprehensions to jedna z tych funkcji Pythona, która przez tydzień wydaje się niezręczna, a potem staje się niemożliwa do życia bez niej. Model mentalny jest prosty: wyrażenie wyjściowe, zmienna pętli, iterowalny, opcjonalny filtr. Trzymaj się tego, a będziesz pisać czytelny, idiomatyczny Python. Gdy logika staje się złożona, to sygnał do powrotu do zwykłej pętli — to nie porażka, tylko właściwe narzędzie do pracy.

Jeśli pracujesz z danymi JSON w Pythonie — zamieniając odpowiedzi API na listy wartości, wyodrębniając pola z rekordów, budując słowniki wyszukiwań — narzędzia na tej stronie dobrze pasują do tego, czego właśnie się nauczyłeś. Wypróbuj Formater JSON, aby sprawdzić i ładnie wyświetlić ładunki JSON przed napisaniem comprehension, która je przetwarza, lub Formater CSV, aby zwalidować dane CSV przed parsowaniem do listy wierszy. Dla pełnej referencji językowej dokumentacja Pythona dotycząca list comprehensions i PEP 202 (oryginalna propozycja) są warte przeczytania.