Cvičení č. 12 rozšířené o poznámky ze cvičení a řešení některých příkladů (ZS 2024/25)¶

V minulém díle jste viděli...¶

Základní kontejnery jsou:

  • str (string neboli textový řetězec) – posloupnost znaků
  • list (seznam) – modifikovatelná (mutable) posloupnost libovolných objektů
  • tuple (n-tice) – nemodifikovatelná (immutable) posloupnost libovolných objektů
  • set (množina) – datová struktura obsahující objekty, pro které není určené jejich vzájemné pořadí
  • dict (slovník) – datová struktura, která představuje zobrazení z množiny klíčů (keys) na libovolné hodnoty (values)

Základní operace s kontejnery jsou:

  1. testování: var in container – výraz s hodnotou True/False
  2. iterování: for var in container

Seznam zapisujeme pomocí hranatých závorek a n-tice pomocí kulatých závorek:

In [3]:
# posloupnosti:
my_list  = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
my_tuple = (10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
my_string = "abcde"

print(my_list, my_tuple, my_string, sep ="\n" )

# další kontejnery:
my_set = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 10, 10}
my_dict = {10: "a", 20: "a", 30: "a", 40: "a", 50: "a", 60: "a", 70: "a", 80: "a", 90: "a", 100: "a"}
print(my_set)
print(my_dict)
[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
(10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
abcde
{100, 70, 40, 10, 80, 50, 20, 90, 60, 30}
{10: 'a', 20: 'a', 30: 'a', 40: 'a', 50: 'a', 60: 'a', 70: 'a', 80: 'a', 90: 'a', 100: 'a'}

Pro přístup k jednotlivým prvkům posloupnosti používáme operátor [] a celočíselnou hodnotu:

In [6]:
print(my_list[0])
print(my_list[-1])
print(my_list[2:-2:2])

print(my_string[::-1])

len(my_list)
for x in my_string:
    print(x, end = " ")

my_list[0] = 457
print(my_list)
#my_tuple[0] = 45 # chyba
45
100
[30, 50, 70]
edcba
a b c d e [457, 20, 30, 40, 50, 60, 70, 80, 90, 100]
In [19]:
a = list(my_string)
print(a)

b = dict([(1,3), (2,4)])
print(b)
['a', 'b', 'c', 'd', 'e']
{1: 3, 2: 4}

Množiny¶

Množina (set) je datová struktura, která se od posloupností liší v tom, že prvky množiny nemají určené pořadí a daná hodnota se v množině může vyskytovat nejvýše jednou. Kvůli kontrole unikátnosti je na objekty, které vkládáme do množiny, kladen dodatečný požadavek – musí být hashovatelné (hashable). Většina nemodifikovatelných (immutable) objektů v Pythonu jsou hashovatelné, modifikovatelné kontejnery hashovatelné nejsou.

Podobně jako v případě seznamu a n-tice má Python dva množinové typy: set (modifikovatelná/mutable množina) a frozenset (nemodifikovatelná/immutable množina).

Množina se typicky používá v algoritmech, kde je potřeba zajistit unikátnost prvků. Pokud algoritmus formulujeme matematicky, odpovídá set přímo matematickému pojmu množina.

V kódu jazyka Pythonu zapisujeme množiny pomocí složených závorek { a }, podobně jako v matematice. Objekty typu set mají dvě hlavní metody: add pro přidání prvku a remove pro odebrání prvku. Kompletní přehled dostupných metod je možné najít v dokumentaci.

Příklad: určení počtu unikátních znaků v textovém řetězci

In [8]:
mnozina = {1, 3, 2, 4, 5, 3, 2}
print(mnozina)

# množina ze seznamu ... odstraní duplicity, nezachová pořadí
seznam = [1,3,2,5,2,2,2]
mnozina = set(seznam)
print(mnozina)

mnozina = set("abababa")
print(mnozina)

# prázdná množina:
mnozina = set()
print(mnozina)

mnozina = {1, 3, 2, 4, 5, 3, 2}
print(mnozina)

# podmínka: je prvek v množině?
if 1 in mnozina:
    print("je tam")

# for-cyklus přes prvky množiny:
for x in mnozina:
    print(x, end = " ")

# počet prvků
print(len(mnozina))
{1, 2, 3, 4, 5}
{1, 2, 3, 5}
{'a', 'b'}
set()
{1, 2, 3, 4, 5}
je tam
1 2 3 4 5 5
In [11]:
mnozina = {1,2,(1,2)}
print(mnozina)
mnozina = {1,2,[1,2]} # chyba
print(mnozina)
mnozina = {1,2,{1,2}} # chyba
print(mnozina)
{1, 2, (1, 2)}
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[11], line 3
      1 mnozina = {1,2,(1,2)}
      2 print(mnozina)
----> 3 mnozina = {1,2,[1,2]} # chyba
      4 print(mnozina)
      5 mnozina = {1,2,{1,2}} # chyba

TypeError: unhashable type: 'list'
In [26]:
mnozina = {1, 3, 2, 4, 5, 3, 2}
print(mnozina)

# přidání prvku do množiny
mnozina.add(6)
mnozina.add(6)
print(mnozina)

# smazání prvku z množiny
mnozina.remove(3)
#mnozina.remove(3)
print(mnozina)
{1, 2, 3, 4, 5}
{1, 2, 3, 4, 5, 6}
{1, 2, 4, 5, 6}
In [41]:
# vstupní data
string = "Hello, world!"

# vytvoření prázdné množiny
unique_chars = set()

# výpočet
for c in string:
    # vložení znaku do množiny (pokud tam ještě není)
    unique_chars.add(c)

# výpis
print("unikátní znaky:", unique_chars)
print("počet unikátních znaků je", len(unique_chars))
unikátní znaky: {',', 'r', 'o', ' ', 'w', '!', 'e', 'l', 'd', 'H'}
počet unikátních znaků je 10
In [42]:
# vstupní data
string = "Hello, world!"

# vytvoření prázdné množiny
unique_chars = set(string)

# výpis
print("unikátní znaky:", unique_chars)
print("počet unikátních znaků je", len(unique_chars))
unikátní znaky: {',', 'r', 'o', ' ', 'w', '!', 'e', 'l', 'd', 'H'}
počet unikátních znaků je 10

S objekty set a frozenset je také možné provádět množinové operace v matematickém smyslu. V následujícím přehledu předpokládáme, že proměnné a a b označují množiny:

  • a <= b – kontrola podmnožiny ($a \subseteq b$)
  • a < b – kontrola podmnožiny ($a \subset b$)
  • a >= b – kontrola nadmnožiny ($a \supseteq b$)
  • a > b – kontrola nadmnožiny ($a \supset b$)
  • a | b | ... – sjednocení množin ($a \cup b \cup \ldots$)
  • a & b & ... – průnik množin ($a \cap b \cap \ldots$)
  • a - b - ... – množinový rozdíl (vrátí prvky, které jsou v a, ale ne b ani v ...)
  • a ^ b – symetrický množinový rozdíl (vrátí prvky, které jsou buď v a nebo b, ale ne v obou současně)
In [12]:
a = {1,3,4}
b = {3, 6}
print(a == a, a != b, a <= b, b > a, a | b, a & b, a - b, a ^ b)
True True False False {1, 3, 4, 6} {3} {1, 4} {1, 4, 6}

Příklad¶

Napište funkci sudoku_line_check(line), která zkontroluje, zda předaný seznam celých čísel reprezentuje správný řádek vyplněného Sudoku, tj. obsahuje pouze čísla 1 až 9 a každé z nich právě jedenkrát.

In [45]:
def sudoku_line_check(line):
    if len(line) != 9:
        return False
    mnozina = set(line)
    if mnozina != {1, 2, 3, 4, 5, 6, 7, 8, 9}:
        return False
    return True

print(sudoku_line_check([1, 2, 8, 9, 3, 5, 6, 7, 4])) 
assert sudoku_line_check([1, 2, 8, 9, 3, 5, 6, 7, 4]) == True
assert sudoku_line_check([1, 2, 8, 9, 3, 5, 7, 4]) == False
assert sudoku_line_check([1, 1, 2, 8, 9, 3, 5, 7, 4]) == False
assert sudoku_line_check([1, 6, 1, 2, 8, 9, 3, 5, 7, 4]) == False
assert sudoku_line_check([0, 1, 2, 3, 4, 5, 6, 7, 8]) == False
True
In [14]:
def sudoku_line_check(line):
    sudoku_set = set()
            
    for x in line:
        if isinstance(x, int) and (1 <= x <= 9) and (x not in sudoku_set):
            sudoku_set.add(x)
        else:
            return False
            
    if len(sudoku_set) != 9:   
            return False
    return True    

assert sudoku_line_check([1, 2, 8, 9, 3, 5, 6, 7, 4]) == True
assert sudoku_line_check([1, 2, 8, 9, 3, 5, 7, 4]) == False
assert sudoku_line_check([1, 1, 2, 8, 9, 3, 5, 7, 4]) == False
assert sudoku_line_check([1, 6, 1, 2, 8, 9, 3, 5, 7, 4]) == False
assert sudoku_line_check([0, 1, 2, 3, 4, 5, 6, 7, 8]) == False

Slovníky¶

Slovník (dict) je modifikovatelná (mutable) datová struktura, která představuje zobrazení z množiny klíčů (keys) na libovolné hodnoty (values). Klíče slovníku musí být unikátní, ale hodnoty se mohou opakovat. Klíče tedy podléhají stejným požadavkům, jako množina – musí být hashovatelné.

Pro zápis slovníků v kódu se také používají složené závorky, ale se syntaktickým rozdílem kvůli mapování klíčů na hodnoty. Každý prvek slovníku je dvojice hodnot klíč-hodnota, které jsou oddělené dvojtečkou:

In [23]:
slovník = {1: "a", 2: "bc", 7: "hello"}
print(slovník)
{1: 'a', 2: 'bc', 7: 'hello'}
In [20]:
zamestnanec = {"jmeno": "Petr", "prijmeni": "Novak", "mzda": 50000, "rodne prijmeni": "Novak"}
print(zamestnanec)

sachovnice = {("a", 1): "bílá věž", ("d", 2): "černý kůn"}
print(sachovnice[("a", 1)])
print(sachovnice["a", 1])
sachovnice["c", 2] = "černý král"
print(sachovnice)
{'jmeno': 'Petr', 'prijmeni': 'Novak', 'mzda': 50000, 'rodne prijmeni': 'Novak'}
bílá věž
bílá věž
{('a', 1): 'bílá věž', ('d', 2): 'černý kůn', ('c', 2): 'černý král'}
In [15]:
zamestnanec = {"jmeno": "Petr", "prijmeni": "Novak", "mzda": 50000, "rodne prijmeni": "Novak"}
print(zamestnanec)

# získání hodnoty pro daný klíč:
x = zamestnanec["jmeno"]
print(x)

# změna hodnoty ve slovníku:
zamestnanec["mzda"] += 20000
print(zamestnanec)

# změna hodnoty ve slovníku nebo přidání nového prvku do slovníku:
zamestnanec["zarazeni"] = "ucetni"
print(zamestnanec)

# smazání prvku ve slovníku
del zamestnanec["mzda"]
print(zamestnanec)
{'jmeno': 'Petr', 'prijmeni': 'Novak', 'mzda': 50000, 'rodne prijmeni': 'Novak'}
Petr
{'jmeno': 'Petr', 'prijmeni': 'Novak', 'mzda': 70000, 'rodne prijmeni': 'Novak'}
{'jmeno': 'Petr', 'prijmeni': 'Novak', 'mzda': 70000, 'rodne prijmeni': 'Novak', 'zarazeni': 'ucetni'}
{'jmeno': 'Petr', 'prijmeni': 'Novak', 'rodne prijmeni': 'Novak', 'zarazeni': 'ucetni'}
In [17]:
# získání hodnoty pro daný klíč:
y = zamestnanec.get("jmeno")
print(y)
x = zamestnanec["jmeno"]
print(x)

y = zamestnanec.get("jmeno1") # vrátí None
print(y)
x = zamestnanec["jmeno1"]     # zhavaruje (KeyError)
print(x)
Petr
Petr
None
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[17], line 9
      7 y = zamestnanec.get("jmeno1") # vrátí None
      8 print(y)
----> 9 x = zamestnanec["jmeno1"]     # zhavaruje (KeyError)
     10 print(x)

KeyError: 'jmeno1'

Důležitá otázka: Je proměnná var v následujícím kódu prázdná množina nebo prázdný slovník? Jak můžeme získat to druhé?

In [21]:
var = {}
print(type(var))
var = set()
print(type(var))
var = dict()
print(type(var))
var = list()
print(type(var))
var = ()
print(type(var))


s = {}
s[0] = 5
print(s)
<class 'dict'>
<class 'set'>
<class 'dict'>
<class 'list'>
<class 'tuple'>
{0: 5}

Jak bylo zmíněno v dřívější sekci, dvě základní operace (testování a iterování) jsou dostupné pro všechny kontejnery, tedy i pro slovníky. Je však důležité si uvědomit, že v obou případech se operace provádí na množině klíčů:

In [23]:
slovník = {1: "a", 2: "b", 3: "c"}
print(1 in slovník)     # slovník obsahuje klíč s hodnotou 1
print("a" in slovník)   # slovník neobsahuje klíč s hodnotou "a"
True
False

Iterování přes slovník pomocí for cyklu vrací implicitně pouze klíče:

In [64]:
# výpis klíčů:
for i in slovník:
    print(i)
1
2
3
In [67]:
# výpis klíčů:
for i in slovník.keys():
    print(i)
slovník.keys()
1
2
3
Out[67]:
dict_keys([1, 2, 3])
In [24]:
# výpis hodnot:
for i in slovník.values():
    print(i)
list(slovník.values())
a
b
c
Out[24]:
['a', 'b', 'c']
In [71]:
for i in slovník.items():
    print(i)
list(slovník.items())
(1, 'a')
(2, 'b')
(3, 'c')
Out[71]:
[(1, 'a'), (2, 'b'), (3, 'c')]
In [89]:
for key, value in slovník.items():
    print(key, value)
list(slovník.items())
1 a
2 b
3 c
Out[89]:
[(1, 'a'), (2, 'b'), (3, 'c')]

Pokud chceme v kódu explicitně vyznačit iterování přes množinu klíčů, můžeme místo celého slovníku jako zdroj dat použít přístupovou metodu keys: for i in slovník.keys(): a chování bude ekvivalentní předchozímu příkladu. Také se hodí lépe pojmenovat iterační proměnnou (např. key místo i).

Pokud chceme ze slovníku získat hodnotu pro příslušný klíč, můžeme použít indexovací operátor []:

In [10]:
for key in slovník.keys():
    value = slovník[key]  # získání hodnoty pro daný klíč
    print(key, value)
1 a
2 b
3 c

Použití klíče a odpovídající hodnoty ve for cyklu je velmi časté a příkaz pro přístup k hodnotě odpovídající danému klíči by se zbytečně často opakoval. Proto mají slovníky v Pythonu přístupovou metodu items, která vrací dvojice (key, value). V příkazu for se často používá rozbalení do dvojice proměnných, což nám umožní předchozí příklad o jeden řádek zkrátit:

In [ ]:
for key, value in slovník.items():
    print(key, value)
1 a
2 b
3 c

Pokud ve for cyklu chceme zpracovat jen hodnoty ze slovníku a klíče by byly zbytečné, můžeme použít přístupovou metodu values. V případě, že slovník obsahuje stejnou hodnotu pro několik různých klíčů, bude se zde opakovat (hodnoty slovníku netvoří množinu).

In [11]:
for value in slovník.values():
    print(value)
a
b
c

Další operace se slovníky¶

Rozhraní pro práci se slovníky je odlišné od posloupností. Kompletní přehled je možné najít v dokumentaci, zde uvedeme jen nejdůležitější operace. Proměnná d v tomto přehledu představuje nějaký slovník, key označuje proměnnou s významem klíče a value představuje libovolnou hodnotu.

  • len(d) – počet prvků (klíčů) ve slovníku
  • d[key] – přístup k hodnotě odpovídající danému klíči (musí ve slovníku existovat, jinak je to chyba)
  • d[key] = value – zápis hodnoty pro daný klíč do slovníku (buď vložení nové hodnoty nebo přepsání existující hodnoty)
  • del d[key] – odstranění klíče (a odpovídající hodnoty) ze slovníku
  • d.get(key) – vrátí hodnotu odpovídající danému klíči, pokud ve slovníku existuje, jinak vrátí None
  • d.get(key, default) – vrátí odpovídající danému klíči, pokud ve slovníku existuje, jinak vrátí default
In [91]:
print(slovník)
print(slovník.get(2))
print(slovník.get(25))
{1: 'a', 2: 'b', 3: 'c'}
b
None

Příklad¶

Napište funkci, která spočítá počet dětí ($<18$ let), dospělých (18-64 let) a seniorů ($\ge65$ let) v zadaném seznamu:

In [84]:
people = [
    {"name": "Bob", "age": 72},
    {"name": "Ali", "age": 18},
    {"name": "Tim", "age": 65},
    {"name": "Tom", "age": 50},
    {"name": "Joe", "age": 40},
    {"name": "Eve", "age": 30},
    {"name": "Tom", "age": 25},
    {"name": "Kia", "age": 5},
    {"name": "Pam", "age": 7},
    {"name": "May", "age": 17},
    {"name": "Liz", "age": 29},
    {"name": "Dom", "age": 68},
    {"name": "Sam", "age": 36},
]

def count_people(d):
    pocet_deti = 0
    pocet_dospelych = 0
    pocet_senioru = 0
    for person in d:
        if person["age"]< 18:
            pocet_deti += 1
        elif person["age"] >= 64:
            pocet_senioru += 1
        else:
            pocet_dospelych += 1
    return pocet_deti, pocet_dospelych, pocet_senioru

count_people(people)  # funkce vrátí trojici čísel nebo slovník se třemi kategoriemi DVE FUNKCE

def count_people(d):
    pocet_deti = 0
    pocet_dospelych = 0
    pocet_senioru = 0
    for person in d:
        if person["age"]< 18:
            pocet_deti += 1
        elif person["age"] >= 64:
            pocet_senioru += 1
        else:
            pocet_dospelych += 1
    return {"pocet deti": pocet_deti, "pocet dospelych": pocet_dospelych, "pocet senioru": pocet_senioru}

count_people(people)  # funkce vrátí trojici čísel nebo slovník se třemi kategoriemi DVE FUNKCE

def count_people(d):
    slovnik = {"pocet deti": 0, "pocet dospelych": 0, "pocet senioru": 0}
    for person in d:
        if person["age"]< 18:
            slovnik["pocet deti"] += 1
        elif person["age"] >= 64:
            slovnik["pocet senioru"] += 1
        else:
            slovnik["pocet dospelych"] += 1
    return slovnik

count_people(people)  # funkce vrátí trojici čísel nebo slovník se třemi kategoriemi DVE FUNKCE
Out[84]:
{'pocet deti': 3, 'pocet dospelych': 7, 'pocet senioru': 3}

Důležité poznámky a příklady¶

Kontejnery jako argumenty funkcí¶

Kontejnery lze předávat jako argumenty funkcím, stejně jako všechny ostatní objekty. Jediná vlastnost jazyka Python, na kterou je nutné dát pozor, souvisí s definicí parametru s výchozí hodnotou, ve které by se nikdy neměly vyskytovat modifikovatelné kontejnery (např. param=[] nebo param={}). Problematické chování demonstrujeme na následujícím příkladu:

In [86]:
def demo(a=[]):
    a.append(5)
    print(a)
demo([1,2,3])
demo([1,3])
demo()
demo()
demo()
[1, 2, 3, 5]
[1, 3, 5]
[5]
[5, 5]
[5, 5, 5]
In [25]:
def demo(a=[]):
    a.append(5)
    print(a)
    return a

b = demo()
c = demo()
d = demo()
tu = b, c, d
b += (6,)
tu
[5]
[5, 5]
[5, 5, 5]
Out[25]:
([5, 5, 5, 6], [5, 5, 5, 6], [5, 5, 5, 6])

Problém spočívá v tom, že příkaz def definující funkci demo se provede jednou, přičemž vznikne právě jeden defaultní objekt (prázdný seznam []) pro parametr a. Poté se funkce demo volá třikrát bez parametru, tj. se stejným defaultním objektem. Dodejme, že v případě nemodifikovatelných kontejnerů (např. tuple nebo str) tento problém není možné pozorovat, protože jediný defaultní objekt pro daný parametr není možné modifikovat.

Jak spravit předchozí příklad? Běžně se používá tento přístup: v definici funkce použijeme objekt None pro odlišení, jestli byl parametr specifikován nebo ne, a potřebný modifikovatelný kontejner vytvoříme uvnitř funkce:

In [87]:
def demo2(a=None):
    # kontrola parametru a vytvoření kontejneru
    if a is None:
        a = []

    # zbytek funkce
    a.append(5)
    print(a)

demo2()
demo2()
demo2()
[5]
[5]
[5]

Mutable vs. immutable objekty¶

Kontejnery v Pythonu se podle svých vlastností dělí na:

  1. mutable (modifikovatelné) – např. list, set, dict
  2. immutable (nemodifikovatelné) – např. str, tuple, frozenset

Modifikovatelné typy mají metody, které umožňují modifikovat daný objekt (např. přidávat nebo odebírat prvky v kontejneru). Naopak nemodifikovatelné typy nemají žádné metody, které by modifikovaly daný objekt – jediný způsob, jak dosáhnout modifikace, je tedy vytvoření nového objektu.

Identita vs hodnota objektů¶

Pro objekty nemodifikovatelných typů je důležitá jen jejich hodnota. Pro objekty modifikovatelných typů je navíc důležitá jejich identita, neboli umístění v paměti počítače. Identity a hodnoty objektů můžeme porovnávat:

  • operátory is a is not porovnávají identitu dvou objektů (např. dvě proměnné jsou identické, pokud se odkazují na stejný objekt v paměti počítače)
  • operátory == a != porovnávají hodnoty dvou objektů (identické objekty mají vždy stejnou hodnotu, ale dva neidentické objekty mohou a nemusí mít stejnou hodnotu)

Přiřazovací operátor¶

Pro pochopení důsledků modifikovatelné vlastnosti kontejnerů je důležité si uvědomit, jak funguje přiřazovací operátor. Každá proměnná se skládá z názvu (identifikátoru) v kódu programu a z odkazu na objekt, který je uložený v paměti počítače a který nese samotná data objektu. Přiřazovací operátor = v Pythonu vždy mění jen odkaz pro danou proměnnou a nikdy nevytváří kopie objektu na jiném místě v paměti počítače.

Demonstrační příklady:

In [40]:
# DOSLI JSME SEM, PRED TENTO PRIKLAD

# přiřazení číselných hodnot
a = 1
b = a
print(a is b)  # `a` a `b` jsou identické

# "modifikace" číselné hodnoty - všechny číselné typy jsou **immutable**
b += 1  # vytvoří nový objekt - ekvivalentní `b = b + 1`
print("a =", a)  # vypíše 1
print("b =", b)  # vypíše 2
print(a is b)  # `a` a `b` nejsou identické

# porovnání operátorů `is` a `==`
a = 1.0
b = 2 / 2
print("a =", a)  # vypíše 1.0
print("b =", b)  # vypíše 1.0
print("is:", a is b)  # `a` a `b` nejsou identické (ale mají stejnou hodnotu)
print("==", a == b)

# porovnání operátorů `is` a `==`
a = 1 + 1
b = 2
print("a =", a)  # vypíše 1.0
print("b =", b)  # vypíše 1.0
print("is:", a is b)  # `a` a `b` nejsou identické (ale mají stejnou hodnotu)
print("==", a == b)
True
a = 1
b = 2
False
a = 1.0
b = 1.0
is: False
== True
a = 2
b = 2
is: True
== True
In [42]:
# přiřazení mutable objektů
a = [0, 1]
b = a
print(a is b)  # `a` a `b` jsou identické

# modifikace kontejneru
b.append(2)
b[0] = 10
print("a =", a)  # vypíše [0, 1, 2]
print("b =", b)  # vypíše [0, 1, 2]
print(a is b)  # `a` a `b` jsou stále identické!
True
a = [10, 1, 2]
b = [10, 1, 2]
True
In [45]:
# přiřazení immutable objektů
a = (0, 1)
b = a
print(a is b)  # `a` a `b` jsou identické

# modifikace kontejneru
b += (2,)
a += (2,)
print("a =", a)  # vypíše [0, 1, 2]
print("b =", b)  # vypíše [0, 1, 2]
print(a is b)  # `a` a `b` jsou stále identické!
True
a = (0, 1, 2)
b = (0, 1, 2)
False

Předchozí příklad používá seznam (list) pro ukázku, ale stejné chování platí všechny mutable objekty, včetně množin a slovníků (set a dict).

Naopak pro immutable kontejnery podobný případ nastat nemůže – objekt nelze modifikovat přímo, pro změnu bychom museli vytvořit nový objekt a použít přiřazovací operátor, což by však rozpojilo vazbu dvou proměnných na jeden objekt.

Předávání argumentů funkci¶

Pro předávání argumentů při volání funkce platí stejné chování jako v případě přiřazovacího operátoru – předává se jen odkaz na objekt a nevytváří se kopie objektu na jiném místě v paměti počítače.

Důsledek: modifikace objektu uvnitř funkce se projeví i navenek! (Záleží na situaci, jestli je tento efekt žádoucí nebo nežádoucí...)

In [47]:
def add_to_dict(d, key, value):
    if key in d:
        print("key", key, "already exists")
    else:
        d[key] = value
        print("added key", key)

people_with_age = {
    "Alice": 20,
    "Bob": 21,
}
add_to_dict(people_with_age, "John", 22)
print(people_with_age)
add_to_dict(people_with_age, "John", 26)
print(people_with_age)
added key John
{'Alice': 20, 'Bob': 21, 'John': 22}
key John already exists
{'Alice': 20, 'Bob': 21, 'John': 22}

Vytváření kopií mutable kontejnerů¶

Jelikož přiřazovací operátor (např. a = b) pouze nastaví odkaz na stejný objekt, pro vytvoření skutečné kopie kontejneru jako list, set a dict je nutné postupovat opatrně. Můžeme např. vytvořit prázdný kontejner a postupně do něj přidat všechny prvky z původního kontejneru:

In [52]:
a = [0, 1]

b = a.copy()
b = a[:]
b = list(a)

b.append(2)
print(a)  # prints [0, 1]
print(b)  # prints [0, 1, 2]
[0, 1]
[0, 1, 2]
In [61]:
a = {0, 1}

b = set(a)

b.add(2)
print(a)  # prints [0, 1]
print(b)  # prints [0, 1, 2]
{0, 1}
{0, 1, 2}
In [64]:
a = {0:"a", 1:"b"}

b = dict(a)
b = a.copy()

b["n"] ="a"
print(a)  # prints [0, 1]
print(b)  # prints [0, 1, 2]
{0: 'a', 1: 'b'}
{0: 'a', 1: 'b', 'n': 'a'}
In [48]:
def copy_list(source):
    """ Creates a new list containing all elements of the source.
        Does **not** modify the parameter.
    """
    copy = []
    for element in source:
        copy.append(element)
    return copy

a = [0, 1]
b = copy_list(a)
b.append(2)
print(a)  # prints [0, 1]
print(b)  # prints [0, 1, 2]
[0, 1]
[0, 1, 2]

Tip: Výraz b = a[:] také vytváří kopii. Jde o slicing s defaultní hodnotou pro počátek i konec rozsahu, tj. od začátku po konec posloupnosti.

Příklad: naprogramujte analogické funkce pro kopírování množiny a slovníku.

In [58]:
def copy_set(source):
    copy = set() # {}
    for x in source:
        copy.add(x)
    return copy

def copy_dict(source):
    copy = {}
    for key in source:
        copy[key] = source[key]
    return copy

def copy_dict(source):
    copy = {}
    for key, value in source.items():
        copy[key] = value
    return copy

s1 = {0, 1}
s2 = copy_set(s1)
s1.add(2)
assert s1 == {0, 1, 2}
assert s2 == {0, 1}

d1 = {"a": 0, "b": 1}
d2 = copy_dict(d1)
d1["c"] = 2
assert d1 == {"a": 0, "b": 1, "c": 2}
assert d2 == {"a": 0, "b": 1}

Příklad: naprogramujte obecnou funkci copy, která vytvoří kopii kontejneru správným způsobem podle toho, o jaký typ kontejneru se jedná.

Typ objektu je možné ověřit buď pomocí funkce isinstance (if isinstance(obj, list): atd.) nebo pomocí funkce type (if type(obj) == list).

In [27]:
s = {3, 6}
print(isinstance(s, list))
isinstance(s, set)
False
Out[27]:
True
In [60]:
def copy(source):
    if isinstance(source, list):
        copy = []
        for element in source:
            copy.append(element)
        return copy

    elif isinstance(source, set):
        copy = set()
        for key in source:
            copy.add(key)
        return copy

    elif isinstance(source, dict):
        copy = {}
        for key, value in source.items():
            copy[key] = value
        return copy

    else:
        # pokud nevíme, jak vytvořit kopii, vrátíme alespoň odkazu
        return source

a = [0, 1]
b = copy(a)
a.append(2)
assert b == [0, 1]

s1 = {0, 1}
s2 = copy(s1)
s1.add(2)
assert s2 == {0, 1}

d1 = {"a": 0, "b": 1}
d2 = copy(d1)
d1["c"] = 2
assert d2 == {"a": 0, "b": 1}

Vytváření hluboké kopie¶

Funkce z předchozí sekce vytváří tzv. mělké kopie – nový objekt vznikne jen pro kontejner nejvyšší úrovně a pro prvky tohoto kontejneru se přiřadí jen odkazy:

In [71]:
s1 = [[0, 1], [2, 3]]

#s2 = copy(s1)
s2 = s1[:]
s2.append([4])  # modifikuje jen s2

s2[0][0]=10
s2[0].append(42)  # modifikuje prvek s2 i s1!
print(s1)
print(s2)
print(s1[0] is s2[0])
[[10, 1, 42], [2, 3]]
[[10, 1, 42], [2, 3], [4]]
True

Pro vytvoření tzv. hluboké kopie je potřeba postupovat rekurzivně a pro každý mutable objekt, na který narazíme, vytvořit kopii:

In [74]:
def deep_copy(source):
    """ Vytváří hlubokou kopii zdrojového kontejneru. Nemění parametr `source`.
    """
    if isinstance(source, list):
        # nejprve vytvoříme prázdný kontejner
        copy = []
        # poté projdeme všechny prvky
        for element in source:
            # vložíme hlubokou kopii
            #copy.append(element)
            copy.append(deep_copy(element))
        # vrátíme výsledek
        return copy
    else:
        return source
s1 = [[0, [1, 2]], [2, 3]]

s2 = deep_copy(s1)

s2.append([4])  # modifikuje jen s2
s2[0][1][0]=10
s2[0].append(42)  # modifikuje prvek s2 i s1!

print(s1)
print(s2)
print(s1[0] is s2[0])
[[0, [1, 2]], [2, 3]]
[[0, [10, 2], 42], [2, 3], [4]]
False
In [28]:
def deep_copy(source):
    """ Vytváří hlubokou kopii zdrojového kontejneru. Nemění parametr `source`.
    """
    if isinstance(source, list):
        # nejprve vytvoříme prázdný kontejner
        copy = []
        # poté projdeme všechny prvky
        for element in source:
            # vložíme hlubokou kopii
            copy.append(deep_copy(element))
        # vrátíme výsledek
        return copy

    elif isinstance(source, set):
        copy = set()
        for element in source:
            # vložíme hlubokou kopii      
            copy.add(deep_copy(element))
        return copy

    elif isinstance(source, dict):
        copy = {}
        for key in source:
            # vložíme hlubokou kopii
            copy[key] = deep_copy(source[key])
        return copy

    else:
        # pokud source není mutable kontejner, vrátíme odkaz
        return source

def modify(source):
    """ Pomocná funkce pro testování. **Mění parametr `source`.**
    """
    # nejprve přidáme nový prvek do kontejneru
    if isinstance(source, list):
        source.append(len(source))
    elif isinstance(source, set):
        for i in range(len(source) + 1):
            if i not in source:
                source.add(i)
                break
    elif isinstance(source, dict):
        for i in range(len(source.keys()) + 1):
            if i not in source:
                source[i] = i
                break

    # poté modifikujeme všechny prvky
    if isinstance(source, list) or isinstance(source, set):
        for element in source:
            modify(element)
    elif isinstance(source, dict):
        for key, value in source.items():
            modify(value)

a = [
    [0, 1],
    {
        "a": {1, 2},
        "b": [],
    },
    {0, 42},
]
b = deep_copy(a)
assert a == b
modify(a)
print(a)
print(b)
assert b == [
    [0, 1],
    {
        "a": {1, 2},
        "b": [],
    },
    {0, 42},
]
[[0, 1, 2], {'a': {0, 1, 2}, 'b': [0], 0: 0}, {0, 1, 42}, 3]
[[0, 1], {'a': {1, 2}, 'b': []}, {0, 42}]

Další zajímavé vlastnosti¶

Funkce s obecným počtem parametrů¶

Syntaxe jazyka Python umožňuje definovat funkce s obecným počtem pozičních a pojmenovaných parametrů/argumentů. Obecně může definice funkce vypadat takto:

In [76]:
help(print)
Help on built-in function print in module builtins:

print(*args, sep=' ', end='\n', file=None, flush=False)
    Prints the values to a stream, or to sys.stdout by default.

    sep
      string inserted between values, default a space.
    end
      string appended after the last value, default a newline.
    file
      a file-like object (stream); defaults to the current sys.stdout.
    flush
      whether to forcibly flush the stream.

In [82]:
def f(a, b, c, *args, k1, k2, **kwargs):
    print(a, b, c, k1, k2)
    print("args:", args)
    print("kwargs:", kwargs)
    print()

Význam jednotlivých parametrů je následující:

  • a, b, c jsou poziční parametry
  • parametr označený hvězdičkou, tedy args, je tuple obsahující všechny další poziční argumenty
  • k1, k2 jsou definovány až za *args, takže je lze specifikovat jen jako pojmenované argumenty
  • parametr označený dvěma hvězdičkami, tedy kwargs, je slovník obsahující všechny další pojmenované argumenty (keyword arguments)

Názvy parametry args a kwargs nejsou vynucené, mohli bychom použít libovolné jiné názvy. Pro přehlednost kódu je však dobré držet se zaběhnuté konvence.

Funkci f můžeme zavolat např. takto:

In [86]:
f(0, 1, 2, k1=10, k2=20)
f(0, 1, 2, 3, k1=10, k2=20, k3=30)
f(0, 1, 2, 3, 4, 5, 6,  k1=10, k2=20, hello="world", aaa=56 )
0 1 2 10 20
args: ()
kwargs: {}

0 1 2 10 20
args: (3,)
kwargs: {'k3': 30}

0 1 2 10 20
args: (3, 4, 5, 6)
kwargs: {'hello': 'world', 'aaa': 56}

In [29]:
def f(*args):
    for x in args:
        print(x, end = " ")
    print("")
f(1, 3, 5)
f(2)
f(*[4, "a"])
1 3 5 
2 
4 a 
In [108]:
def f(**kwargs):
    for x, value in kwargs.items():
        print(x, value)
    print("")
f(a=1, b=3, c=5)
f(a=2)

d = {"a": 1, "b": 2}
f(d=d)
f(**d)
a 1
b 3
c 5

a 2

d {'a': 1, 'b': 2}

a 1
b 2

In [89]:
def f(*args):
    print(len(args))
f(1, 3, 5)
f(2)
3
1

Rozbalovací hvězdička a dvojhvězdička¶

V předchozí sekci jsme viděli, že operátory * a ** označují v definici funkce obecné parametry pro poziční a pojmenované argumenty. Co když ale máme v kódu kontejnery, např. seznam nebo slovník, které chceme předat takové funkci?

In [97]:
def print_sequence(*args):
    for item in args:
        print(item)

print_sequence(1, 3, 2, 3)
s = [0, 1, 2]
print_sequence(s, 6, 7)
1
3
2
3
[0, 1, 2]
6
7
In [104]:
def print_sequence(*args):
    for item in args:
        print(item)

s = [0, 1, 2]
t = {2, 3}
u = [0, 1, *s, *t, 2]
print(u)
#print_sequence(*s, *t)
d = {"a": 1, "b": 2}
[0, 1, 0, 1, 2, 2, 3, 2]

V předchozím příkladu máme seznam s, který předáváme funkci print_sequence. Jak vypadá v tomto případě args? Je to tuple délky 1, kde první prvek je seznam s (ověřte to pomocí nástroje pytutor).

Toto použití je naprosto funkční, ale možná neodpovídá tomu, co programátor zamýšlel. Jak můžeme docílit toho, aby args odpovídal přímo seznamu s, tj. aby cyklus uvnitř funkce iteroval přes hodnoty seznamu s? K tomu nám pomůže tzv. rozbalovací hvězdička:

In [22]:
print_sequence(*s)
0
1
2

Pokud v kódu použijeme operátor * před názvem nějaké posloupnosti, způsobí rozbalení obsažených prvků na daném místě. V předchozím příkladu je print_sequence(s) ekvivalentní print_sequence([0, 1, 2]), ale print_sequence(*s) je ekvivalentní print_sequence(0, 1, 2).

Rozbalování posloupností není svázáno jen s voláním funkcí. Je to obecný prvek jazyka Python, který lze použít všude tam, kde lze syntakticky použít seznam prvků:

In [30]:
s1 = [10, 20, *s, 30]
print(s1)
[10, 20, 3, 6, 30]
In [34]:
d = {"a": "ahoj", "b": "čau"}
u = {"c": 15, **d, "d": 14}
print(u)
{'c': 15, 'a': 'ahoj', 'b': 'čau', 'd': 14}

Podobným způsobem funguje rozbalování slovníku pomocí ** – vyzkoušejte sami.

Povinně poziční a povinně pojmenované parametry/argumenty¶

Jazyk Python umožňuje při definici funkce označit, které parametry/argumenty jsou povinně poziční a pojmenované:

  • def f(a, b, c): – všechny parametry lze použít buď jako poziční, nebo jako pojmenované
  • def f(a, b, *, c) – parametr c je povinně pojmenovaný (nelze použít jako poziční, např. f(1, 2, 3))
  • def f(a, /, b, c) – parametr a je povinně poziční (nelze použít jako pojmenovaný, např. f(a=1, b=2, c=3))
  • def f(a, /, b, *, c): – parametr a je povinně poziční a parametr c je povinně pojmenovaný

Vyzkoušejte všechny varianty definice funkce a volání s pozičními/pojmenovanými parametry/argumenty:

In [119]:
def f(*, a, b,  c):
    print("a =", a)
    print("b =", b)
    print("c =", c)

#f(1, 2, 3)
#f(1, b=2, c=3)
#f(1, 2, c=3)
f(a=1, b=2, c=3)
a = 1
b = 2
c = 3