G. Jagiella | ostatnia modyfikacja: 12.11.2024 |
Poniższy materiał jest powtórzeniem i przeglądem zagadnień z wykładu 5 (12.11.2024). Pewnie przykłady zostały poszerzone.
Listy składane, lub schematy konstrukcji list, to sposób tworzenia list z użyciem elementów innego obiektu iterowalnego (na przykład innej listy). Przykładowe poniższe wyrażenie tworzy nową listę, złożoną z kwadratów dziesięciu pierwszych liczb naturalnych:
lst = [n ** 2 for n in range(10)] # po prawej stronie '=' mamy listę składaną
print(lst)
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Ogólniejszą postacią listy składanej jest wyrażenie:
[expr for name in iterable]
gdzie: expr
to wyrażenie, name
to nazwa, iterable
to obiekt iterowalny. Kolejnymi elementami tak skonstruowanej listy stają się wartości expr
wyliczone dla kolejnych elementów z iterable
. W wyrażeniu expr
może występować nazwa name
, nazywająca te kolejne elementy obiektu iterable
.
W przykładzie powyżej, wyrażeniem expr
jest n ** 2
, gdzie n
to nazwa kolejnych obiektów z range(10)
. Stąd elementami powstałej listy są kwadraty n = 0, 1, ..., 9
.
Listy składane powinny budzić skojarzenia ze schematami zapisu zbiorów w matematyce. Zbiór dziesięciu pierwszych kwadratów można zapisać tak: $$\{n^2 : n \in \mathbb{N} \cap [0, 10)\}.$$
Zapis zbioru mówi, że jego elementami są wyrażenia $n^2$ dla $n \in \mathbb{N} \cap [0, 10)$, gdzie zbiór $\mathbb{N} \cap [0, 10)$ odpowiada range(10)
.
W listach, w odróżnieniu od zbiorów, wyróżniamy kolejność elementów. W listach składanych, elementy (wartości wyrażeń) dokładane są zgodnie z kolejnością elementów iterable
.
Kilka dalszych przykładów:
# reszty z dzielenia liczb 0, 4, 8, 12, 16 przez 3
lst = [i % 3 for i in range(0, 20, 4)]
print(lst)
# przebiegając przez napis 'python', c ma wartości `p', 'y', 't', ..., zaś c + c wynosi `pp`, `yy`, `tt`, ...
print([c + c for c in 'python'])
lst = [2, 5, 10, 20, 40] # pewna lista
print([1 / x for x in lst]) # lista odwrotności elementów z innej listy
[0, 1, 2, 0, 1] ['pp', 'yy', 'tt', 'hh', 'oo', 'nn'] [0.5, 0.2, 0.1, 0.05, 0.025]
Przy konstrukcji listy składanej można też dodać warunek, pozwalający na pominięcie obiektów z iterable
, które go nie spełniają. Ogólniejsza postać wyrażenia list składanych:
[expr for name in iterable if condition]
gdzie condition
to wyrażenie logiczne (wyliczające się do True
/False
), w którym można użyć nazwy name
. Przykładowo:
lst = [c for c in 'python python' if c != 'y'] # lista elementów (znaków) napisu, ale z pominięciem 'y'
print(lst)
lst = [1, 'aa', 2.0, [], (1, 2), 'bb'] # lista różnych obiektów
print([obj for obj in lst if type(obj) == str]) # "filtr": lista obiektów z lst, które są napisami
# lista połówek tych liczb naturalnych < 20, których kwadrat ma cyfrę jedności 1
lst = [n / 2 for n in range(20) if n ** 2 % 10 == 1]
print(lst)
['p', 't', 'h', 'o', 'n', ' ', 'p', 't', 'h', 'o', 'n'] ['aa', 'bb'] [0.5, 4.5, 5.5, 9.5]
Odnosząc się do intuicji ze zbiorami i matematyką, odpowiednikiem ostatniego z powyższych trzech przykładów jest: $$\{\frac{n}{2} : n \in \mathbb{N} \cap [0, 20) \land n^2 \text{ mod } 10 = 1\}.$$
Konstrukcję listy składanej można traktować jako skrót (tzw. lukier syntaktyczny) następującego kodu, tworzącego listę:
lst = [] # początkowo pusta lista
for name in iterable: # przebiegając po iterable...
if condition: # ... jeśli spełniony jest warunek dla obiektu...
lst.append(expr) # ... to dokładamy wyrażenie na listę
# lst to skonstruowana lista
Zatem np. przypisanie:
lst = [n / 2 for n in range(20) if n ** 2 % 10 == 1]
Tłumaczy się na:
lst = []
for n in range(20):
if n ** 2 % 10 == 1:
lst.append(n / 2)
Wreszcie, tworząc listę składaną nie musimy ograniczać się do wyłącznie jednego obiektu iterowalnego. Przykład:
lst = [n + m for n in range(3) for m in range(4)]
print(lst)
[0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5]
Wyrażenie tworzy listę sum n + m
dla n
z range(3)
i m
z range(4)
. Kolejność przebiegania jest zgodna z następującym odpowiednikiem powyższego przykładu:
lst = []
for n in range(3):
for m in range(4):
lst.append(n + m)
W takich wyrażeniach wciąż możemy dołożyć warunek logiczny na nazwy n
i m
(lub dowolne inne), na przykład:
lst = [c * n for c in 'python' for n in range(1, 3) if c != 'y' or n == 2]
print(lst)
['p', 'pp', 'yy', 't', 'tt', 'h', 'hh', 'o', 'oo', 'n', 'nn']
Jedno z zastosowań: "iloczyn kartezjański" dwóch (lub więcej) obiektów iterowalnych:
txt = 'abc'
lst = [1, 2, 3]
product = [(c, n) for c in txt for n in lst] # wyrażenie (c, n), czyli para (krotka) dwóch obiektów
print(product)
[('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3), ('c', 1), ('c', 2), ('c', 3)]
Używając list, możemy reprezentować dwuwymiarowe układy liczb (lub innych obiektów), na przykład macierze rozmiaru $n \times m$. Pojedynczy wiersz macierzy możemy przedstawić jako listę, a macierz złożoną z wierszy jako listę list reprezentujących wiersze. Przykładowo, macierze
$$\begin{pmatrix} 1 & 2 \\ -1 & 2\end{pmatrix} \text{ i } \begin{pmatrix} 0 & 1 \\ 4 & 2 \\ -1 & 3\end{pmatrix}$$
można przedstawić odpowiednio jako listy [[1, 2], [-1, 2]]
i [[0, 1], [4, 2], [-1, 3]]
. Dla listy lst
reprezentującą taką macierz, indeksowanie lst[i]
odpowiada wzięciu i
-tego wiersza macierzy. Ponieważ wiersz jest listą, można go indeksować, więc lst[i][j]
odpowiada wzięciu współczynnika macierzy na współrzędnej (i,j)
.
W podobny sposób możemy przedstawić np. planszę do gry w kółko i krzyżyk: pojedyncze pole reprezentowane jest jako pojedynczy napis 'O'
, 'X'
lub '.'
reprezentujący odpowiednio pole zajęte przez kółko, pole zajęte przez krzyżyk, i pole puste. Każdy rząd (wiersz) planszy to trzy pola, plansza to lista trzech rzędów. Wtedy przykładową pozycję po dwóch parach ruchów możemy reprezentować następująco:
[['.', 'O', 'X'], ['.', 'O', '.'], ['.', 'X', '.']]
Uwaga. Podana wcześniej konstrukcja list poprzez mnożenie listy przez liczbę (np. [1, 2] * 3
, której wynikiem jest [1, 2, 1, 2, 1, 2]
) ma w sobie pewną pułapkę. Gdy x
jest dowolnym obiektem, wtedy [x] * 3
tworzy listę długości 3, w której x
występuje trzykrotnie. Należy mieć świadomość, że chodzi o wystąpienie tego samego obiektu (nie jego trzech kopii!) trzy razy. Ma to szczególne znaczenie, gdy obiekt x
jest zmienialny (na przykład jest listą). Rozważmy [[]]
, czyli listę, której jedynym elementem jest lista pusta. Napis [[]] * 3
tworzy listę długości 3, w której trzykrotnie pojawia się ta sama lista pusta:
lst = [[]] * 3
print(lst) # ok, lista z trzema listami pustymi ...
print(lst[0]) # ... lst[0] to pierwszy element listy, czyli "pierwsza" z trzech list pustych) ...
lst[0].append('!') # ... doklejamy do tej listy '!'...
print(lst) # ... dokleiło się do wszystkich list na liście!
# W istocie, w całym programie są teraz dokładnie dwie listy: jedna lista początkowo pusta,
# a teraz zawierająca '!'; oraz lista zawierająca ją potrójnie (na indeksach 0, 1 i 2).
[[], [], []] [] [['!'], ['!'], ['!']]
Powyższa pułapka manifestuje się, gdy używamy list do reprezentowania np. macierzy. Gdybyśmy stworzyli listę reprezentującą macierz $2 \times 2$ w następujący sposób:
matrix = [[0, 0]] * 2
print(matrix)
matrix[0][0] = 1 # zmieniamy lewy górny wyraz na 1
print(matrix) # zmieniliśmy za dużo
[[0, 0], [0, 0]] [[1, 0], [1, 0]]
wtedy w istocie stworzymy macierz złożoną z jednego, zduplikowanego wiersza. Możemy tego uniknąć, stosując listy składane:
matrix = [[0, 0] for _ in range(2)]
print(matrix)
matrix[0][0] = 1 # zmieniamy lewy górny wyraz na 1
print(matrix) # Ok.
[[0, 0], [0, 0]] [[1, 0], [0, 0]]
Powyżej, wyrażenie [0, 0]
konstruujące listę zostało wyliczone dwa razy, za każdym razem tworząc nową listę (wiersz).