Datové typy - kontejnery

V této lekci se nučíte používat Pythonovské kontejnery:

  • tuple (neměnný seznam),
  • list (měnitelný seznam),
  • dict (asociativní pole),
  • set (množina).

Zjistíte, že:

  • Prvky nemusejí být stejného typu (nejsou homogenní).
  • Kontejnery mají v Pythonu zásadní význam a najdete je téměř všude.
  • Si nedokážete představit, jak může někdo programovat bez list, tuple nebo dict.
  • Pro indexování (při získávání i přiřazování prvku) se používají hranaté závorky []. Podobně jako v jazyce C se prvky indexují od 0.

Poznámka na začátek: mutable versus immutable

Datové typy mohou být mutable (měnitelný) nebo immutable (neměnitelný). Immutable objekty (v Pythonu je v podstatě všechno objekt -- mnohokrát se s tím ještě setkáme) nemoho měnit svou hodnotu. Naproti tomu mutable objekty svou hodnotu měnit mohou (aniž by ztratili svou identitu). Immutable typy jsou např čísla (int, float, complex atd.), řetězce (str a unicode) a tuple. Mutable typy jsou např. list, dict nebo set.

Tuple

Správně česky snad "n-tice", nicméně často se používá prostě "tuple".

In [1]:
tuple1 = (1, 'a', 5)           # Základní syntax vytváření tuple (kulaté závorky)
tuple2 = 1, 'a'                # Závorky nejsou povinné, ale... !
tuple3 = tuple(["a", "b"])     # Pokročilé: Vytvoření tuple z jiného kontejneru
tuple4 = tuple(range(0, 10))   # Pokročilé: Vytvoření tuple z iterátoru / generátoru
tuple5 = ()                    # Prázdný tuple
tuple6 = ("single", )          # Tuple s jedním prvkem
tuple7 = 0, "1", (0, 1, 2)     # Tuple může pochopitelně obsahovat další tuple

# A co nám vylezlo?
print(f"tuple1={tuple1}")
print(f"tuple2={tuple2}")
print(f"tuple3={tuple3}")
print(f"tuple4={tuple4}")
print(f"tuple5={tuple5}")
print(f"tuple6={tuple6}")
print(f"tuple7={tuple7}")
tuple1=(1, 'a', 5)
tuple2=(1, 'a')
tuple3=('a', 'b')
tuple4=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
tuple5=()
tuple6=('single',)
tuple7=(0, '1', (0, 1, 2))

K získání prvku tuple použijeme hranaté závorky:

In [2]:
print(tuple4[0])         # První prvek
print(tuple4[-1])        # Poslední prvek
print(tuple4[-2])        # Předposlední prvek
0
9
8

Tuple nelze měnit:

In [3]:
tuple1[0] = "b"          # Vyhodí výjimku
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 tuple1[0] = "b"          # Vyhodí výjimku

TypeError: 'tuple' object does not support item assignment

Lze ale vytvořit nový tuple z existujících

In [4]:
print(tuple1 + tuple2)
print(2 * tuple6)
(1, 'a', 5, 1, 'a')
('single', 'single')

Metody tuple:

In [5]:
# I tuhle krkolomnou syntaxi brzy pochopíte ;-)
", ".join(item for item in dir(tuple) if not item.startswith("_"))
Out[5]:
'count, index'

Rozbalování (unpacking)

Tuple lze použít pro přiřazení hodnot do více proměnných najednou. Např.

In [6]:
(x, y, z) = (1, 2, 3)
print(y)
2

V tomto případě se závorky často vynechávají, takže můžeme psát

In [7]:
x, y, z = (1, 2, 3)
print(y)
2

To je užitečné zejména pro funkce, které vracejí více hodnot.

In [8]:
from math import ceil, floor

def neighbors(x):
    """Vrátí celá čísla a, b menší a větší než x, tj a < x < b"""
    a = int(floor(x))
    b = int(ceil(x))
    # pokud je x celé číslo, musíme přičíst/odečíst 1
    if a == x:
        a -= 1
        b += 1
    return a, b
In [9]:
# uvidíme, že funkce vrací tuple
print(neighbors(-1))
(-2, 0)
In [10]:
x = 3.3
# teď přiřadíme výsledek do dvou proměnných
a, b = neighbors(x)
print(f"{a} < {x} < {b}")
3 < 3.3 < 4

Na pravé straně může být jakýkoli iterabilní objekt (o iterátorech více později), např. seznam (o tom se dozvíme za chvilku) nebo string.

In [11]:
# vezmeme prvky ze seznamu
a, b, c = [1, 2, 3]
print(a, b, c)
1 2 3
In [12]:
# a nebo z textového řetězce
a, b, c = "ABC"
print(a, b, c)
A B C

Ještě více možností přináší extended unpacking a additional unpacking generalizations.

In [13]:
# do c se přiřadí všechny zbývající prvky v podobě seznamu
a, b, *c = (1, 2, 3, 4, 5, 6)
print(a)
print(b)
print(c)
1
2
[3, 4, 5, 6]

Důležitá je samozřejmě ona hvězdička, která může být i uprostřed.

In [14]:
a, *b, c = (1, 2, 3, 4, 5, 6)
print(a)
print(b)
print(c)
1
[2, 3, 4, 5]
6

Unpacking se hojně využívá i pro definice nebo volání funkcí, které mají proměnný počet argumentů. Všimněte si, že unpacking pomocí * může použit oběma směry - jak při volání, tak při definici funkce.

In [15]:
def print_all(*items, sep="<->"):
    print(sep.join(items))


arguments = ("a", "A", "Z")
print_all(*arguments)
a<->A<->Z
In [16]:
print_all("a", "A", "Z", sep="↵\n")
a↵
A↵
Z

Python vyhodí vyjímku pokud není počet prvků stejný

In [17]:
a, b, c = (1, 2, 3, 4, 5, 6)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[17], line 1
----> 1 a, b, c = (1, 2, 3, 4, 5, 6)

ValueError: too many values to unpack (expected 3)

List (seznam)

List je obdobou tuple, je ovšem mutable, tj. můžeme měnit jeho prvky. Vytváří se hranatými závorkami nebo funkcí list.

In [18]:
list(), []                 # prázdný list
list1 = ["a", "b", "c"]    # list vytvoříme pomocí [...]
list2 = [0, 0.0, "0.0"]    # můžeme tam dát libovolné typy
list3 = list(tuple1)       # nebo list vytvořit z tuple

print(list1)
print(list2)
print(list3)
['a', 'b', 'c']
[0, 0.0, '0.0']
[1, 'a', 5]

Metod obsahuje list více než tuple, což vyplývá z toho, že je mutable.

In [19]:
", ".join(item for item in dir(list) if not item.startswith("_"))
Out[19]:
'append, clear, copy, count, extend, index, insert, pop, remove, reverse, sort'

Přirozené pro seznam je přidávání na konec pomocí append a odebírání z konce pomocí pop:

In [20]:
list1.append("d")         # přidání prvku
print(list1)              # list1 se změnil!
list1.sort(reverse=True)
print(list1)
print(list1.pop())        # vyjme poslední prvek
print(list1)              # který je z původního listu vymazán
['a', 'b', 'c', 'd']
['d', 'c', 'b', 'a']
a
['d', 'c', 'b']

Odebrat prvek můžeme i pomocí remove, tato metoda ale musí prvek nejprve vyhledat.

In [21]:
list1.remove("d")         # odstranění prvku(ů)
print(list1)
['c', 'b']

Pomocí vnořených seznamů lze vytvářet "vícerozměrné" seznamy.

In [22]:
l = [[11, 12], [21, 22]]   # "vícerozměnrý" list
print(l[0][0])             # prvek [0,0]
11

Všechny mutable typy (a tedy i list) v podstatě reference nebo, chcete-li, ukazatele (pointery). Na to musíme pamatovat, abychom nechtěně nepřepisovali obsah jiné proměnné.

In [1]:
a = [1, 2, 3]
b = a            # b je identický list jako a (ne jeho kopie)
b.insert(0, 0)   # protože list je mutable, zmení se b i a
print(a)
[0, 1, 2, 3]
In [2]:
print(a is b)          # operátor is testuje identitu (objektů)
True
In [3]:
b = a.copy()           # pokud chceme kopii, můžeme použít metodu copy

print(a is b)
print(a == b)          # operátor == testuje hodnoty
False
True

Jakmile b změníme, už se nezmění a jelikož jsme vytvořili nový objekt. Už se ani nerovnají podle hodnot, tj. podle operátoru ==.

In [4]:
b.append(5)
print(a)
print(b)
print(a == b)
[0, 1, 2, 3]
[0, 1, 2, 3, 5]
False

Indexování neboli řezání (slicing)

Řezy jsou velice důležitým konceptem. Pro proměnný typu list a tuple lze řezy použít pro výběr prvku(ů) poměrně sofistikovaným způsobem, lze je použít i pro změnu seznamu. list a tuple umožňují tzv. jednoduchý řez (simple slice), detaily viz dokumentace. Rozšířené řezy (extended slicing) uvidíme později pro pole Numpy. Syntaxe jednoduchého řezu je

[[dolní_mez] : [horní_mez] [: [krok]]

Implicitní hodnota pro horní a dolní mez je None, pro krok je implicitné hodnota 1. Výsledek obsahuje prveky s indexy od dolní meze (včetně) až po prvky s indexy menšími než horní mez, případně s daným krokem. Na příkladech si ukážeme, jak to funguje.

In [30]:
# vytvoříme jednoduchý seznam (range v Pythonu 3 nevrací list, proto je lepší použít konverzi)
l = list(range(10))
# všechny prvky seznamu
print(l[:])
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [31]:
# První dva prvky
print(l[0:2])
[0, 1]
In [32]:
# Poslední tři prvky
print(l[-3:])
[7, 8, 9]
In [34]:
# Sudé prvky
print(l[::2])
[0, 2, 4, 6, 8]
In [35]:
# obrácené pořadí pomocí řezů
print(l[::-1])
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Pomocí řezů můžeme do seznamu prvky přidávat (pro přidávání existuje ještě metoda insert)

In [36]:
l = list(range(10))
print(l)
l[:1] = ["jsem prvni"]
print(l)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
['jsem prvni', 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [37]:
# můžeme nahradit několik prvků jinými, jejichž počet nemusí být stejný
l[1:3] = ["jedna", "dva", "tri"]
print(l)
['jsem prvni', 'jedna', 'dva', 'tri', 3, 4, 5, 6, 7, 8, 9]

... nebo je i mazat.

In [38]:
l = list(range(10))
print(l)
# vymaže prvky [0:2]
l[0:2] = []
print(l)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[2, 3, 4, 5, 6, 7, 8, 9]
In [39]:
# nebo můžeme použít del
del l[0:2]
print(l)
[4, 5, 6, 7, 8, 9]

Už víme, že seznam (list) je mutable a že kopii můžeme vytvořit pomocí metody copy. Můžeme použít ale i řez [:], tj.

In [40]:
a = ["a"]
b = a[:]
# otestujeme pomocí is, jestli jsou a, b identické obejkty
print(a is b)
False

Cvičení: Vytvořte seznam programovacích jazyků (alespoň pěti), které znáte (nezapomeňte Python :).

  1. Pomocí metody sort ho setřiďte ho abecedy a vypište.
  2. Vypište první a poslední jazyk.
  3. Na konec seznamu přidejte jazyk "Lisp".
  4. Vymažte ze seznamu vše kromě prvních dvou a posledních dvou jazyků.

Hledání v seznamech

Pro testování, zda je (není) prvek v seznamu (nebo tuple) použijeme klíčové slovo in (not in). Dále existuje metoda index, která vrací polohu nějakého prvku.

In [41]:
l = ["a", "A", "b", "ABC", 1, "2"]
# použijeme in pro test jestli list obsohuje "b" a "B"
print("b" in l)
print("B" in l)
print("B" not in l)
True
False
True
In [43]:
# nyní vyzkoušíme metodu index
print(l.index("b"))
2
In [44]:
print(l.index("B"))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[44], line 1
----> 1 print(l.index("B"))

ValueError: 'B' is not in list

Dictionary (slovník)

Slovník je asociativní pole, jehož klíči jsou jakékoliv hashovatelné objekty (čísla, řetězce, tuply a většina uživatelsky definovatelných tříd). Jako klíč se nedají použít např. mutable kontejnery (dict, list apod.), které jsou nehashovatelné.

In [45]:
prazdny_slovnik = dict()
prazdny_slovnik2 = {}                               # Ekvivalentní zápis

slovnik = {69: 5, "pole_podle_skal" : [1, 2, 3]}  # Různé typy uložených hodnot
print(slovnik)
{69: 5, 'pole_podle_skal': [1, 2, 3]}

Získávání hodnot pomocí []

In [47]:
print(slovnik[69])                           # => 5
5
In [48]:
print(slovnik["pole_podle_skal"])        # => [1, 2, 3]
[1, 2, 3]
In [49]:
slovnik[100]           # Neexistující klíč -> vyhodí výjimku
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[49], line 1
----> 1 slovnik[100]           # Neexistující klíč -> vyhodí výjimku

KeyError: 100
In [50]:
print(slovnik.get(100))                       # Varianta, která výjimku nevyhodí (a vrátí None)
print(slovnik.get(100, "vychozi_hodnota"))    # Varianta, která vrátí definovanou výchozí hodnotu, když se prvek nenajde
None
vychozi_hodnota

Metody slovníku:

In [51]:
", ".join(item for item in dir(dict) if not item.startswith("_"))
Out[51]:
'clear, copy, fromkeys, get, items, keys, pop, popitem, setdefault, update, values'

Pro test, zda slovník (ne)obsahuje položku s daným klíčem, slouží stejně jako pro tuple a list operátor in, resp. not in.

Set (množina)

Neřazený a neindexovatelný seznam hashovatelných prvků, ve kterém každý prvek smí být pouze jednou. V zásadě se chová podobně jako slovník obsahující jenom klíče.

In [53]:
print({"srdce", "piky", "kule", "krize"})
print(set(("a", "a", "a", "b", "b")))           # Duplicitní prvky se odstraní
{'krize', 'kule', 'piky', 'srdce'}
{'a', 'b'}
In [54]:
{0}[0]                                           # Nelze indexovat
<>:1: SyntaxWarning: 'set' object is not subscriptable; perhaps you missed a comma?
<>:1: SyntaxWarning: 'set' object is not subscriptable; perhaps you missed a comma?
/var/folders/dm/gbbql3p121z0tr22r2z98vy00000gn/T/ipykernel_39494/326244760.py:1: SyntaxWarning: 'set' object is not subscriptable; perhaps you missed a comma?
  {0}[0]                                           # Nelze indexovat
/var/folders/dm/gbbql3p121z0tr22r2z98vy00000gn/T/ipykernel_39494/326244760.py:1: SyntaxWarning: 'set' object is not subscriptable; perhaps you missed a comma?
  {0}[0]                                           # Nelze indexovat
/var/folders/dm/gbbql3p121z0tr22r2z98vy00000gn/T/ipykernel_39494/326244760.py:1: SyntaxWarning: 'set' object is not subscriptable; perhaps you missed a comma?
  {0}[0]                                           # Nelze indexovat
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[54], line 1
----> 1 {0}[0]                                           # Nelze indexovat

TypeError: 'set' object is not subscriptable

Pro množiny jsou k dispozici operátory (případně metody)

  • | (union)
  • & (instersection)
  • - (difference)
  • ^ (symmetric_difference)
  • <, <= (issubset)
  • >, >= (issuperset)

Cvičení::

  1. Vytvořte slovník ve kterém přiřadíte programovacím jazykům body podle oblíbenosti (1 - 10).
  2. Zjistěte pomocí množinových operací, které z vašich programovacích jazyků jsou a které nejsou společné s množinou COMMON_LANGUAGES definovanou níže.
In [55]:
COMMON_LANGUAGES = {'Python', 'C', 'Java'}

Vestavěné funkce pro práci s kontejnery

Python má několik důležitých funkcí, které se hodí pro práci s kontejnery.

len vrací počet prvků

In [59]:
o = [1, 1, 2, 2]
len_o = len(o)
print(f"len({o}) = {len_o}")
len([1, 1, 2, 2]) = 4
In [1]:
o = {1, 1, 2, 2}
len_o = len(o)
print(f"len({o}) = {len_o}")
len({1, 2}) = 2

sum vrací součet prvků

In [2]:
o = [1, 1, 2, 2]
sum_o = sum(o)
print(f"sum({o}) = {sum_o}")
sum([1, 1, 2, 2]) = 6
In [3]:
o = {1, 1, 2, 2}
sum_o = sum(o)
print(f"sum({o}) = {sum_o}")
sum({1, 2}) = 3

Pozor, python je silně typovaný jazyk

In [4]:
o = 1, 1, 2, 2, "3"
sum(o)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 2
      1 o = 1, 1, 2, 2, "3"
----> 2 sum(o)

TypeError: unsupported operand type(s) for +: 'int' and 'str'

min a max vrací nejmenší, resp. největší prvky.

In [5]:
o = [1, 2, -1, -10, 0]
print(f"min({o}) = {min(o)}")
print(f"max({o}) = {max(o)}")
min([1, 2, -1, -10, 0]) = -10
max([1, 2, -1, -10, 0]) = 2

sorted vrací setříděné prvky pole.

reversed vrací prvky pozpátku (pomocí iterátoru).

all, any a meší odbočka k bool

all a any vrátí logické and, resp. or aplikované mezi všemi prvky.

In [1]:
o = [True, True, True]
print(f"all({o}) = {all(o)}")
print(f"any({o}) = {any(o)}")
all([True, True, True]) = True
any([True, True, True]) = True
In [2]:
o = [True, False, True]
print(f"all({o}) = {all(o)}")
print(f"any({o}) = {any(o)}")
all([True, False, True]) = False
any([True, False, True]) = True

Při této příležitosti ještě odbočíme a ukážeme si, jak Python převádí cokoli (tj. objekt jakéhokoli typy) na bool, tj. na True nebo False. Tento převod můžeme udělat explicitně pomocí samotné funkce bool, děje se ale také implicitně v blocích typu if nebo while a také při použití all a any.

všechna čísla kromě 0 jsou True

In [3]:
print(bool(0))
print(bool(0.0))
print(bool(0.0 + 0j))
print(bool(-1))
False
False
False
True

řetězce jsou True pokud nejsou prázdné

In [4]:
print(bool(""))
print(bool("0"))
False
True

kontejnery tuple, list, dict, set apod. jsou True pokud nejsou prázné

In [6]:
print(bool([]))
print(bool(()))
print(bool({0}))
False
False
True

Pokud chceme otestovat jednotlivé prvky, musíme použít all nebo any

In [7]:
print(f"all({{0}}) = {all({0})}")
all({0}) = False

Pozor na all a any logiku prázdných kontejnerů, nemusí být zcela intuitivní.

In [8]:
o = []
print(f"all({o}) = {all(o)}")
print(f"any({o}) = {any(o)}")
all([]) = True
any([]) = False

Cvičení

Rozšiřte hru oko bere z předchozí kapitoly o zaznamenávání a výpis tažených karet. T.j. uživatel se na konci hry dozví, které karty byly taženy. Použijte k tomu seznam.

Pokročilejší verze: Místo pouhých čísel s hodnotou karet pracujte s názvy karet. Uložte si hodnoty jednotlivých karet do slovníku.

Hodnoty karet:

  • karty 7 – 10 si nechávají hodnotu ( sedma za sedm, osma, za osm, … )
  • spodek a svršek za 1 bod
  • král za 2 body
  • Eso vždy za 11 (ale dvě esa mají součet 21)

Comments

Comments powered by Disqus