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 [9]:
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)
<ipython-input-3-d04f6f927064> in <module>
----> 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 [10]:
def neighbors(x):
    """Vrátí celá čísla a, b menší a větší než x, tj a < x < b"""
    from math import ceil, floor
    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
# uvidíme, že funkce vrací tuple
print(neighbors(-1))
x = 3.3
# teď přiřadíme výsledek do dvou proměnných
a, b = neighbors(x)
print(f"{a} < {x} < {b}")
(-2, 0)
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)
# a nebo z textového řetězce
a, b, c = "123"
print(a, b, c)
1 2 3
1 2 3

Python 3 přidává velice užitečnou funkcionalitu v podobě extended unpacking, 3.5 pak ještě additional unpacking generalizations.

In [12]:
# 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 [13]:
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ů.

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


arguments = ("a", "A", "Z")

print_all(*arguments)

print_all("a", "A", "Z", sep="↵\n")
a<->A<->Z
a↵
A↵
Z

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

In [15]:
a, b, c = (1, 2, 3, 4, 5, 6)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-15-fa13b31242da> in <module>
----> 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 [16]:
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 [17]:
", ".join(item for item in dir(list) if not item.startswith("_"))
Out[17]:
'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 [18]:
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 [19]:
list1.remove("d")         # odstranění prvku(ů)
print(list1)
['c', 'b']

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

In [20]:
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 [21]:
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 [22]:
print(a is b)          # operátor is testuje identitu (objektů)
from copy import copy
b = copy(a)            # pokud chcepe kopii, potřebujeme modul copy
print(a is b)
print(a == b)          # operátor == testuje hodnoty
b.append(5)
print(a)
print(b)
True
False
True
[0, 1, 2, 3]
[0, 1, 2, 3, 5]

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 [23]:
# 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[:])
# První dva prvky
print(l[0:2])
# Poslední tři prvky
print(l[-3:])
# Sudé prvky
print(l[::2])
# obrácené pořadí pomocí řezů
print(l[::-1])
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1]
[7, 8, 9]
[0, 2, 4, 6, 8]
[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 [24]:
l = list(range(10))   
print(l)
l[:1] = ["jsem prvni"]
print(l)
# můžeme nahradit několik prvků jinými, jejichž počet nemusí být stejný
l[1:3] = ["jedna", "dva", "tri"]
print(l)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
['jsem prvni', 1, 2, 3, 4, 5, 6, 7, 8, 9]
['jsem prvni', 'jedna', 'dva', 'tri', 3, 4, 5, 6, 7, 8, 9]

... nebo je i mazat.

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

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

In [26]:
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 :). Setřiďte ho podle abecedy a vypište.

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 [27]:
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)
# nyní vyzkoušíme metodu index
print(l.index("b"))
print(l.index("B"))
True
False
True
2
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-27-c4e4fee9f0d2> in <module>
      6 # nyní vyzkoušíme metodu index
      7 print(l.index("b"))
----> 8 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 [26]:
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 [27]:
print(slovnik[69])                           # => 5
print(slovnik["pole_podle_skal"])        # => [1, 2, 3]
5
[1, 2, 3]
In [28]:
slovnik[100]           # Neexistující klíč -> vyhodí výjimku
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-28-7f6772f6f060> in <module>()
----> 1 slovnik[100]           # Neexistující klíč -> vyhodí výjimku

KeyError: 100
In [29]:
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 [30]:
", ".join(item for item in dir(dict) if not item.startswith("_"))
Out[30]:
'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.

Další varianty slovníku

collections.OrderedDict

V běžném slovníku jsou data neřazená. Pokud tedy procházíte slovníkem prvek po prvku, klíče budou dosti přeházené (podle toho, jak je zpracuje interní hashovací funkce). Alternativou je OrderedDict (seřazený slovník), který prvky za sebou řadí. Ovšem pozor: Řadí je podle toho, v jakém pořadí byly do slovníku vloženy, nikoliv podle abecedy nebo jiného kritéria.

In [31]:
import collections
pairs = (("a" , 0), ("b", 1), ("c", 2), ("d", 3), ("e", 4))      # tuple tuplů
normal = dict(pairs)
ordered = collections.OrderedDict(pairs)

print("Keys (normal dictionary): " + ", ".join(key for key in normal))
print("Keys (ordered dictionary): " + ", ".join(key for key in ordered))
Keys (normal dictionary): a, b, c, d, e
Keys (ordered dictionary): a, b, c, d, e

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 [32]:
print({"srdce", "piky", "kule", "krize"})
print(set(("a", "a", "a", "b", "b")))           # Duplicitní prvky se odstraní
{'kule', 'srdce', 'piky', 'krize'}
{'b', 'a'}
In [33]:
{0}[0]                                           # Nelze indexovat
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-33-c34f5b5aeb2b> in <module>()
----> 1 {0}[0]                                           # Nelze indexovat

TypeError: 'set' object does not support indexing

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

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

Cvičení::

  1. Pomocí vhodného kontejneru přiřaďte programovacím jazykům body podle oblíbenosti (1 - 10).
  2. Zjistěte, které z těchto jazyků jsou / nejsou společné s množinou COMMON_LANGUAGES definovanou níže. a. Přímo pomocí množinové operace, t.j. pomocí set onjektů. b. Pomocí for cyklu.

Můžete doplnit implementaci níže nebo vymyslet svou vlastní hezčí.

In [28]:
COMMON_LANGUAGES = {'Python', 'C', 'Java'}

Pole v Numpy

Budeme trochu předbíhat, ale univerzalita základních kontejnerů může být v mnoha případech (při vědeckých výpočtech) nevýhodou - co se týče rychlosti, ale i vzhledem k tomu, že nemáme zaručený typ prvků ani rozměry pole. Proto knihovna numpy zavádí typ vícerozměrného pole ("ndarray"), který je extrémně rychlý (vektorové operace s ním jsou implementovány v C) a velice šikovný (umožňuje snadno pracovat s mnohorozměrnými poli i jejich komplikovanými indexy). Seznámíme se s ním v některé z dalších hodin.

Na jednoduché ukázce si ukážeme, jak se liší rychlost sčítání vektorů bez a s numpy.

In [35]:
array1 = list(range(0, 1000000))
array2 = list(range(1000000, 2000000))
print("Bez numpy")
%timeit array3 = [array1[i] + array2[i] for i in range(0, len(array1))]
Bez numpy
149 ms ± 1.47 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [36]:
import numpy as np
array1 = np.arange(0, 1000000)
array2 = np.arange(1000000, 2000000)
print("S numpy")
%timeit array3 = array1 + array2
S numpy
2.3 ms ± 20.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Jak vidíte, stejná operace (sečtení pole prvek po prvku) s využitím numpy proběhla asi 60x rychleji.

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 [37]:
o = [1, 1, 2, 2]
print("len(%s) = %i" % (o, len(o)))
o = 1, 1, 2, 2
print("len(%s) = %i" % (o, len(o)))
o = {1, 1, 2, 2}
print("len(%s) = %i" % (o, len(o)))
len([1, 1, 2, 2]) = 4
len((1, 1, 2, 2)) = 4
len({1, 2}) = 2

sum vrací součet prvků

In [38]:
o = [1, 1, 2, 2]
print("sum(%s) = %i" % (o, sum(o)))
o = 1, 1, 2, 2
print("sum(%s) = %i" % (o, sum(o)))
o = {1, 1, 2, 2}
print("sum(%s) = %i" % (o, sum(o)))
sum([1, 1, 2, 2]) = 6
sum((1, 1, 2, 2)) = 6
sum({1, 2}) = 3

Pozor, python je silně typovaný jazyk

In [39]:
o = 1, 1, 2, 2, "3"
print("sum(%s) = %i" % (o, sum(o)))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-39-35e36c336854> in <module>()
      1 o = 1, 1, 2, 2, "3"
----> 2 print("sum(%s) = %i" % (o, sum(o)))

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

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

In [40]:
o = [1, 2, -1, -10, 0]
print("min(%s) = %i" % (o, min(o)))
print("max(%s) = %i" % (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 [41]:
o = [True, True, True]
print("all(%s) = %i" % (o, all(o)))
print("any(%s) = %i" % (o, any(o)))
o = [True, False, True]
print("all(%s) = %i" % (o, all(o)))
print("any(%s) = %i" % (o, any(o)))
all([True, True, True]) = 1
any([True, True, True]) = 1
all([True, False, True]) = 0
any([True, False, True]) = 1

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 [42]:
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 [43]:
print(bool(""))
print(bool("0"))
False
True

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

In [44]:
print(bool([]))
print(bool(()))
print(bool({0}))
# pokud chceme otestovat jednotlivé prvky, musíme použít all nebo any
print(all({0}))
False
False
True
False

toto už možná není zcela intuitivní

In [45]:
o = []
print("all(%s) = %s" % (o, all(o)))
print("any(%s) = %s" % (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. Šlo by nějak napravit bug, který jsme možná odhalili?

In [ ]:
 

Komentáře

Comments powered by Disqus