Pochylimy się teraz nad typem danych, jakim jest lista aby poznać bądź przećwiczyć kilka ważnych operacji z listą związanych. Będzie to m. in. konwersja z napisu do listy, składanie list (list comprehension), funkcje map, filter, reduce, any i all.
Konwersja napis-lista – list, split
Jeżeli chcemy otrzymać listę, której każdy element to litera naszego napisu, nic prostszego niż użyć funkcję list() na dowolnym napisie:
msg = "Hello"
lst = list(msg)
print(lst)
#['H', 'e', 'l', 'l', 'o']
Oczywiście, czysto dla treningu, to samo możemy osiągnąć przechodząc po liście w pętli i dodając „ręcznie” każdy element:
msg = "Hello"
lst = []
for letter in msg:
lst.append(letter)
print(lst)
#['H', 'e', 'l', 'l', 'o']
Również tutaj możemy skorzystać ze składania list, czyli list comprehension:
msg = "Hello"
lst = [letter for letter in msg]
print(lst)
#['H', 'e', 'l', 'l', 'o']
Warto też zaznaczyć, że funkcja list może przyjąć obiekt range:
lst = list(range(10))
print(lst)
#[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Tutaj tworzymy listę liczb od 0 do 9 włącznie. Można też stworzyć od 1 do 10:
lst = list(range(1,11))
print(lst)
#[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Range 1,11 oznacza od 1 włącznie do 11 bez 11 (czyli de facto od 1 do 10).
Wiemy, jak z napisu zrobić listę, której elementem będzie każda litera. Co jednak, gdy chcemy listy, której elementami ma być np. każde słowo?
W takim wypadku należy użyć funkcji split, która „pokroi” nam napis do postaci listy. Musi tylko wiedzieć, co ma pokroić. Uznajmy, że kroimy po spacjach, bo tym będą słowa oddzielone:
msg = "Hello world"
lst = msg.split(" ")
print(lst)
# ['Hello', 'world']
„Kroi” po spacji, każde słowo staje się elementem listy.
Konwersja lista-napis – for, join
Z listy do napisu też przekonwertować w Pythonie dane można. Najbardziej oczywistym na początek sposobem może być użycie pętli for:
lst = ["a", "b", "c"]
msg = ""
for el in lst:
msg += el
print(msg)
#abc
Tworzymy pusty napis, iterujemy po elementach listy i dodajemy do napisu element po elemencie. Niestety takie rozwiązanie ma swoje wady. Proszę zobaczyć:
lst = ["hello", "world"]
msg = ""
for el in lst:
msg += el
print(msg)
#helloworld
Jeden napis. W logikę bawić się możemy:
lst = ["hello", "world"]
msg = ""
for el in lst:
msg += el
msg += " "
print(msg)
#hello world
Niby dobrze, ale w tym wypadku ostatni element, czego może nie widać, ma po sobie niepotrzebną spację. Możemy dalej się bawić w logikę:
lst = ["hello", "world"]
msg = ""
for el in lst:
msg += el
msg += " "
else:
msg = msg[0:-1]
print(msg)
#hello world
print(len(msg) == len("hello world"))
#True
Tutaj po wykonaniu pętli (blok else) bierzemy msg i wycinamy z niego wszystko, poza ostatnim znakiem. To oznacza zapis [0:-1] – od indeksu zerowego (włącznie) do indeksu ostatniego (-1), ale już bez niego. Można to skrócić do [:-1] btw.
Mimo wszystko, do takich zabaw jak sklejanie wyrazów znajdujących się w liście, służy funkcja join, wywoływana na fragmencie tekstu, który jest naszym „klejem”. Przykład:
lst = ["hello", "world"]
msg = " ".join(lst)
print(msg)
#hello world
„Klejem” jest spacja, sklejamy listę lst, otrzymujemy tekst.
List comprehension – składanie listy
Spróbujmy przekopiować z listy 1 do listy 2 wszystkie liczby, ale podnieść je do potęgi:
lst = list(range(1,6))
lst2 = [num ** 2 for num in lst]
print(lst)
print(lst2)
# [1, 2, 3, 4, 5]
# [1, 4, 9, 16, 25]
Za pomocą list i range tworzymy listę liczb od 1 do 6 (bez 6), następnie używając list comprehension podnosimy do kwadratu każdą liczbę z listy pierwszej. Wynik widać, działa.
To teraz może tak: liczby od 1 do 10 i podnosimy do potęgi tylko te, które są parzyste:
lst = list(range(1,11))
lst2 = [num ** 2 for num in lst if num % 2 == 0]
print(lst2)
#[4, 16, 36, 64, 100]
Nie będę tłumaczyć po raz n-ty testu na parzystość, jeżeli przykłady liczbowe nam nie wchodzą, zróbmy sobie tekstowy. Mamy z listy imion przekopiować każde imię, ale uwaga, zapisane wielką literą:
lst = ["Bob", "John", "Jim", "Jane"]
lst2 = [name.upper() for name in lst]
print(lst)
print(lst2)
# ['Bob', 'John', 'Jim', 'Jane']
# ['BOB', 'JOHN', 'JIM', 'JANE']
A teraz kopiujemy i zapisujemy wielką literą, ale tylko te, które zaczynają się na literę J:
lst = ["Bob", "John", "Jim", "Jane"]
lst2 = [name.upper() for name in lst if name[0] == "J"]
print(lst)
print(lst2)
# ['Bob', 'John', 'Jim', 'Jane']
# ['JOHN', 'JIM', 'JANE']
Tak działa składanie list. Warto zapamiętać, ponieważ pozwala ono uniknąć innych zabaw z listami, jak map czy filter, które jednak znać wypada, dlatego do nich przechodzimy.
Map – wywołaj funkcję na każdym elemencie
Map pozwala nam zrobić to samo, co składanie listy, czyli zwrócić zmodyfikowaną listę, w której na każdym elemencie wykonaliśmy jakąś operację. Nasz przykład z podnoszeniem do potęgi:
lst = list(range(1,6))
lst2 = [num ** 2 for num in lst]
print(lst)
print(lst2)
# [1, 2, 3, 4, 5]
# [1, 4, 9, 16, 25]
Możemy do tego użyć map. Istotą map jest zwrócenie listy, na której każdy z elementów listy oryginalnej jest poddany działaniu jakiejś funkcji. A zatem nasza funkcja będzie musiała brać element i podnosić go do kwadratu, operując na liście lst. Użyjemy takiego zapisu:
lst = list(range(1,6))
lst2 = list(map(lambda x : x ** 2, lst))
print(lst)
print(lst2)
# [1, 2, 3, 4, 5]
# [1, 4, 9, 16, 25]
Obiekt map zawsze musimy konwertować do listy. Funkcja lambda przyjmuje element i zwraca jego wartość podniesioną do kwadratu operując na elementach listy lst. Wolę list comprehension, jest czytelniejsze.
A czy map poradzi sobie z czymś takim jak kopiowanie elementów z listy do listy bez ich zmieniania? List comprehension tutaj daje radę:
lst = list(range(1,6))
lst2 = [num for num in lst]
print(lst)
print(lst2)
# [1, 2, 3, 4, 5]
# [1, 2, 3, 4, 5]
Nie wiem dlaczego ktoś miałby kopiować w ten sposób, zwłaszcza że z listy do listy kopiujemy tak:
lst = list(range(1,6))
lst2 = lst[:]
print(lst)
print(lst2)
# [1, 2, 3, 4, 5]
# [1, 2, 3, 4, 5]
Tym bardziej dużo dziwniejsze byłoby używanie map do prostego kopiowania, ale zastanówmy się: czym jest map a czym kopiowanie?
Istota map – zwróć listę, w której każdy element poddany jest działaniu jakiejś funkcji. Istota kopiowania – weź element i zwróć ten element bez modyfikacji.
Zbierając to do kupy mamy:
lst = list(range(1,6))
lst2 = list(map(lambda x: x, lst))
print(lst)
print(lst2)
# [1, 2, 3, 4, 5]
# [1, 2, 3, 4, 5]
Lambda bierze element i zwraca go, bez modyfikacji. Map wywołuje lambdę na każdym elemencie listy lst i zwraca taki obiekt, który możemy do listy konwertować.
To może jeszcze warto dodać, że map potrafi też korzystać z funkcji wbudowanych, nie tylko lambd.
lst = ["john", "jim", "jane"]
lst2 = list(map(len, lst))
print(lst, lst2)
# ['john', 'jim', 'jane'] [4, 3, 4]
Tutaj wywołujemy funkcję len na każdym elemencie i otrzymujemy listę długości danych imion. To samo może zrobić list comprehension:
names = ["john", "jim", "jane"]
lengths = [len(name) for name in names]
print(lengths)
# [4, 3, 4]
Filter – przechodzą tylko te, które spełniają warunek
Filter działa podobnie do map, ale zwraca nam listę przefiltrowaną. Każdy element poddawany jest działaniu funkcji i jeżeli ta funkcja zwraca prawdę – element przechodzi. Jeżeli nie – nie przechodzi.
Składnia podobna jak w map i podobnie jak w map, nie musimy tego używać, jak list comprehension znamy:
numbers = [1,-1,2,3,-1,4,5]
only_positive = list(filter(lambda x : x > 0, numbers))
print(only_positive)
# [1, 2, 3, 4, 5]
print([number for number in numbers if number > 0])
# [1, 2, 3, 4, 5]
Wersja z filter, którą trzeba do listy konwertować i podać jej funkcję, zaś funkcją jest „weź element i sprawdź czy jest większy od zera” oraz wersja z list comprehension, moim zdaniem łatwiejsza.
Przykład z imionami, które mają się na „J” zaczynać, wersja z filter i list comprehension:
names = ["Bob", "John", "Jane"]
names_j = list(filter(lambda x: x[0] == "J", names))
print(names_j)
#['John', 'Jane']
print([name for name in names if name[0] == "J"])
#['John', 'Jane']
Od nas zależy, co wolimy. List comprehension jest typowe dla Pythona, za to czytelniejsze moim zdaniem. Konstrukcje typu filter istnieją w innych językach programowania, stąd ich popularność wśród ludzi, którzy na Pythona się „przełączyli” z innego języka
Reduce – redukcja listy do jednej wartości
Reduce znajduje się w module functools. Jest to kolejna zabawka obok map i filter do obrabiania list. O ile jednak map i filter zwracają listę (map ze zmodyfikowanymi elementami, filter z odfiltrowanymi elementami) o tyle reduce przyjmuje pewną funkcję i ma za zadanie zredukować całą listę do jednej wartości.
Taką redukcją listy do jednej wartości może być na przykład dodanie jej elementów do siebie:
from functools import reduce
nums = list(range(1,11))
nums_sum = reduce(lambda x, y: x + y, nums)
print(nums_sum)
#55
Musimy zaimportować. Do listy konwertować nie musimy, ponieważ reduce zwraca jedną wartość, nie listę. Bierze funkcję, która ma dwa argumenty: akumulator i obecny element.
I działa to tak, że idąc po liście x jest akumulatorem (na początku 0) zaś y elementem. Do tego x dodawany jest y (taka nasza logika) i tak aż do końca listy i ten y jest nam zwracany przez reduce.
Może i skomplikowane, zresztą reduce nie jest w Pythonie tak często używane, skoro przeniesiono je do innego modułu. Ale zrozumieć jak to działa warto.
Oto inny przykład – mamy listę imion. Postarajmy się ją zredukować do sumy ich długości. Czyli po ludzku – dodajemy do siebie ilość liter każdego imienia, używając reduce. Dodam, że pracując na napisach już musimy, jako trzeci argument podać wartość początkową 0, inaczej Python się pogubi:
from functools import reduce
names = ["Bob", "John", "Jim"]
names_sum = reduce(lambda x, y: x + len(y), names, 0)
print(names_sum)
#10
A zatem tak: mamy listę imion. Mamy Reduce, które przyjmuje funkcję. Funkcja przyjmuje akumulator, ustawiony początkowo na 0 oraz wartość y, czyli każdy element listy. Każdy element listy jest zamieniany na jego długość i dodawany do akumulatora. Pracujemy na names.
Otrzymujemy 0 + 3 + 4 + 3 i wynik tego działania to wartość, do której zredukowaliśmy naszą listę.
Może i trudne, ale znać warto.
All, any – redukcja listy do typu prawda/fałsz
All i any służą w zasadzie temu samemu, co reduce – redukcji listy do jednego, prostego typu wartości. W odróżnieniu od reduce nie korzystamy z żadnego akumulatora, który np. dodaje coś do siebie tylko redukujemy do typu bool, typu prawda/fałsz.
Aby takiej redukcji dokonać możemy przyjąć strategię, że każdy element poddajemy jakiemuś testowi, i jeżeli wszystkie elementy go spełnią, lista jest prawdziwa. Do tego służy all:
names = ["John", "Jim", "Jane"]
all_start_with_J = all([name[0] == "J" for name in names])
print(all_start_with_J)
#True
Tutaj mamy all oraz swego rodzaju warunek, że każdy element w names ma się zaczynać od litery J. Jako że każdy spełnia ten warunek, dostajemy prawdę.
Nie wchodząc w szczegóły dodam, że te nawiasy [] możemy pominąć:
names = ["John", "Jim", "Jane"]
all_start_with_J = all(name[0] == "J" for name in names)
print(all_start_with_J)
#True
Podobnie możemy sprawdzić, czy wszystkie elementy danego zbioru są większe od 0 albo 10:
nums = [1,2,3,10]
print(all(num > 0 for num in nums))
#True
print(all(num > 10 for num in nums))
#False
Prawdą jest, że wszystkie są większe niż 0, fałszem jest, że wszystkie są większe niż 10.
Any to kolejny sposób na redukcję listy do typu prawda/fałsz. Tutaj jednak poddajemy pewnemu testowi wszystkie elementy danego zbioru i wystarczy że jeden z nich spełnia warunek – mamy prawdę:
nums = [-1, 1,2,3,10]
print(any(num > 0 for num in nums))
#True
print(any(num >= 10 for num in nums))
#True
print(any(num == 100 for num in nums))
#False
Prawdą jest, że przynajmniej jeden jest większy od 0, prawdą jest, że przynajmniej jeden jest większy lub równy 10, nieprawdą jest, aby znajdował się tam choć jeden równy 100 – stąd nasze wyniki.
Poznaliśmy naprawdę dużo. To chyba dobry moment, aby zakończyć ten wpis.