Wyrażenia listowe

Wyrażenia listowe (ang. list comprehensions) to zwięzły i czytelny sposób tworzenia list w Pythonie. Pozwalają na generowanie nowych list na podstawie istniejących sekwencji lub innych iterowalnych obiektów, opcjonalnie z zastosowaniem filtrowania lub transformacji elementów. Są wydajniejsze i czytelniejsze niż budowanie list za pomocą tradycyjnych pętli for.

Podstawowa składnia

Ogólna postać wyrażenia listowego wygląda następująco:

nowa_lista = [wyrażenie for element in iterowalny_obiekt]

Jest to skrócony zapis równoważny poniższej pętli:

nowa_lista = []
for element in iterowalny_obiekt:
    nowa_lista.append(wyrażenie)

Przykład podstawowy

# Tradycyjna pętla
lista = []
for i in range(10):
    lista.append(i)
print(lista)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Wyrażenie listowe
lista = [i for i in range(10)]
print(lista)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Transformacja elementów

Wyrażenia listowe świetnie nadają się do przekształcania każdego elementu kolekcji:

# Kwadraty liczb
kwadraty = [i ** 2 for i in range(10)]
print(kwadraty)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# Zamiana liter na wielkie
imiona = ["anna", "jan", "maria", "piotr"]
imiona_wielkie = [imie.upper() for imie in imiona]
print(imiona_wielkie)  # ['ANNA', 'JAN', 'MARIA', 'PIOTR']

Wyrażenia listowe z warunkiem

Możemy dodać warunek if, aby filtrować elementy:

nowa_lista = [wyrażenie for element in iterowalny_obiekt if warunek]

Przykład z filtrowaniem

# Tylko liczby parzyste
parzyste = [i for i in range(10) if i % 2 == 0]
print(parzyste)  # [0, 2, 4, 6, 8]

# Imiona dłuższe niż 3 znaki
imiona = ["adam", "ewa", "kasia", "tomek", "jan", "grzegorz"]
dlugie_imiona = [imie for imie in imiona if len(imie) > 3]
print(dlugie_imiona)  # ['kasia', 'tomek', 'grzegorz']

Warunek if-else w wyrażeniu listowym

Możemy również stosować wyrażenia warunkowe (operator trójargumentowy) po stronie wyrażenia, nie jako filtr:

# Etykieta: "parzysta" lub "nieparzysta"
etykiety = ["parzysta" if i % 2 == 0 else "nieparzysta" for i in range(6)]
print(etykiety)  # ['parzysta', 'nieparzysta', 'parzysta', 'nieparzysta', 'parzysta', 'nieparzysta']

Wyrażenia listowe z funkcją

Wyrażenia listowe mogą wywoływać dowolne funkcje na elementach kolekcji:

def kwadrat(x):
    return x ** 2

lista = [kwadrat(i) for i in range(10)]
print(lista)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Zagnieżdżone wyrażenia listowe

Wyrażenia listowe mogą być zagnieżdżone — odpowiadają zagnieżdżonym pętlom for:

# Wszystkie pary (i, j) gdzie i != j
pary = [(i, j) for i in range(3) for j in range(3) if i != j]
print(pary)
# [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]

Spłaszczanie listy list

macierz = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
plaski = [element for wiersz in macierz for element in wiersz]
print(plaski)  # [1, 2, 3, 4, 5, 6, 7, 8, 9]

Wyrażenia słownikowe i zbiorowe

Python oferuje analogiczną składnię dla słowników i zbiorów:

Wyrażenia słownikowe (dict comprehensions)

# Słownik: liczba → jej kwadrat
kwadraty = {i: i ** 2 for i in range(6)}
print(kwadraty)  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Odwracanie klucz-wartość w słowniku
oryginalny = {'a': 1, 'b': 2, 'c': 3}
odwrocony = {v: k for k, v in oryginalny.items()}
print(odwrocony)  # {1: 'a', 2: 'b', 3: 'c'}

Wyrażenia zbiorowe (set comprehensions)

# Zbiór unikalnych długości słów
slowa = ["ala", "ma", "kota", "ala", "kota"]
dlugosci = {len(slowo) for slowo in slowa}
print(dlugosci)  # {2, 3, 4}

Wyrażenia generatorowe

Używając nawiasów okrągłych zamiast kwadratowych, uzyskujemy generator, który oblicza wartości leniwie (jeden po drugim), nie tworząc całej listy w pamięci:

# Generator
gen = (i ** 2 for i in range(10))
print(type(gen))   # <class 'generator'>
print(next(gen))   # 0
print(next(gen))   # 1

# Użycie w pętli
for v in (i ** 2 for i in range(5)):
    print(v)

Generatory są szczególnie przydatne, gdy pracujemy z dużymi zbiorami danych, które nie muszą być jednocześnie trzymane w pamięci.

Porównanie wydajności

Wyrażenia listowe są zazwyczaj szybsze od równoważnych pętli for, ponieważ Python optymalizuje je wewnętrznie. W przypadku bardzo dużych zbiorów danych warto jednak rozważyć użycie generatorów, które są oszczędniejsze pamięciowo.

from timeit import timeit

# Pętla for
t1 = timeit("[i**2 for i in range(1000)]", number=10000)

# Generator w sum()
t2 = timeit("sum(i**2 for i in range(1000))", number=10000)

print(f"Wyrażenie listowe: {t1:.3f}s")
print(f"Generator:         {t2:.3f}s")

Podsumowanie

Typ wyrażenia Składnia Wynik
Listowe [wyrażenie for x in it] list
Listowe z filtrem [wyrażenie for x in it if warunek] list
Słownikowe {k: v for x in it} dict
Zbiorowe {wyrażenie for x in it} set
Generatorowe (wyrażenie for x in it) generator

Wyrażenia listowe są idiomatycznym, czytelnym i wydajnym narzędziem Pythona, które warto stosować wszędzie tam, gdzie budujemy nową kolekcję na podstawie istniejących danych.

Najczęstsze wzorce i idiomy

Wzorzec Wyrażenie listowe Opis
Filtrowanie [x for x in dane if warunek(x)] Zachowanie wybranych elementów
Transformacja [f(x) for x in dane] Przekształcenie każdego elementu
Filtrowanie + transformacja [f(x) for x in dane if warunek(x)] Oba na raz
Spłaszczanie [x for sub in dane for x in sub] Lista list → płaska lista
Iloczyn kartezjański [(x, y) for x in A for y in B] Wszystkie pary
Zamiana wartości [x if x > 0 else 0 for x in dane] Warunkowe podstawienie
Indeksowanie [(i, x) for i, x in enumerate(dane)] Pary (indeks, wartość)

Typowe pułapki i dobre praktyki

Kiedy NIE stosować wyrażeń listowych

# ❌ Zbyt skomplikowane — trudne do zrozumienia
wynik = [f(x) for x in dane if g(x) > 0 for y in h(x) if y != 0]

# ✓ Lepiej użyć zwykłej pętli
wynik = []
for x in dane:
    if g(x) > 0:
        for y in h(x):
            if y != 0:
                wynik.append(f(x))

Pułapka z mutowalnymi obiektami

# ❌ Wszystkie wiersze to ta sama lista!
macierz = [[0] * 3] * 3
macierz[0][0] = 1
print(macierz)  # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

# ✓ Poprawnie — każdy wiersz to nowa lista
macierz = [[0] * 3 for _ in range(3)]
macierz[0][0] = 1
print(macierz)  # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]

Efekt uboczny — wyrażenia listowe nie powinny modyfikować stanu

# ❌ Nie używaj wyrażeń listowych dla efektów ubocznych
[print(x) for x in range(5)]  # Tworzy niepotrzebną listę [None, None, ...]

# ✓ Użyj pętli for
for x in range(5):
    print(x)

Zaawansowane przykłady praktyczne

Transpozycja macierzy

macierz = [[1, 2, 3],
            [4, 5, 6],
            [7, 8, 9]]

transponowana = [[wiersz[i] for wiersz in macierz] for i in range(len(macierz[0]))]
print(transponowana)  # [[1, 4, 7], [2, 5, 8], [3, 6, 9]]

Grupowanie elementów w paczki (chunking)

dane = list(range(10))
n = 3
paczki = [dane[i:i+n] for i in range(0, len(dane), n)]
print(paczki)  # [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Słownik częstości wystąpień

tekst = "abrakadabra"
czestosci = {znak: tekst.count(znak) for znak in set(tekst)}
print(czestosci)  # {'a': 5, 'b': 2, 'r': 2, 'k': 1, 'd': 1}

Filtrowanie słownika

oceny = {"Anna": 5, "Jan": 3, "Kasia": 4, "Tomek": 2, "Ewa": 5}
najlepsi = {k: v for k, v in oceny.items() if v >= 4}
print(najlepsi)  # {'Anna': 5, 'Kasia': 4, 'Ewa': 5}