Struktury danych w Pythonie: lista

przez | 4 grudnia 2019

Dzisiaj na warsztat weźmiemy podstawową strukturę danych w Pythonie jaką jest lista. Nawet jeśli znasz już trochę Pythona to zachęcam Cię do pozostania do końca i sprawdzenia, czy na pewno wiesz o listach wszystko 😎

A jeśli chcesz dowiedzieć się więcej o samym Pythonie i o tym jak powstał, to serdecznie zachęcam Cię do obejrzenia filmu, który kiedyś o tym nagrałem: https://pythonowiec.pl/2019/11/jak-powstal-python/

Charakterystyka list w Pythonie

Lista (list) w Pythonie cechuje się trzema podstawowymi charakterystykami:

  • jest tzw. container sequence, czyli może przechowywać elementy różnego typu, np. liczbę całkowitą, zmiennoprzecinkową, ciąg znaków i bajty w jednej liście (przykład poniżej). Jednak w praktyce najczęściej i tak w liście trzyma się raczej tylko elementy jednego typu.
items = ["bmw", 1, 2.0, b'0xAF']
  • jest strukturą mutowalną, czyli możemy do niej elementy dodawać, odejmować, zmieniać ich wartość lub kolejność itp. bez tworzenia nowego obiektu,
  • gwarantuje nam zachowanie kolejności elementów, w jakiej zostały one dodane do listy (chyba że sami jawnie zmienimy ich kolejność, np. w wyniku sortowania czy odwrócenia listy).

Lista nie przechowuje wartości swoich elementów fizycznie w obrębie swojej przestrzeni w pamięci (w przeciwieństwie, na przykład, do sekwencji bytes czy array), a jedynie posiada referencję do każdego z obiektów, będących jej elementami.

Inicjalizacja

Listę tworzymy używając tzw. literału listy, czyli po prostu nawiasów kwadratowych: [ ], w środku podając kolejne elementy po przecinku:

cars = ["bmw", "audi", "mercedes", "toyota", "nissan"]

Możemy też stworzyć listę na podstawie innej struktury – wystarczy, by była zgodna z Iterable, czyli by implementowała metodę __iter__().
Może to być na przykład tuple (krotka) czy set (zbiór) – o których powiem więcej w następnych wpisach – ale także string. W przypadku podania ciągu znaków wynikowa lista będzie składała się z pojedyńczych liter tego ciągu:

>>> list("bmw")
['b', 'm', 'w']

Podstawowe operacje na listach

Aby upewnić się, że mamy do czynienia z listą, możemy skorzystać z wbudowanej funkcji type:

>>> cars = ["bmw", "audi", "mercedes", "toyota", "nissan"]
>>> type(cars)
<class 'list'>

Do sprawdzenia długości listy, jak w przypadku każdej sekwencji w Pythonie, służy funkcja len:

>>> len(cars)
5

Dostęp do elementów

Do elementów listy odwołujemy się za pomocą ich indeksów. W Pythonie, tak jak w większości prawilnych języków programowania, elementy indeksowane są od zera. Czyli jeśli chcę otrzymać pierwszy element z listy, to wykonam:

>>> cars[0]
'bmw'

Jeśli spróbuję odwołać się do indeksu, który nie istnieje, to otrzymam błąd:

>>> cars[10]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

Jako że lista jest strukturą mutowalną, to możemy zmienić wartość listy pod danym indeksem:

>>> cars[0] = "hyundai"
>>> cars
['hyundai', 'audi', 'mercedes', 'toyota', 'nissan']

To, o czym mówiliśmy do tej pory, jest dosyć typowe dla wielu języków programowania. Ale Python oferuje dużo więcej bardzo zgrabnych funkcjonalności dla list, które nie są dostępne np. w Javie.

Po pierwsze możemy dostawać się do elementów listy od jej końca! W tym celu należyć użyć ujemnych indeksów. Czyli aby otrzymać ostatni element z listy, wystarczy napisać:

>>> cars
['bmw', 'audi', 'mercedes', 'toyota', 'nissan']
>>> cars[-1]
'nissan'

Zauważ, że wcześniej, aby otrzymać pierwszy element z listy, użyliśmy indeksu 0, natomiast teraz nie użyliśmy -0, ale -1. Trzeba pamiętać, że patrząc od tyłu, elementy są „indeksowane od -1”.

A co stałoby się po użyciu -0?

>>> cars[-0]
'bmw'

Otrzymamy po prostu pierwszy element listy. Czyli wywołania cars[0] i cars[-0] są całkowicie równoważne.

Dzielenie włosa na czworo, czyli slicing listy w Pythonie

Kolejną superopcją w Pythonie jest slicing, czyli możliwość łatwego wydzielania fragmentów z sekwencyjnych struktur danych takich jak lista.

Jeśli chcemy na przykład ograniczyć naszą listę do dwóch pierwszych elementów, to napiszemy:

>>> cars
['bmw', 'audi', 'mercedes', 'toyota', 'nissan']
>>> cars[:2]
['bmw', 'audi']

A jeśli chcemy wybrać co drugi element z listy?

>>> cars[::2]
['bmw', 'mercedes', 'nissan']

Co tu się właściwie dzieje?
Pełna formuła slicingu wygląda start:stop:krok. Start to indeks elementu, od którego zaczynamy slicing a stop to indeks, przed którym zakończymy slicing. Natomiast krok określa co który element będziemy brać pod uwagę.

Jak widać w powyższych przykładach możemy pomijać różne z tych opcji. Na przykład nie podając start zaczniemy domyślnie od pierwszego elementu, a nie podając stop skończymy na ostatnim. A domyślny krok wynosi 1, czyli bierze pod uwagę każdy element.

Poniżej jeszcze kilka przykładów:

>>> cars
['bmw', 'audi', 'mercedes', 'toyota', 'nissan']
>>> cars[1:]
['audi', 'mercedes', 'toyota', 'nissan']
>>> cars[1:3]
['audi', 'mercedes']
>>> cars[:3]
['bmw', 'audi', 'mercedes']
>>> cars[1::2]
['audi', 'toyota']

Bardzo ciekawym trikiem jest wykorzystanie kroku do odwócenia listy. Jak to zrobić? Wystarczy podać jako krok wartość -1:

>>> cars[::-1]
['nissan', 'toyota', 'mercedes', 'audi', 'bmw']

Taki to ten nasz Python fajny 😄

Warto zauważyć, że przy slicingu nie modyfikujemy originalnej listy. Pozostaje ona bez zmian, a jako wynik operacji otrzymujemy nową listę:

>>> reversed_cars = cars[::-1]
>>> reversed_cars
['nissan', 'toyota', 'mercedes', 'audi', 'bmw']
>>> cars
['bmw', 'audi', 'mercedes', 'toyota', 'nissan']

Dodawanie elementów do listy

Do dodawania nowego elementu na końcu listy służy metoda append():

>>> cars.append("ferrari")
>>> cars
['bmw', 'audi', 'mercedes', 'ferrari']

Jeśli chcemy natomiast dodać wiele elementów na raz, to możemy skorzystać z metody extend() – jako argument podajemy dowolny iterable, np. listę lub tuplę:

>>> cars
['bmw', 'audi', 'mercedes']

>>> cars.extend(["honda", "rover"])
>>> cars
['bmw', 'audi', 'mercedes', 'honda', 'rover']

To samo możemy osiągnąć również dodając do siebie dwie listy przy pomocy „plusa” (+). Przy czym w tym wypadku oryginalna lista nie jest modyfikowana – otrzymujemy nową listę:

>>> more_cars = cars + ["honda", "rover"]
>>> more_cars
['bmw', 'audi', 'mercedes', 'honda', 'rover']
>>> cars
['bmw', 'audi', 'mercedes']

A co jeśli chcemy dodać element w dowolnym miejscu, a nie tylko na końcu? Do tego służy metoda insert(index, elem). Argument index określa, przed jakim elementem wstawimy nowy:

>>> cars.insert(0, "honda") # na początku listy
>>> cars
['honda', 'bmw', 'audi', 'mercedes']

>>> cars.insert(3, "rover") # przed elementem o indeksie 3
>>> cars
['honda', 'bmw', 'audi', 'rover', 'mercedes']

>>> cars.insert(len(cars), "ferrari") # na końcu listy, równoważne z append()
>>> cars
['honda', 'bmw', 'audi', 'rover', 'mercedes', 'ferrari']

Usuwanie elementów z listy

Ok, a co zrobić jak masz już za dużo tych samochodów? Możesz jeden oddać mnie 😎
Ale jeśli chcesz po prostu usunąć go z listy, to użyj metody remove(). Jako argument podajemy wartość elementu, który chcemy usunąć.

Usunięty zostanie pierwszy element o tej wartości. A jeśli nie ma żadnego, to Python rzuci w nas ValueError.

>>> cars
['honda', 'bmw', 'audi', 'rover', 'mercedes', 'ferrari']

>>> cars.remove("mercedes")
>>> cars
['honda', 'bmw', 'audi', 'rover', 'ferrari']
>>> cars.remove("maserati")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: list.remove(x): x not in list

No dobra, a jak usunąć dowolny element? Jest taka bardzo fajna metoda jak pop(index), która oprócz usunięcia elementu jeszcze nam go zwraca!
Możemy podać wartość index, spod którego chcemy otrzymać element. Jeśli go nie podamy to dostaniemy ostatni:

>>> cars
['honda', 'bmw', 'audi', 'rover', 'ferrari']

>>> item = cars.pop()
>>> item
'ferrari'
>>> cars
['honda', 'bmw', 'audi', 'rover']

>>> another_item = cars.pop(1)
>>> another_item
'bmw'
>>> cars
['honda', 'audi', 'rover']

A jak już mamy całkowicie dość tych spalinowych śmierdzieli i chcemy przesiąść się na tramwaje, to możemy na dwa sposoby wezerować naszą listę:

>>> cars.clear()
>>> cars
[]

>>> del cars[:]
>>> cars
[]

Wyszukiwanie elementów

Zobaczmy jeszcze jak możemy znaleźć położenie elementu w liście szukając po jego wartości. Użyjemy do tego metody index().
Dla odpoczynku od aut pobawmy się teraz owocami 🍌

>>> fruits = ["banana", "raspberry", "apple", "banana"]
>>> fruits.index("banana")
0

Zauważ, że otrzymaliśmy index pierwszego z kolei elementu o podanej wartości. Ale możemy użyć dodatkowych elementów metody index(), żeby zawęzić przedział wyszukiwania:

>>> fruits.index("banana", 1, 4)
3

Przy czym sytuacja wygląda znowu tak jak przy slicingu – początek przedziału jest „zamknięty” (czyli szukamy od indeksu 1), a koniec jest „otwarty” (szukamy tak naprawdę do indeksu 3, a nie 4).

Jeśli elementu w ogóle nie ma na liście, to dostaniemy ValueError. Żeby tego uniknąć, możemy najpierw sprawdzić, czy dana wartość w ogóle występuje w liście, używając słówka kluczowego in:

>>> fruits.index("peach")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: 'peach' is not in list

>>> "peach" in fruits
False

I na koniec możemy jeszcze zliczyć ilość wystąpień danej wartości w liście:

>>> fruits.count("banana")
2

Sortowanie i odwracanie listy

Jedną z rzeczy, za które tak bardzo lubię Pythona, jest duża ilość wbudowanych i bardzo eleganckich operacji na strukturach danych, czego brakuje np. w Javie.

Jedną z takich operacji jest sortowanie listy. Jak możecie się już pewnie domyślić, służy do tego metoda sort():

>>> fruits
['banana', 'raspberry', 'apple', 'banana']

>>> fruits.sort()
>>> fruits
['apple', 'banana', 'banana', 'raspberry']

Sortowanie dokonuje się in place, czyli zmieniamy naszą listę, a nie tworzymy nowej. Wartości porównywane są do siebie w zależności od typu: stringi alfabetycznie, liczby w kolejności etc.

>>> nums = [4, 3, 7, 9, 1]
>>> nums.sort()
>>> nums
[1, 3, 4, 7, 9]

Ale jak zapewne pamiętasz, w liście możemy przechowywać dane różnego typu! Na przykład stringi i liczby jednocześnie. I co wtedy? 🤔

Jeśli Python nie będzie w stanie porównać do siebie elementów naszej listy, to jej nie posortuje i co mu wtedy zrobisz – nic nie zrobisz. A do tego rzuci w nas jeszcze wyjątkiem:

>>> mixed = ["kangaroo", 5, "elephant", 2]
>>> mixed.sort()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'int' and 'str'

No niestety, sortowanie nie jest dla wszystkich.
Ale jeśli chcemy zmodyfikować trochę logikę sortowania, to możemy podać jako argument key funkcję, która zwróci wartość, po której będziemy sortować.

Żeby to pokazać wróćmy do naszych owoców. Załóżmy, że nie chcemy tej listy sortować alfabetycznie, tylko po długości słów – od najkrótszego do najdłuższego:

>>> fruits = ["apple", "raspberry", "mango", "banana", "pineapple"]
>>> fruits.sort(key=len)
>>> fruits
['apple', 'mango', 'banana', 'raspberry', 'pineapple']

Jako key podaliśmy tutaj wbudowaną funkcję len(x), ale może to być dowolna funkcja przyjmująca jeden argument – będzie to każdy element naszej listy po kolei.

Odwrócenie kolejności sortowania jest bardzo proste:

>>> fruits.sort(key=len, reverse=True)
>>> fruits
['raspberry', 'pineapple', 'banana', 'apple', 'mango']

No właśnie, a co jeśli chcemy odwrócić naszą listę tak po prostu, bez sortowania? Może pamiętasz, że przy slicingu pokazywałem taki sposób:

>>> fruits = ["apple", "raspberry", "mango", "banana", "pineapple"]
>>> fruits[::-1]
['pineapple', 'banana', 'mango', 'raspberry', 'apple']

W ten sposób otrzymujemy nową, odwróconą listę, a stara zostaje bez zmian. Ale jest jeszcze jeden sposób odwracania, gdzie znów robimy to in place, modyfikując oryginalną kolekcję:

>>> fruits
['apple', 'raspberry', 'mango', 'banana', 'pineapple']
>>> fruits.reverse()
>>> fruits
['pineapple', 'banana', 'mango', 'raspberry', 'apple']

Kopiowanie listy

I na sam koniec powiem Ci o tym, jak skopiować Twoją listę. Służy do tego – a jakże – metoda copy()!

>>> new_fruits = fruits.copy()
>>> new_fruits
['apple', 'raspberry', 'mango', 'banana', 'pineapple']

Musisz jednak wiedzieć, że jest to tak zwana płytka kopia (ang. shallow copy). W podanym przykładzie nie ma to znaczenia, bo przechowujemy w naszej liście prosty typ zmiennej – string, który jest niemutowalny.

Ale gdybyśmy naszej liście trzymali bardziej złożone obiekty (np. słownik – dict), to po dokonaniu takiej płytkiej kopii, zmieniając „bebechy” naszych złożonych obiektów wpływamy na zawartość obu list:

>>> complex = [
... {"name": "John", "age": 27},
... {"name": "Martha", "age": 24}
... ]

>>> new_complex = complex.copy()
>>> complex
[{'name': 'John', 'age': 27}, {'name': 'Martha', 'age': 24}]
>>> new_complex
[{'name': 'John', 'age': 27}, {'name': 'Martha', 'age': 24}]

>>> complex[0]['name'] = "Luke"
>>> complex
[{'name': 'Luke', 'age': 27}, {'name': 'Martha', 'age': 24}]
>>> new_complex
[{'name': 'Luke', 'age': 27}, {'name': 'Martha', 'age': 24}]

Zauważ, że zmiana wartości w słowniku poprzez odniesienie do oryginalnej listy wpłynęła też na wartość w drugiej liście.

Ma to związek z tym, o czym pisałem na samym początku artykułu – lista tak naprawdę nie przechowuje samych wartości swoich elementów, a jedynie referencje do nich.

Dlatego w przypadku takiej płytkiej kopii obie listy de facto wskazują na te same obiekty w pamięci 😉

Podsumowanie

To tyle na dzisiaj w temacie list w Pythonie i operacji na nich. Mam nadzieję, że znalazłaś lub znalazłeś w tym artykule coś ciekawego i nowego 😉

A jeśli jesteś już Pythonowym wymiataczem i nic z tych rzeczy nie było dla Ciebie odkrywczego, to mam do Ciebie jedną prośbę – wyślij link do tego artykułu do jednej osoby, która Twoim zdaniem może z niego skorzystać. Dzięki!

4 myśli nt. „Struktury danych w Pythonie: lista

  1. Pingback: [VLOG #11] Python od podstaw #1: Struktury danych - Lista - Dziwne, u mnie działa

  2. Pingback: Struktury danych w Pythonie: tupla (krotka) - pythonowiec.pl

  3. Pingback: Struktury danych w Pythonie: set (zbiór) - pythonowiec.pl

  4. Pingback: Struktury danych w Pythonie: dict (słownik) - pythonowiec.pl

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *