Struktury danych w Pythonie: tupla (krotka)

przez | 30 grudnia 2019

W tym kolejnym wpisie z serii o strukturach danych w Pythonie dowiesz się wszystkiego o tupli! Dziwna nazwa, nie? To spolszczona forma angielskiego tuple, ale polska nazwa jest chyba jeszcze dziwniejsza – „krotka”. Na co dzień zazwyczaj mówię „tupla”, więc takiej nazwy będę się tutaj trzymał.

Dobrym wprowadzeniem do tupli jest poznanie najpierw listy, dlatego jeśli jeszcze nie czytałeś mojego poprzedniego posta, gdzie zdradzam wszystkie ich tajniki, to serdecznie Cię do tego zachęcam 😉
https://pythonowiec.pl/2019/12/struktury-danych-lista-w-pythonie/

Mam dla Ciebie zagadkę: czy wiesz, jak w Pythonie zamienić miejscami wartości dwóch zmiennych, bez używania żadnej dodatkowej zmiennej tymczasowej?

Jeśli nie to odpowiedź znajdziesz w tym artykule!

Charakterystyka tupli

Tupla jest bardzo podobna do listy, czyli również można w niej trzymać elementy różnego typu oraz jest strukturą zapewniająca zachowanie kolejności elementów.

Tupla różni się od listy przede wszystkim w jednym aspekcie: jest niemutowalna. To oznacza, że raz utworzona tupla nie może zmienić swojej wartości:

  • nie można do niej dodawać ani odejmować elementów
  • nie można podmieniać żadnego elementu na inny

W dalszej części omówię różne aspekty niemutowalności ze szczegółami.

Jak stworzyć tuplę?

Do tworzenia listy służą nawiasy kwadratowe [], a do deklaracji tupli – zwykłe nawiasy okrągłe: ().

Pusta tupla

Tak utworzymy pustą tuplę:

>>> t = ()
>>> t
()

Ponieważ tupla jest niemutowalna to pusta nam się zazwyczaj do niczego nie przyda. Najczęściej skorzystamy z niej jeśli jakaś funkcja lub metoda oczekuje tupli / iterabla, a my chcemy przekazać akurat pustą.

Tupla jednoelementowa

Częściej, choć też rzadko, może nam się przydać tupla z jednym elementem. Jest z nią o tyle śmiesznie, że nie możemy zrobić po prostu tak:

>>> t = ("a")
>>> t
'a'
>>> type(t)
<class 'str'>

Jeśli podamy jedną wartość w nawiasie, to Python „oleje” ten nawias i utworzy po prostu zmienną o wartości naszego jedynego elementu – w tym wypadku str.

Dlaczego tak się dzieje? Dlatego, że w Pythonie nawiasy w wyrażeniach służą do określania kolejności działań, np. w wyrażeniach algebraicznych – dokładnie tak samo, jak na matmie 😉

Co możemy z tym fantem zrobić? Żeby powiedzieć Pythonowi, że chodzi nam o tuplę z jednym elementem, musimy dodać po nim przecinek:

>>> t = ("a",)
>>> t
('a',)
>>> type(t)
<class 'tuple'>

Tupla wieloelementowa

Najczęściej jednak będziemy chcieli stworzyć tuplę z więcej niż jednym elementem. Jak zapewne się już domyślasz, robimy to tak:

>>> t = (1, "banana", True)
>>> t
(1, 'banana', True)

Tak dla przypomnienia: jak widzisz na przykładzie powyżej, tupla – tak samo jak lista – może przechowywać wartości różnego typu.

Panie, po co komu te nawiasy?

Ciekawą cechą tupli jest to, że w wielu sytuacjach możemy w ogóle pominąć nawiasy! Tak naprawdę to przecinek mówi o tym, że tworzymy tuplę, a nie nawiasy. Jeśli tworzymy tuplę z jednym lub więcej elementami, to wystarczy zapis:

>>> t = "a",
>>> t
('a',)
>>> t = 1, "banana", True
>>> t
(1, 'banana', True)

Jedną z sytuacji, kiedy nie możemy tak zrobić, jest gdy wywołujemy jakąś funkcję. Jeśli jako argument chcielibyśmy przekazać literał tupli (czyli bez przypisania jej wcześniej do zmiennej), to będzie problem. Spójrzmy na poniższy przykład:

>>> def fun(value):
...     print(value)
...
>>> fun((1, 2, 3))
(1, 2, 3)

Mamy funkcję, która przyjmuje jedną wartość i printuje ją w konsoli. Wywołaliśmy ją podając jako argument literał tupli, ale z nawiasami. A co się stanie bez nawiasów?

>>> fun(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: fun() takes 1 positional argument but 3 were given

Python widząc takie wywołanie „gubi się”. Myśli, że podajemy mu 3 argumenty, a wie, że funkcja fun przyjmuje tylko 1 argument. Stąd mamy błąd. Dlatego tutaj musimy albo użyć nawiasów, albo przypisać wcześniej wartość tupli do zmiennej:

>>> t = 1, 2, 3
>>> fun(t)
(1, 2, 3)

Jakby komuś było mało, to jest jeszcze jeden sposób

Podobnie jak przy liście i słowie kluczowym list, tak i tuplę możemy stworzyć wywołując tuple(). Jednak działa to tylko dla dwóch przypadków: pustej tupli i tworzenia tupli na podstawie innego iterabla, na przykład listy:

>>> t = tuple()
>>> t
()
>>>
>>> l = [6, 7, 8]
>>> type(l)
<class 'list'>
>>>
>>> t = tuple(l)
>>> t
(6, 7, 8)
>>> type(t)
<class 'tuple'>

Operacje na tuplach

Co do zasady na tuplach możemy wykonywać praktycznie wszystkie podstawowe operacje dla sekwencji. Opisałem je szczegółowo w artykule o listach, dlatego jeśli chcesz dowiedzieć się więcej, to zajrzyj właśnie tam.

Wyjątkiem są te operacje, które modyfikują sekwencję. Ich nie możemy użyć w przypadku tupli w związku z jej niemutowalnością. Przykład operacji, których na tupli nie wykonamy:

>>> t = 1, "banana", True
>>> t.pop()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'pop'
>>> t.append("kiwi")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'append'
>>> t.copy()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'copy'

Przykład kilku operacji, które działają dokładnie tak samo (tym razem wejdziemy w bardziej świąteczny klimat):

>>> t = ("santa", "reindeer", "elf", "gift", "mistletoe", "snow")
>>> t[0]
'santa'
>>> t[::-1]
('snow', 'mistletoe', 'gift', 'elf', 'reindeer', 'santa')
>>> t[2:-1]
('elf', 'gift', 'mistletoe')
>>> "elf" in t
True

Jeśli któreś z powyższych wyrażeń jest dla Ciebie niezrozumiałe, to jeszcze raz odsyłam Cię do mojego artykułu o listach – tam wszystko wyjaśniam.

Operacje dające w wyniku nową tuplę

Tupli nie możemy zmieniać, więc nie dodamy do niej na przykład nowego elementu. Ale jest pewien „wytrych”.

Może pamiętasz, że o ile metoda append() w przypadku listy dodaje nowy element i zmienia tę samą listę, to za pomocą operatora + możemy dodać nowy element (albo elementy) i w wyniku dostajemy nową listę.

Ten sam mechanizm działa w przypadku tupli!

>>> t = ("santa", "reindeer", "elf", "gift", "mistletoe", "snow")
>>> t.append("Christmas tree")  # nie zadziała
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'append'
>>>
>>> t + ("Christmas tree",)
('santa', 'reindeer', 'elf', 'gift', 'mistletoe', 'snow', 'Christmas tree')
>>>
>>> t
('santa', 'reindeer', 'elf', 'gift', 'mistletoe', 'snow')

Ja widać powyżej, oryginalna tupla została bez zmian.

Sortowanie

Przy listach mówiłem o metodzie sort(), którą wywołujemy na liście i dostajemy w wyniku automagicznie posortowaną listę. Ponieważ dzieje sie to „w miejscu”, czyli modyfikowana jest oryginalna lista, to – jak pewnie się już domyślasz – nie możemy z niej skorzystać przy tuplach.

Ale, ale! W tamtym artykule nie wspomniałem o pewnej funkcji z biblioteki standardowej, działającej zarówno dla list jak i dla tupli, która sortuje i zwraca nową sekwencję, nie modyfikując starej. Jest to funkcja sorted():

>>> t = ("santa", "reindeer", "elf", "gift", "mistletoe", "snow")
>>> t.sort()  # nie zadziała
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'sort'
>>>
>>> sorted(t)
['elf', 'gift', 'mistletoe', 'reindeer', 'santa', 'snow']
>>>
>>> t
('santa', 'reindeer', 'elf', 'gift', 'mistletoe', 'snow')

„Minusem” jest to, że sorted() zwraca nową listę, więc jeśli chcemy z powrotem mieć znowu tuplę, to będziemy musieli sami o to zadbać:

>>> tuple(sorted(t))
('elf', 'gift', 'mistletoe', 'reindeer', 'santa', 'snow')

Podobnie jak w przypadku sort(), tutaj też możemy podać argumenty key i reverse:

>>> sorted(t, key=len)
['elf', 'gift', 'snow', 'santa', 'reindeer', 'mistletoe']
>>>
>>> sorted(t, key=len, reverse=True)
['mistletoe', 'reindeer', 'santa', 'gift', 'snow', 'elf']

O niemutowalności słów jeszcze kilka

Jak pokazałem wyżej, tuple są niemutowalne. Oznacza to dokładnie tyle, że nie możemy do nich dodawać ani im zabierać elementów (append(), pop(), etc.), a także nie możemy elementów zamieniać, np:

>>> t = ("santa", "reindeer", "elf", "gift", "mistletoe", "snow")
>>> t[0] = "fake santa"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

Zmiana referencji

Ale, jak mogliście zauważyć w wielu przykładach powyżej, nic nie stoi na przeszkodzie, żeby do zmiennej, która wskazuje na jakąś tuplę, przypisać nową wartość – na przykład inną tuplę:

>>> t = ("santa", "reindeer", "elf", "gift", "mistletoe", "snow")
>>> t
('santa', 'reindeer', 'elf', 'gift', 'mistletoe', 'snow')
>>>
>>> t = (1, 2, 3)
>>> t
(1, 2, 3)

Może się to wydawać oczywiste, ale warto o tym powiedzieć głośno: t tylko „wskazuje”, czy też ma „referencję” do jakieś tupli. A sama niemutowalność tupli nie przeszkadza w zmianie tej referencji na inną.

Typy złożone wewnątrz tupli już nie takie niemutowalne

Podobnie jak to pokazywałem przy kopiowaniu listy, tak i przy tupli trzeba pamiętać o mutowalności typów złożonych.

Jeśli przechowujemy wewnątrz tupli obiekty, które nie są typami prostymi (jak int, str, bool, itp.), tylko np. słownikiem (dict) albo instancją naszej własnej klasy, to niemutowalność samej tupli w żaden sposób nie „zabezpiecza” nam „wnętrz” tych obiektów przed modyfikacją.

Spójrzmy na przykład ze słownikiem:

>>> t = ({"name": "Grzegorz"}, {"name": "Agnieszka"})
>>> t[0]
{'name': 'Grzegorz'}
>>> t[0]["name"]
'Grzegorz'
>>> t[0]["name"] = "Adam"
>>> t
({'name': 'Adam'}, {'name': 'Agnieszka'})

Bez problemu jesteśmy w stanie zmienić wartość w słowniku, który jest elementem tupli.

Dlatego trzeba pamiętać, że niemutowalność tupli ma swoje granice 😉

Tuple unpacking, czyli prawie jak odpakowywanie prezentów

Sekwencje w Pythonie mają ciekawą właściwość jaką jest „unpacking”. Mimo, że nie wspomniałem o tym w poprzednim artykule, to działa to zarówno dla list, tupli jak i setów (o których powiem innym razem).

Chodzi o to, że w prosty sposób jesteśmy w stanie przypisać wszystkie elementy sekwencji do indywidualnych zmiennych:

>>> t = "bicycle", "lego", "video game"
>>> t
('bicycle', 'lego', 'video game')
>>>
>>> p1, p2, p3 = t
>>> p1
'bicycle'
>>> p2
'lego'
>>> p3
'video game'

Wymaganie jest takie, że zmiennych po lewej stronie musi być tyle samo, co elementów sekwencji. Inaczej dostaniemy błąd:

>>> p1, p2 = t
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)

Rozwiązanie zagadki

I tu dochodzimy do wyjaśnienia zagadki z początku artykułu:

Jak w Pythonie zamienić miejscami wartości dwóch zmiennych, bez używania żadnej dodatkowej zmiennej tymczasowej?

Klasycznie, ze zmienną tymczasową, zrobilibyśmy to tak:

>>> a = 1
>>> b = 2
>>> a, b
(1, 2)
>>>
>>> temp = a
>>> a = b
>>> b = temp
>>>
>>> a, b
(2, 1)

Jak widzisz powyżej, gdy w interpreterze piszemy a, b to wynik wyświetlony jest jako tuple. To sprawia, że do zamiany wartości zmiennych możemy wykorzystać tuple unpacking!

>>> a, b
(1, 2)
>>>
>>> a, b = b, a
>>>
>>> a, b
(2, 1)

Niezłe, co nie? 😎 Jak kiedyś użyjesz tego „na produkcji” to daj koniecznie znać 😂

Kiedy używać tupli?

Okey, wiesz już dokładnie o co chodzi w tuplach, to powiedzmy jeszcze kiedy warto ich używać zamiast list.

Ze względu na swoją niemutowalność są nieco wydajniejsze niż listy, choć przy małych rozmiarach różnica będzie raczej niezauważalna. Ale przy dużej ilości danych można coś tam „urwać” z naszego CPU.

Dla mnie najważniejszym powodem jest chęć pokazania wyraźnie w kodzie, że dana kolekcja ma być niezmienna. I chociaż nie mamy gwarancji, że ktoś nie przepisze nam referencji naszej tupli albo nie zmieni „bebechów” złożonych obiektów będących elementami, to jednak jest to sygnał dla innych programistów i dla mnie samego z przyszłości, że pożądana jest niezmienność. „Tego proszę nie dotykać!”

Niemutowalność pozwala też na zastosowanie tupli tam, gdzie wymagany jest tzw. „hashowalny” obiekt. Na tupli możemy wywołać funkcję hash() właśnie dlatego, że jest niezmienna. I to między innymi pozwala wykorzystać ją jako klucz w słowniku, czego z listą nie możemy zrobić. Ale o tym porozmawiamy więcej przy okazji słowników już niedługo 😉

Podsumowanie

Dzięki za dotrwanie do końca tego artykułu! Jeśli Ci się podobał i dał Ci wartość, to będę mega wdzięczny za słówko komentarza tutaj, albo na YouTubie! To bardzo motywuje do dalszego działania 😁

A jeśli nie chcesz przegapić kolejnych wpisów, to gorąco Cię zachęcam do zasubskrybowania mojego kanału na YouTube. Jeśli wolisz czytać raczej niż oglądać, to nie przejmuj się – pod każdym filmem znajdziesz link do artykułu na stronie.

Jeszcze raz dzięki, wszystkiego dobrego i do następnego razu! 👋


Jeśli programujesz w Javie i interesuje Cię Python, to koniecznie przeczytaj mój darmowy dokument! Wyjaśniam w nim, dlaczego warto nauczyć się Pythona będąc Javowcem 🙂

Link do Twojego dokumentu: https://java.pythonowiec.pl/

2 myśli nt. „Struktury danych w Pythonie: tupla (krotka)

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

  2. 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 *