W tym epizodzie skupiamy się na nowym typie danych, jakim jest lista. Poznamy różne, podstawowe operacje na listach, nauczymy się czym one są i jak z nich korzystać.

Do dzieła.

Lista w Pythonie

Lista to uszeregowany zbiór, który może zawierać dane dowolnego typu (liczbowe, tekstowe, inne) a nawet różnych typów pomieszanych ze sobą. Listę tworzymy w nawiasach „nieokrągłych” [ ] zaś elementy oddzielamy przecinkami.

Elementy listy podlegają indeksowaniu tak samo jak napisy. W zasadzie napisy to takie listy typu znakowego w programowaniu. Małe przypomnienie z indeksowania napisów:

msg = "Hello World"
print(msg[0]) #H
print(msg[0:5]) #Hello
print(msg[:5]) #Hello
print(msg[5:]) # World
print(msg[6:]) #World
print(msg[1:-1]) #ello Worl
print(msg[::-1]) #dlroW olleH
print(msg[-1]) #d

Czyli liczenie zaczynamy od 0, pierwszy element ma indeks zerowy. Pierwszy argument to start zaś drugi to stop. Jeżeli ustawimy indeks [0:5] to będzie to oznaczać „od indeksu zerowego (włącznie) do indeksu 5, ale bez niego”. Ostatni element ma indeks -1 (przedostatni -2 i tak dalej) i w ten sposób też się do niego można odnieść.

Teraz mały przykład z indeksowaniem listy:

lst = ["a", "b", "c", "d"]
print(lst[0]) #a
print(lst[-1]) #d
print(lst[3]) #d
print(lst[len(lst)-1]) #d
print(lst[0:2]) #['a', 'b']
print(lst[1:]) #['b', 'c', 'd']

Użyłem liter jako elementów listy, żeby nie robić większego zamieszania niż to konieczne. Ale mogą tam być liczby, napisy, inne wartości. Możemy zamienić sobie nasze litery na 1,2,3,4 i zobaczyć, co się stanie.

Split, join, list – wprowadzenie

Przy pomocy funkcji split możemy podzielić napis na elementy listy i stworzyć listę. Musimy tylko podać jak mamy je dzielić. Zrobimy to po spacji i otrzymamy z naszego napisu taką listę:

msg = "Hello World"
words = msg.split(" ")
print(words)
#['Hello', 'World']

Odwrotnie działa funkcja join. Możemy wybrać sobie jakiś znak, jaki ma być „klejem” do łączenia elementów listy (spacja pasuje jak ulał) i połączyć elementy listy w jeden napis:

lst = ["Hello", "World"]
msg = " ".join(lst)
print(msg) #Hello World

Podobnie jak mieliśmy funkcje str() i int() oraz float() zmieniające typ danych mamy też funkcję list(). Możemy ją sobie wypróbować:

print(list("Hello World"))
#['H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd']
print(list("John"))
#['J', 'o', 'h', 'n']
print(list(range(10)))
#[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(range(1,11)))
#[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Jak widać, czasami może się przydać.

Podstawowe metody listy – append, insert, pop

Aby do listy coś dodać na samym końcu, używamy append:

lst = [1,2]
lst.append(3)
print(lst)
#[1, 2, 3]

Aby coś dodać na początku bądź w miejscu innym niż koniec, używamy insert. Podajemy indeks, który nas interesuje i element:

lst = [1,2]
lst.append(3)
print(lst)
#[1, 2, 3]
lst.insert(0,0)
print(lst)
#[0, 1, 2, 3]
lst.insert(0, -1)
print(lst)
#[-1, 0, 1, 2, 3]

Tutaj najpierw na indeksie zerowym (czyli w pierwszym elemencie) dodaliśmy zero, później znowu na indeksie zerowym dodaliśmy element -1.

Ogólnie dodawanie na końcu listy (na tym etapie nie powinno nam to robić żadnej różnicy) poprzez append jest dużo wydajniejsze niż dodawanie gdziekolwiek przy użyciu insert. Dodając bowiem element na końcu wykonujemy tylko jedną operację. Dodając element na początku musimy przeindeksować wszystkie elementy, przesunąć całość. Tak to mniej więcej działa „pod spodem”.

Z usuwaniem jest podobnie. Usunięcie ostatniego elementu to tylko usunięcie ostatniego elementu. Wszystkie inne elementy mają taki sam indeks. Natomiast usunięcie pierwszego elementu to dodatkowo przesunięcie reszty o indeks do tyłu. Nie musimy sobie specjalnie tym głowy zawracać, aczkolwiek warto wiedzieć, że operacje na końcu listy są wydajniejsze od tych na początku.

Aby usunąć element z końca listy używamy funkcji pop:

lst = [1,2,3]
print(lst)
#[1, 2, 3]
lst.pop()
print(lst)
#[1, 2]

Pop nie tylko usuwa ostatni element, ale także go zwraca. Możemy zrobić coś takiego:

lst = [1,2,3]
print(lst)
#[1, 2, 3]
print(lst.pop())
#3
print(lst)
#[1, 2]

Tutaj element 3 został usunięty a jednocześnie zwrócony do funkcji print. Możemy także go „złapać” do zmiennej:

lst = [1,2,3]
print(lst)
#[1, 2, 3]
last_one = lst.pop()
print(lst)
#[1, 2]
print(last_one)
#3

Do pop możemy też przekazać indeks i w ten sposób usuwać elementy np. na początku listy (mniej wydajne, ale jak najbardziej dozwolone):

lst = [1,2,3]
print(lst.pop(0))
#1
print(lst)
#[2, 3]

Pętla while, truthy values – lista

Myślę, że listy to dobry moment na wprowadzenie pętli while. Jest to druga z pętli w Pythonie (więcej nie ma w tym języku) i oznacza „rób coś tak długo jak warunek jest prawdziwy”.

Weźmy sobie taki przykład: chcemy pętlę, która będzie usuwać po elemencie z końca listy tak długo, aż lista się skończy i printować te elementy. Wiemy, że mamy funkcję len(), która nam podaje długość listy oraz funkcję pop, która usuwa element z końca listy, jednocześnie go zwracając. Zróbmy coś takiego:

lst = [1,2,3,4,5]
while len(lst) > 0:
    print(lst.pop())
print(lst) #[]

Najpierw nasza lista, potem bloczek kodu z while i warunkiem, że długość listy ma być większa od 0. We wcięciu instrukcja, aby usuwać ostatni element z listy i go drukować. W pewnym momencie otrzymamy pustą listę i warunek dotyczący długości większej niż 0 przestanie być prawdziwy. Wtedy, poza wcięciem, jeszcze drukujemy całą listę i widzimy, że jest opustoszała.

A jak zrobić, aby uzyskać to samo ale zaczynając od elementu pierwszego? Usuwać element pierwszy tak długo aż lista się skończy? To już powinniśmy wiedzieć, pop ale z argumentem indeks o wartości 0 (indeks pierwszego elementu):

lst = [1,2,3,4,5]
while len(lst) > 0:
    print(lst.pop(0))
print(lst) #[]

Tutaj mamy 1,2,3,4,5 wypisane w pętli aż do wyczerpania listy. Wtedy pętla się kończy.

Dodam, że Python jest „domyślny”. I ten zapis możemy uprościć, bowiem każda lista, której długość to minimum jeden element zwraca wartość prawdziwą zaś puste listy, puste napisy i tak dalej (liczba o wartości 0 na przykład) zwracają fałsz. Zatem ten zapis możemy uprościć:

lst = [1,2,3,4,5]
while lst:
    print(lst.pop(0))
print(lst) #[]

„while lst” znaczy „tak długo, jak lst będzie prawdziwa”. A co to znaczy prawdziwa? W przypadku liczb inna niż 0, w przypadku napisów niebędąca pustym napisem, w przypadku list – niebędąca pustą listą. Aczkolwiek nasz poprzedni zapis (while len(lst) > 0 czyli „tak długo jak długość listy jest większa od 0”) jest bardziej czytelny. Niektóre rzeczy, jeżeli o tym wiemy, można sobie w programowaniu uprościć, natomiast generalnie zawsze lepiej trzymać się bardziej czytelnych zapisów i notacji.

Jeszcze jeden przykład, zupełnie z listami niezwiązany, abyśmy to zrozumieli:

number = 5
while number > 0:
    print(number)
    number -= 1

Tutaj mamy zmienną number równą pięć i pętlę, która działa tak długo, jak number jest większy od 0. Drukujemy zmienną i zmniejszamy ją o 1. I tak mamy 5,4,3,2,1. Przy wydrukowaniu 1 i zmniejszeniu go do 0 warunek przestaje być prawdziwy, number wynosi 0, koniec pętli.

Natomiast wiedząc, że każda liczba wynosząca 0 zwraca fałsz, zaś liczba wynosząca cokolwiek innego niż zero zwraca prawdę ten akurat przypadek możemy sobie uprościć:

number = 5
while number:
    print(number)
    number -= 1

I tak na dobrą sprawę jest to zupełnie inna pętla. „while number” sprawdza, czy number jest prawdziwy. Jako że number to liczba, prawdziwość zależy od niebycia zerem. Ten warunek to nie sprawdzenie „czy number jest większy od zera” ale uproszczony zapis „czy number nie jest zerem”. Może dla kogoś wydawać się to skomplikowane (nie musi używać takiej notacji) ale jest to swego rodzaju trik, sztuczka pozwalająca niektóre warunki spełnić.

Zagadnienie „prawdziwości” różnych typów danych jest dość podstępne, ale po przyswojeniu go sobie już nas nie zaskoczy, więc pozwolę sobie jeszcze na kilka przykładów. Na początku „klasyczny” warunek:

number = 0
if number != 0:
    print("Prawda")
else:
    print("Fałsz")
#Fałsz

Sprawdzamy, czy liczba nie jest zerem. Jako że jest, wywołuje się else i dostajemy fałsz. Możemy też sprawdzić coś innego, np. czy number jest większy od 0:

number = 0
if number > 0:
    print("Prawda")
else:
    print("Fałsz")
#Fałsz

Ale możemy też zrobić „if number” które jest „testem na prawdziwość number”:

number = 0
if number:
    print("Prawda")
else:
    print("Fałsz")
#Fałsz

Prawdziwość w przypadku liczb oznacza nie bycie zerem.

number = 1
if number:
    print("Prawda")
else:
    print("Fałsz")
#Prawda

I tak samo możemy sprawdzić „prawdziwość” listy. Prawdziwość listy to jest jej nie bycie listą pustą:

lst = []
if lst:
    print("Prawda")
else:
    print("Fałsz")
#Fałsz

Pusta lista daje fałsz blokowi „if lst” a zatem wykonuje się blok else. Wystarczy jednak dać listę niepustą, aby wynik był odwrotny:

lst = [1]
if lst:
    print("Prawda")
else:
    print("Fałsz")
#Prawda

Podobnie możemy bawić się z napisami:

msg = ""
if msg:
    print("Prawda")
else:
    print("Fałsz")
#Fałsz

Pusty napis, bez ani jednego znaku – fałsz. Wystarczy tam coś dodać, spację choćby, a będzie prawda:

msg = " "
if msg:
    print("Prawda")
else:
    print("Fałsz")
#Prawda

Pętla for – przechodzenie po elementach listy

Powinniśmy pamiętać, że po elementach napisu, jego literach, możemy sobie przejść w pętli for:

msg = "Hello"
for letter in msg:
    print(letter)

Tak samo możemy przejść po elementach listy:

lst = [1,2,3,4,5]
for el in lst:
    print(el)

W pętli for drukujemy, element po elemencie, 1,2,3,4,5. Możemy też przejść po liście od tyłu:

lst = [1,2,3,4,5]
for el in reversed(lst):
    print(el)

Swoją drogą, to samo możemy zastosować wobec napisu i dostać litera po literze od tyłu:

msg = "hello"
for letter in reversed(msg):
    print(letter)

Możemy też wziąć listę całkowicie nieposortowaną i najpierw ją posortować a później iterować od 1 do 5 na przykład:

lst = [5,1,2,4,3]
for el in sorted(lst):
    print(el)

Warto zwrócić uwagę, że sorted zwraca nową, posortowaną listę. Oryginalna lista nie jest modyfikowana a ta stworzona przez sorted istnieje w pamięci tylko na czas użycia jej w pętli for. Możemy to sobie sprawdzić:

lst = [5,1,2,4,3]
for el in sorted(lst):
    print(el)
print(lst)
#[5, 1, 2, 4, 3]

Jeżeli chcemy posortować listę lst „in-place”, na stałe, modyfikując ją a nie tylko zwracając jej posortowaną kopię na potrzeby pętli for, musimy użyć metody sort:

lst = [5,1,2,4,3]
lst.sort()
print(lst)
#[1, 2, 3, 4, 5]
for el in lst:
    print(el)

I tak najpierw tę listę sortujemy a później przechodzimy element to elemencie. Warto dodać, że reversed również nie zmienia nam niczego, tylko zwraca kopię odwróconego obiektu.

Jeżeli chcemy, możemy też iterować po zarówno indeksie jak i elemencie używając enumerate:

lst = [5,1,2,4,3]

for idx, el in enumerate(lst):
    print(f"Indeks {idx} posiada element o wartości {el}")

Dodawanie elementów w pętli

Jeżeli chcemy mieć listę liczb od 1 do 10 możemy to osiągnąć w bardzo prosty sposób:

lst = list(range(1,11))
print(lst)
#[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Ale możemy też dodawać te elementy jeden po drugim w pętli for:

lst = []
for num in range(1,11):
    lst.append(num)
print(lst)
#[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Zaletą takiego rozwiązania jest możliwość postawienia jakiegoś warunku. Dodawajmy zatem liczby, ale tylko parzyste:

lst = []
for num in range(1,11):
    if num % 2 == 0:
        lst.append(num)
print(lst)
#[2, 4, 6, 8, 10]

Zamiast warunku, możemy wykonać jakąś operację. Pododawajmy liczby od 1 do 10, ale przemnożone przez 2:

lst = []
for num in range(1,11):
    lst.append(num * 2)
print(lst)
#[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

Operacje możemy też wykonywać na znakach. Zróbmy listę liter w napisie, ale każda litera zapisana wielką:

name = "anna"
lst = []
for letter in name:
    lst.append(letter.upper())
print(lst)
#['A', 'N', 'N', 'A']

Istnieje też coś takiego, jak list comprehension. Jest to rozwiązanie typowe dla Pythona o dość specyficznej składni, nie musimy go stosować, ale warto je znać. Myślę, że kilka przykładów jest w stanie to zobrazować lepiej niż długie definicje. Przykład 1 – liczby od 1 do 10:

lst = [num for num in range(1,11)]
print(lst)
#[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Przykład 2 – liczby od 1 do 10 mnożone przez 2:

lst = [num * 2 for num in range(1,11)]
print(lst)
#[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

Przykład 3 – liczby z zakresu 1 do 10 ale tylko parzyste:

lst = [num for num in range(1,11) if num % 2 == 0]
print(lst)
#[2, 4, 6, 8, 10]

Przykład 4 – wszystkie litery imienia anna zapisane wielką literą:

name = "anna"
lst = [letter.upper() for letter in name ]
print(lst)
#['A', 'N', 'N', 'A']

Takie zabawy nazywają się list comprehension i albo to „czujemy” albo nie, albo będziemy z tego chętnie korzystać, albo wprost przeciwnie. Warto jednak wiedzieć, że coś takiego istnieje.

Clear, remove, count – kolejne metody

Metoda clear czyści całą listę. Dostajemy pustą, wszystkie elementy zostają usunięte:

lst = [1,2,3]
lst.clear()
print(lst) #[]

Metoda remove usuwa określony element:

lst = [1,2,3]
lst.remove(1)
print(lst)
#[2, 3]

Uwaga – remove usuwa pierwszy taki element. Jeżeli występuje on częściej, ten drugi, trzeci, n-ty nie zostanie usunięty za pierwszym razem:

lst = [1,2,3,1]
lst.remove(1)
print(lst)
#[2, 3, 1]

Conut zaś podaje nam ile razy dany element występuje:

lst = [1,2,3,1]
print(lst.count(1)) #2
print(lst.count(2)) #1
print(lst.count(10)) #0

Zadanie z listą – usuń określone elementy

Mamy taką oto listę. Chcemy albo zrobić drugą, która będzie pozbawiona elementu 1, występującego tutaj nazbyt często, albo usunąć ten element z tej listy.

lst = [1,2,1,3,1,4,1,5,1,6]

Podejście pierwsze – pętla for:

lst = [1,2,1,3,1,4,1,5,1,6]
lst2 = []
for num in lst:
    if num != 1:
        lst2.append(num)
print(lst2)
#[2, 3, 4, 5, 6]

Tworzymy drugą listę, iterujemy po pierwszej i jeżeli nasz num nie równa się 1 to dodajemy go do drugiej listy. Drukujemy i mamy wynik.

Podejście drugie – list comprehension:

lst = [1,2,1,3,1,4,1,5,1,6]
lst2 = [num for num in lst if num != 1]
print(lst2)
#[2, 3, 4, 5, 6]

W zasadzie to samo ale w innej notacji. Daj mi liczbę dla każdej liczby z listy pierwszej pod warunkiem, że ta liczba nie jest równa 1.

Podejście trzecie – modyfikacja in-place czyli modyfikacja oryginalnej listy.

lst = [1,2,1,3,1,4,1,5,1,6]
while lst.count(1) > 0:
    lst.remove(1)
print(lst)
#[2, 3, 4, 5, 6]

Zastosowaliśmy pętlę while z warunkiem „tak długo jak ilość jedynek w liście jest większa niż 0”. Jako ciało pętli użyliśmy instrukcji „usuń jedynkę”. Wykonywał to polecenie tak długo, aż mu wyszło, że ilość jedynek w liście jest równa zero i przestał.

I oczywiście możemy to uprościć. Zero to zawsze fałsz, zaś nie-zero prawda, więc zadziała na również taki bloczek:

lst = [1,2,1,3,1,4,1,5,1,6]
while lst.count(1):
    lst.remove(1)
print(lst)
#[2, 3, 4, 5, 6]

Podejście czwarte, czyli coś, czego nie mamy prawa znać, chyba że wcześniej już programowaliśmy (jeżeli nie, zignorujmy to) – użycie filter:

lst = [1,2,1,3,1,4,1,5,1,6]
lst = list(filter(lambda x: x!= 1, lst))
print(lst)
#[2, 3, 4, 5, 6]

Jest to skomplikowane, nieintuicyjne i w tym wypadku spory przerost nad treścią. Natomiast tak, używamy funkcji filter z lambdą, która przyjmuje coś (x) i zwraca prawdę jak ten x nie jest równy 1. Używamy to na liście lst i ten filter jeszcze musimy na listę zamienić funkcją list() i przypisać do naszej zmiennej.

Takich sztuczek jeszcze nie znamy, ba, własnych funkcji (normalnych, nie lambda) jeszcze nie napisaliśmy zaś w tym wypadku jest to spory przerost formy nad treścią i jeżeli w ogóle jako tako ten fragment kodu rozumiemy to jest naprawdę dobrze, nawet lepiej niż dobrze. Niech to będzie narobieniem smaku na przyszłość zaś dla wcześniej programujących w jakimś języku – pokazaniem, że filter w Pythonie też istnieje, ale nie jest specjalnie intuicyjny.

Własne funkcje oraz inne bardziej zaawansowane zabawy poznamy w następnych artykułach o podstawach Pythona.