pre up next title end end

3. Postupy a techniky

  1. Iterace a iterátory
  2. Interní index elementu
  3. Úseky sekvencí
    1. Úsekový operátor
    2. Funkce slice()
  4. Pomocné techniky
    1. Hromadné přiřazení
    2. Rozbalení sekvence
    3. Rozbalení slovníku
  5. Komprehence
  6. Generátory
    1. Generátorový výraz
    2. Generátorová funkce
    3. Líný výpočet
  7. Jmenné prostory
    1. Všechno je objekt
    2. Globální a lokální proměnná
    3. Jmenný registr
    4. Scope jména
  8. Proměnná   " name "
  9. Introspekce Pythonu
  10. Testování s doctestem
  11. Glosář
  12. Cvičení

3.1 Iterace a iterátory

Iterace
je opakovaný výpočet pro hodnoty jednotlivých členů iterovatelného objektu, neboli iteráblu.. Iterovatelným objektem (iteráblem) jsou sekvence typu string, list, tuple, range, bytes, bytearray, memoryview a kolekce typu set, frozenset, dict.

Iterace iteráblu produkuje 'dychtivý' (eager) výstup, iterace iterátoru produkuje 'líný' (lazy) výstup, viz odst. 3.6.3.

Iterábl
je iterovatelný objekt typu list, tuple, range(), set, frozenset (neměnitelná verze setu), dict a string (formálně také bytes, bytearray a memoryview), který vlastní magickou (dunder) metodu __iter__() a/nebo __getitem__(). Přítomnost těchto metod v objektu lze zjistit funkcí dir(iterable).

Iterátor
je interní objekt, vlastnící vestavěnou funkci iter() a next(). Explicitně jej vytvoříme z iteráblu funkcí iter(iterable) nebo metodou iterable.__iter__(); samotný iterátor je rovněž iteráblem.
Implicitně si iterátor pro zadaný iterábl vytváří smyčka for... a funkce list(), open(), min(), max(), map(), filter(), zip(), enumerate() a reversed(). Implicitně je iterátorem rovněž objekt typu file (file = open('some.ext').

Iterátory jsou výhodné tím, že snižují spotřebu paměti; vyprázdněný iterátor je "garbage collected".

Iterátor lze generovat i generátorovou funkcí a generátorovým výrazem - viz odst. 3.6.
Iterátory, definované třídou, jsou popsány v Kap. 10.7.

Provádění iterace

Funkce next(iterator) postupně vrací aktuálně dosažené prvky iterátoru a vypouští je z kopie. Vypuštění posledního elementu kopie je indikováno sdělením StopIteration:

>>> li = [2, 5, 0]            # iterable

>>> ili = iter(li)            # vytvoření iterátoru
>>> ili = li.__iter__()       # alternativní způsob

>>> next(ili)                 # první krok iterace
2
>>> ili.__next__()            # alternativně další krok iterace
5
>>> next(ili)                 # poslední krok iterace
0
>>> next(ili)       
StopIteration                 # info o konci iterace

Iterátor je v této chvíli prázdný. Opětovně použitelný iterátor vytvoříme opětovným použitím funkce iter(), případně metody  __iter__().
To platí i při aplikaci smyčky for .. :

>>> ili = iter(li)
>>> next(ili) 
2
>>> for i in ili:
        print(i, end =" ")
5 0
>>> next(ili)
StopIteration

Nutno poznamenat, že při aplikaci smyčky for ..
pro iterábl li žádné "StopIteration" nehrozí,
neboť si smyčka vytvoří svůj vlastní interní iterátor:

>>> for i in li:
    print(i, end =", ")
2, 5, 0,	

3.2 Interní index elementu

Elementy sekvencí jsou interně indexovány a pomocí těchto indexů jsou přímo dostupné závorkovým operátorem [i] nebo prostřednictvím metody __getitem__(i) - odst. 3.2. __getitem__(i)
Názornou ilustraci indexování těchto elementů představuje obrázek:

indexing

Indexování zleva začíná nulou, indexování zprava doleva se provádní pomocí záporných čísel.:

>>> pozdrav = "Hello World"
>>> pozdrav[5]
' '
>>> pozdrav[-5]
'W'
>>> pozdrav[11]                          
IndexError: string index out of range
>>> pozdrav.__getitem__(0)
'H'

Hodnoty elementů objektu typu dictionary (slovník) jsou přístupné přes klíče slovníku:

>>> dc = {"m": 5, "n": 6}   # objekt s klíči "m", "n"
>>> dc["m"]                 # použití závorkového operátoru
5
>>> dc.__getitem__("n")     # použití metody
6
>>> for i in dc:            # použití indexu ve smyčce
        print(dc[i], end =" ")
5 6		

3.3 Úseky sekvencí

Vymezená část sekvence typu string, list, tuple, set, bytes, range se nazývá úsek (slice) nebo subsekvence. Výběr úseku se provádí jednak úsekovým operátorem (např. [n:m]) nebo funkcí slice(). Operátor vrací úsek řetězce od znaku n včetně (inclusive), před znak m (exclusive).

3.3.1 Výběr úsekovým operátorem

Skladba příkazu: seq[start : end : krok] - uvedené parametry jsou obecně vzato - nepovinné.

Není-li krok zadán, má implicitně hodnotu +1 a skladbu příkazu lze zjednodušit na seq[start : end]. Hodnota parametru end je index prvního elementu, který do zamýšleného úseku již nepatří.

V následující tabulce jsou u všech příkazů zobrazeny orientace deklarovaných úseků. U některých příkazů jsou explicitně rozepsány hodnoty explicitních i implicitních parametrů:

    #  0  1  2  3  4  5  6         indexy  0 ÷  6
seq = [1, 2, 3, 4, 5, 6, 7]        upravovaná sekvence
    # -7 -6 -5 -4 -3 -2 -1         indexy -1 ÷ -7

>>> a = seq[::]; a         # --> [1, 2, 3, 4, 5, 6, 7]                    
    # dtto seq[:]          # 0 : (len(seq)+1) : 1
	
>>> b = seq[2::]; b        # --> [3, 4, 5, 6, 7]
    # dtto seq[2:]         # 2 : (len(seq)+1) : 1            

>>> c = seq[:5:]; c        # --> [1, 2, 3, 4, 5]             
    # dtto seq[:5]         # 0 : 5 : 1          

>>> d = seq[2:6:]; d       # --> [3, 4, 5, 6]
    # dtto seq[2:6]        # 2 : 6 : 1  

>>> e = seq[::2]; e        # --> [1, 3, 5, 7]  
                           # 0 : (len(seq)+1) : 2
                                              
>>> f = seq[::-2]; f       # <-- [7, 5, 3, 1]

>>> g = seq[2:6:2]; g      # --> [3, 5]

>>> h = seq[2:6:-2]; h     # --> [] (prázdná subsekvence)

>>> i = seq[6:2:2]; i      # <-- [] (prázdná subsekvence)

>>> j = seq[2::2]; j       # --> [3, 5, 7]
                           # 2 : (len(seq)+1) : 2

>>> k = seq[8:1:-2]; k     # <-- [7, 5, 3]

>>> l = seq[-4:-2]; l      # --> [4, 5]
                           # -4 : -2 : 1

Poznámka:

Úsek je jako orientovaná úsečka (-->) - má počátek a konec. Není-li směr úseku vyjádřen hodnotou počátku a konce (např. jen :: krok - viz případ e, f), je jeho orientace dána orientací kroku. Orientace kroku je dána jeho kladnou či zápornou hodnotou.
Krok vychází z počátku úseku. Pokud mají úsek a krok opačnou orientaci, je výsledkem prázdný úsek - viz případ h, i


3.3.2 Výběr funkcí slice():

Skladba parametrů funkce slice() má formálně tři parametry: start (inkluzivní index počátku), end (exkluzivní index konce) a krok (viz). Parametry start a krok jsou nepovinné. Nejsou-li zadány, znamená to, že start = 0, krok = 1.
Podle znaménka indexů start, end může být úsek orientován zleva doprava nebo zprava doleva. Stejně tak krok může směřovat vpravo či vlevo - opět v závislosti na kladné či záporné hodnotě kroku.

Působení funkce slice() je zajímavé. Tato funkce vytvoří objekt bez ohledu na zamýšlenou sekvenci:

seq = "Nazdar Pythone!"
slice_obj = slice(3,11)       # opakovaně použitelný objekt

# Tento objekt lze uplatnit na sekvence různých typů:
sub_seq = seq[slice_obj]         
print(sub_seq)                   # --> dar Pyth                

# Případně lze pro jedno použití slice(3,11) dosadit přímo:
sub_seq = seq[slice(3,11)]
print(sub_seq)                   # --> dar Pyth 

Lepší představě o vlivu znamének na vytvoření úseku může posloužit toto schéma indexů pro použitý řetězec:

#        0   1   2   3   4   5   6  7  8  9 10 11 12 13 14
# seq:  "N   a   z   d   a   r      P  y  t  h  o  n  e  !"
#       -15 -14 -13 -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1

V následující tabulce jsou u všech příkazů zobrazeny orientace deklarovaných úseků. U některých příkazů jsou explicitně rozepsány hodnoty explicitních i implicitních parametrů:

>>> seq = "Nazdar Pythone!"

>>> ap = seq[slice(3)]; ap           # --> Naz
                                     # 0 : 3 : 1
                       
>>> an = seq[slice(-3)]; an          # --> Nazdar Pytho
                                     # 0 : -3 : 1  
         
>>> b1p = seq[slice(3,5)]; b1p       # --> da
                                     # 3 : 5 : 1

>>> b1n = seq[slice(-3,-5)]; b1n     # <-- prázdná subsekv.

>>> b2p = seq[slice(5,3)]; b2p       # <-- prázdná subsekv.

>>> b2n = seq[slice(-5,-3)]; b2n     # --> ho subsekv.

>>> b3i = seq[slice(3,-5)]; 3i       # --> dar Pyt

>>> b3j = seq[slice(-3,5)]; b3j      # <-- prázdná subsekv.

>>> cp = seq[slice(3,12,2)]; cp      # --> drPto

>>> cn = seq[slice(-3,-12,-2)]; cn   # <-- nt

>>> cm = seq[slice(3,-12,-2)]; cm    # <-- prázdná subsekv.

>>> d = seq[slice(-2, -6)]; d        # <-- prázdná subsekv.

>>> e = seq[slice(-6, -2)]; e        # --> thon

>>> f = seq[slice(-2, -6, -1)]; f    # <-- enoh

>>> g = seq[slice(-6, -2, -1)]; g    # --> prázdná subsekv.

Závěrečná poznámka:
Dobrou zvláštní okolností při zadávání kladně i záporně orientovného úseku je skutečnost, že jeho deklarovaná délka může být delší než len(seq)+1 a nemůže zde dojít k chybě typu "IndexError: end index out of range":

>>> seq_op = [1, 2, 3, 4, 5, 6, 7]
>>> seq_sl = "Nazdar Pythone!"

d = seq_op[2:9]; d                    # --> [3, 4, 5, 6, 7]
f = seq_sl[slice(-10, -20, -1)]; f    # <-- radzaN

3.4   Pomocné techniky

3.4.1  Hromadné přiřazení

Hromadné přiřazení (multiple assignment) umožňuje přiřadit jediným příkazem výčet hodnot na pravé straně k výčtu jmen na straně levé. Tento příkaz lze zadat dvojím způsobem:

# V jednom řádku:
>>> x,y,z = 10,20,30; x,y,z   # 3 jména <-- 3 hodnoty             
(10, 20, 30)
# Ve dvou řádcích:
>>> seq = 1,2,3               # 1 jméno <-- 3 hodnoty 
>>> a,b,c = seq               # 3 jména <-- 1 entice 
>>> a*b*c
6
# Při přiřazení došlo k rozbalení hodnot z entice.

Při stejném počtu prvků na obou stranách proběhne jejich propojení v zadaném pořadí.

Výčet hodnot na pravé straně může mít formát entice (tuple), seznamu (list), řetězce (string), setu (set), slovníku (dictionary) a rozsahu (range).

>>> a,b = 12, 22; a,b             # tuple = tuple 
(12, 22)
>>> a,b = [12, 22]; a,b           # tuple = list 
(12, 22)             
>>> c,d,e = "Haf"; c,d,e          # tuple = string
('H', 'a', 'f')
>>> f,g = {5, "pivo", 5}; f,g     # tuple = set
('pivo', 5)
>>> f,g = {"j":15, "k":25}; f,g   # tuple = dict
('j', 'k')                             (vrací jen klíče)
>>> b,f,l = range(3); b,f,l       # tuple = range
(0, 1, 2)

Hromadným přiřazením rozbalíme slovník ve smyčce for ...

>>> dic = {'a':2, 'b':4, 'c':10}     # deklarace slovníku
>>> for pol in dic.items():          # vestavěná methoda         
        key, value = pol             # hromadné přiřazení
        print(f"Klíč {key} má hodnotu {value}")
Klíč a má hodnotu 2
Klíč b má hodnotu 4
Klíč c má hodnotu 10

Hromadné přiřazení lze výhodně použít i ve spojení s funkcí, pokud její invokace produkuje stejný počet hodnot jako zadaný počet proměnných:

>>> def mocniny(num):
        return num, num**2, num**3     # fce vrací 3 hodnoty 
>>> num, sqr, cub = mocniny(2)         # jejich přiřazení
>>> num, sqr, cub                      # invokace
(2, 4, 8)		

Hromadné přiřazení použijeme i ve spojení s komprehencí iteráblu (viz 3.5):

>>> x,y,z = (x**2 for x in (1,3,5))    # hromadné přiřazení
>>> x,y,z                              # invokace
(1, 9, 25)

Hromadným přiřazením lze rozbalit i vnořené iterábly:

>>> body = (1,2), (-1,-2)              # entice entic
>>> (x1,y1), (x2,y2) = body            # hromadné přiřazení
>>> print(f"x1,y1 ={x1,y1}, x2,y2 ={x2,y2}")
x1,y1 =(1, 2), x2,y2 =(-1, -2)

Hromadné přířazení použijeme výhodně při záměně přiřazení:

>>> x,y,z = 10,20,30
>>> z,y,x = x,y,z
>>> x,y,z
(30, 20, 10)            

Poznámka:
Alternativní označení hromadného přiřazení je rozbalení iteráblu (iterable unpacking).

3.4.2  Rozbalení sekvence

Pokud je počet členů na levé straně   <=   1+ počet členů   na pravé straně, lze při hromadném přiřazení použít hvězdičkami dekorovaná jména (variadická jména), např:

>>> *a, = 1,2,3,4,5; a          # *a, je entice (tuple)
[1,2,3,4,5]                     # tuple 'rozbaluje' do listu
>>> [*a] = 1,2,3,4,5; a
[1,2,3,4,5]                     # stejně jako list

Ve výčtu jmen na levé straně smí být jen jedna hvězdičková proměnná. Neohvězdičkovaná jména se nazývají povinné (mandatory) identifikátory a mají při přiřazování hodnot přednost před hvězdičkou dekorovanými identifikátory:

>>> a,*b,c = 1,2,3,4,5; a,b,c
(1,[2,3,4],5)
>>> a,*_,c = 1,2,3,4,5; a,_,c   # _ je 'anonymní' proměnná
(1,[2,3,4],5)
>>> a,*b,c = 1,2; a,b,c         # OK: 3 <= 1 + 2
(1,[],2)   # hodnoty 1,2 měly přednost, na b* nic nezbylo                      

Možné jsou i tyto (poněkud rozpustilé) kombinace:

>>> f,*m,(*ir,il) = 0,1,2,3,(4,5,6)       # přiřazení
>>> f; m; ir; il                          # invokace
0
[1, 2, 3]
[4, 5]
6

Hvězdičkové operátory jsou mnohdy nezbytné pro rozbalení některých iteráblů:

>>> *ran, = range(8); ran    # tuple či list na levé straně!
[0, 1, 2, 3, 4, 5, 6, 7]     
>>> r = list(ran); r         # alternativní invokace
[0, 1, 2, 3, 4, 5, 6, 7]
>>> *gen, = [2**x for x in range(8)]; gen     # komprehence
[1, 2, 4, 8, 16, 32, 64, 128]

Hromadné přiřazení s hvězdičkovým členem usnadňuje přístup k prvkům iteráblu bez použití indexů:

>>> items = "zdař", 25, True, 2+5
Výběr pomocí indexů:
>>> print(f"První položka je {items[0]}, poslední je {items[-1]}")
První položka je zdař, poslední je 7
Výběr hromadným přiřazením:
>>> first, *middle, last = items
>>> print(f"Prostřední položka je {middle}, poslední je {last}")
Prostřední položka je [25, True], poslední je 7

3.4.3  Rozbalení slovníku dekorací **

Dekorace **, uvedená před názvem slovníku, přikazuje jeho rozbalení před dalším použitím:

>>> dic = {'a':2, 'b':4, 'c':10}
>>> def fun(a,b,c):
        return a,b,c
>>> fun(**dic)
(2, 4, 10)

Jiný příklad se sloučením slovníků:

>>> prvý = {"A": 1, "B": 2}
>>> druhý = {"C": 3, "D": 4}
>>> spolu = {**prvý,**druhý,**{"e": 5, "f": 6}}  # deklarace
>>> spolu                                        # invokace
{'A': 1, 'B': 2, 'C': 3, 'D': 4, 'e': 5, 'f': 6}

Jen tak mimochodem - stejně úsporně lze totéž provést operátorem | (OR):

>>> prvý = {"A": 1, "B": 2}
>>> druhý = {"C": 3, "D": 4}
>>> spolu = prvý | druhý | {"e": 5, "f": 6}
>>> spolu
{'A': 1, 'B': 2, 'C': 3, 'D': 4, 'e': 5, 'f': 6}

3.5 Komprehence

Komprehence je skladebný předpis pro vytvoření modifikovaného objektu z poskytnutého iteráblu typu list, tuple, range(), set, frozenset (neměnitelná verze setu), dict a string (formálně také bytes, bytearray a memoryview) s použitím kompaktního předpisu, případně ještě při splnění jisté (avšak nepovinné) podmínky. Výsledným modifikovaným objektem může být:

  1. objekt jiného typu
  2. výraz, pracující s prvky zadaného iteráblu.

Komprehenci ad b) lze ilustrovat matematickým výrazem, který pro všechna x z oboru přirozených čísel N, jejichž druhé mocniny jsou větší než 3, provede výraz 2*x:

  S = { 2*x | x € N, x² > 3 }

Obdobou výše uvedeného matematického výrazu může být toto schéma skladby v Pythonu:

S = <f(x) for x in <objekt>                 # bez podmínky
S = <f(x) for x in <objekt> if <podmínka>>

Závorky kolem deklarované komprehence jsou povinné a mají vliv na výsledný typ modifikovaného objektu. Používají se závorky: [], {} a (). Použijí-li se kulaté závorky, nutno je použít s funkcí tuple.

Idiom (f(x) for x in <objekt>) je zároveň generátorovým výrazem viz odst. 3.6.1, umožňujícím iteraci.

Ukažme si konkretní příklad komprehence bez podmínky u iteráblů list, tuple, range(), string, set, frozenset, dictionary, bytes, bytarray a memoryview.
Nutno poznamenat, že pro řádné provedení transformací je nutné aby objekty typu set a klíče slovníku neodkazovaly na typ string:

# Výpis výchozích iteráblů:
li, tu = [1, 2, 3, 4], (1, 2, 3, 4)       # list, tuple
rg, sg = range(2,6), "růže"               # range, string  
st = {4, 7, 4, True}                      # set 
fs = frozenset(st)                        # frozenset
dc = {4:"size", 5:"Karel"}                # dictionary
by = bytes(sg, 'utf-8')                   # bytes from string
ba, mv = bytearray(by), memoryview(by)    # bytearray, memoryview
   
# Použité závorky (<> = [], (), {}) mají vliv na výsledný 
typ modifikovaného objektu.
Pokusíme-li se o obecný záznam zamýšlené komprehence, mohli 
bychom napsat třeba toto:
>>> mod_obj = <x*2 for x in iterable>     # kde <> = [], {}
# Kulaté závorky () použijeme s funkcí tuple:
>>> mod_obj = tuple(x*2 for x in iterable)
# Bez funkce tuple získáme místo upraveného objektu 
generátorový objekt, například:
>>> gen_obj = (x*2 for x in mv)
<generator object <genexpr> at 0x00000124DACB2810>

Jak uvedeno, iterábly lze komprehovat pro výstup ve formátu list, tuple, set, přičemž pro výstup ve formátu tuple (entice) musime použít funkci tuple().

Dále si ukážeme komprehenci s podmínkou:

>>> numbers = [1, 3, 2, 5, 3, 1, 4]
>>> {x**2 for x in numbers if x**2 > 3}
{16, 9, 4, 25}               # komprehence s podmínkou a redukcí
>>>
>>> files = ('bin', 'Data', 'Desktop', '.bashrc', '.ssh', '.vimrc')
>>> [name for name in files if name[0] != '.']
['bin', 'Data', 'Desktop']   # komprehence s podmínkou
Výrazových předpisů může být v jedné komprehenci více:
>>> [(x, x**2, x**3) for x in numbers]   # komprehence seznamu
[(1, 1, 1), (2, 4, 8), (3, 9, 27), (4, 16, 64)]
Může být více i poskytnutých objektů:
>>> numbers = (1, 2, 3, 4)
>>> letters = ['a', 'b', 'c']
>>> [n*letter for n in numbers for letter in letters]
['a', 'b', 'c', 'aa', 'clr-bb', 'cc', 'aaa', 'clr-bbb', 'ccc', 'aaaa', 'clr-bbclr-bb', 'cccc']

Zajímavá je úprava argumentu v této komprehenci:

>>> [v*2 for v in ("a", 5, True)]
['aa', 10, 2]                      # 2*True = 2*1 = 2

Za povšimnutí stojí i toto spojení komprehence s formátováním řetězce dle kap. 6.11:

>>> chlapci = ["Pavel", "Petr", "Jan"]
>>> synci = ["{} Novotný".format(boy) for boy in chlapci]
>>> synci
['Pavel Novotný', 'Petr Novotný', 'Jan Novotný']

Ukázka komprehence setu z řetězce:

>>> s = {v for v in "ABCDABCD" if v not in "CB"}; s
{'A', 'D'}

Komprehencí setu z řetězce lze s pomocí funkce enumerate (kap. 5.7) vytvořít výběrový slovník:

>>> d = {key:val for key,val in enumerate ('ABCD') if val not in "CB"}
>>> d
{0: 'A', 3: 'D'}

Zajímavé je použití pojmenovaného výrazu (mroží operátor - 2.6.2) při komprehenci seznamu:

>>> def f(x):
        return x + 2

>>> print([(y := f(x), x/y) for x in range(3)])
[[2, 0.0], [3, 0.3333333333333333], [4, 0.5]]

případně se zaokrouhlením des. čísla dle kap. 6.13:
>>> print([[y := f(x), round(x/y, 2)] for x in range(3)])
[[2, 0.0], [3, 0.33], [4, 0.5]]

případně s podmínkou a přehledněji:
>>> print([(x, y, round(x/y, 2)) for x in range(3) if (y := f(x)) > 0])
[(0, 2, 0.0), (1, 3, 0.33), (2, 4, 0.5)]

Následující komprehencí vybereme ze zadané množiny bodů ty, které leží uvnitř kružnice o poloměru r = 2:

>>> import math
>>> radius = 2
>>> [(x, y) for x in range(-2,3) for y in range(-2,3) if
...  math.sqrt(x**2 + y**2) < radius]
[(-1,-1), (-1,0), (-1,1), (0,-1), (0,0), (0,1), (1,-1), (1,0), (1,1)]

3.6 Generátory

Generátor (generátorový objekt) vznikne přiřazením generátorového výrazu nebo generátrové funkce ke jménu proměnné.
Tento objekt je iterátorem, který neobsahuje žádná data, pouze je evokuje v okamžiku invokace.

3.6.1   Generátorový výraz

Generátorový výraz má formát idiomu f(x) for x in <objekt>, neboli komprehence (viz 3.5).
Iterace iteráblu provádí normální (eager) iteraci, iterace iterátoru provádí tak zvanou línou (lazy) iteraci. Líná iterace je méně náročná na paměť
V následující ukázce jsou obě iterace realizovány smyčkou for i in ~.

>>> st = "abakus"           # iterovatelný objekt - iteráble 
>>> stv = (i for i in st)   # generátorový objekt - iterátor
>>> type(stv)
<class 'generator'>
>>> for i in stv: print (i,end=" ") # líná (lazy) iterace iterátoru
a b a k u s 
>>> for i in st: print (i,end=" ")  # dychtivá (eager) iterace iteráblu
a b a k u s 

Generátorový výraz lze vytvořit pro traverzování všech iteráblů. Zde vytvoříme iterátor pro iterábl typu range():

>>> gv = (i*i for i in range(5))        # iterátor  
>>> for i in gv: print (i, sep=", ")    # líná (lazy) iterace 
0, 1, 4, 9, 16 

3.6.2   Generátorová funkce

Generátorová funkce je normální funkce, která místo příkazu return (jenž ukončí běh funkce) obsahuje příkaz yield (jenž pouze přeruší běh funkce). Volání generátorové funkce vrací generátorový objekt, jenž je iterátorem s přímo použitelnou funkcí next(~) či metodou __next__(), případně lze použít smyčku for ... jako v předchozím případě:

def count_to(m):           # vrací iterátor
    n = 0
    while n <= m:
        yield n            # poskytuje elementy iterátoru
        n += 1
>>> ct = count_to(5)          # gener. objekt, iterátor 
# Invokace iterátoru:
>>> next(ct)                  # 1. krok iterace (funkce)
0
>>> ct.__next__()             # 2. krok iterace (metoda)
1
>>> for i in ct: print (i, end=" ")  # pokračování iterace
2 3 4 5                                smyčkou for ...
>>> next(ct)
StopIteration                 # iterátor je prázdný
>>> ct = count_to(5)          # objekt nutno obnovit 
>>> print(list(ct))                 
[0, 1, 2, 3, 4, 5]            # výtisk generátor. objektu
>>> print(list(ct))
[]                            # iterátor je prázdný

Generátorový objekt (iterátor) vytvoří i tato generátorová funkce:

def squares(start, stop): 
    for i in range(start, stop):
        yield i * i

Přiřazením invokace generátorové funkce ke jménu vytvoříme iterovatelný generátorový objekt (typu generátor).
Iteraci provedeme buď opakovaným použitím fce next(sqr) nebo najednou prostřednictvím smyčky for ... či pomocí funkce list(sqr), která v tomto případě působí jako iterátor:

>>> sqr = squares(1,7)            # lazy iterátor

>>> next(sqr), next(sqr)          # opakovaná iterace
(1, 4)
>>> for i in sqr:                 # smyčkou for ... vyčerpáme                 
        print (i, end=" ")         zbytek najednou              
9 16 25 36

>>> sqr = squares(1,7)            # obnova iterátoru
>>> list(sqr)                     # najednou fcí list()                   
[1 4 9 16 25 36]                  

Výpočet řady Fibonacciho čísel můžeme provést pomocí následující generátorové funkce s nekonečnou smyčkou:

def fibo():                        # generátorová funkce> 
    a, b = 0, 1 
    while True:                    # nekonečná smyčka 
        yield a
        a, b = b, a+b 

Generátorový objekt (iterátor) s nekonečnou smyčkou voláme buď po krocích nebo pro konkretní mez, určenou funkcí range():

>> f = fibo()                # generátorový objekt, iterátor
>> f.__next__()              # -->  0  jiná verze fce next()
>> next(f), next(f)          # -->  (1, 1)  

# Můžeme pokračovat do omrzení pomocí fce range():                         
>>> for i in range(4):        
       print(next(f), sep=", ")    # --> 2, 3, 5, 8

>>> for i in range(5): 
        print(next(f), sep=", ")   # --> 13, 21, 34, 55, 89  

Funkce next(f) zde ve smyčce začíná tam, kde skončila předchozí invokace generátorového objektu f. Funkcí range() určujeme délku pokračující iterace po nekonečné řadě Fibonacciho čísel.

3.6.3   Líný výpočet

Generátorový výraz a generátorová funkce vrací generátorový objekt, jímž je pomalu (lazy) vyhodnocovaný iterátor.

Při líném výpočtu (lazy evaluation) se odkládá výpočet výrazu až do chvíle jeho potřeby. Důsledkem je zmenšená potřeba zdrojů (paměti) při zvýšeném výkonu procedury.

Lze sestavit potenciálně nekonečné struktury, např. smyčky - viz fibonacci().

Líný výpočet se obtížně kombinuje s imperativními strukturami jako je ošetření výjímek nebo vstup/výstup; může vytvářet "space leaks" (viz).

Příkladem procedury, uplatňující líný výpočet, je použití slovníku v kapitole 9.12 (Switch case), kde u funkce kalkul(oprerátor, x,y) je řešeno částečné ošetření výjimek.


3.7 Jmenné prostory

Všechny aktivity spuštěného programu se odvíjejí od textu, vyskytujícího se v různých lexikálních (textových) oblastech kódu, zvaných jmenný prostor (namespace), JP.

Těmito oblastmi (jmennými prostory) jsou:

  1. Interní knihovna souborů Pythonu (builtins), ve které jsou uloženy deklarace vestavěných (built-in) funkcí, tříd a konstant. Tato knihovna reprezentuje oblast, zvanou built-in.
    Vestavěná oblast se automaticky aktivuje při spuštění interpreta a její objekty jsou vždy přímo dostupné z kterékoli další oblasti.
  2. Další oblastí, označovanou slovem globální, je text skriptu a modulu (viz Kap.1.3; 3. odst.). Tato oblast obsahuje uživatelem psaný text programu - deklarace proměnných, funkcí a tříd.
    Globální oblastí je také text, zapsaný v otevřeném prostoru konzoly Pythonu (interpreta).
  3. Následujícími oblastmi uvnitř skriptu či modulu jsou vnitřní prostory funkcí a tříd, nepřekvapivě označované slovem lokální.
    Aktivní stav lokálního prostoru vzniká invokací funkce a zaniká ukončením (výstupem z) funkce. Objekty lokálního prostoru jsou dostupné z lokální a vnitřní oblasti.
    Poznámka: O funkcích a třídách víme, že jejich
     definice  se skládají ze záhlaví a z těla (funkce
     či třídy). Jejich lokální  oblast je tvořena jejch
     vnitřním prostorem (tělem).
    
  4. Pokud funkce sama obsahuje ještě vnitřní (vnořenou) funkci, je za další pojmenovanou oblast považován vnitřní prostor funkce vnořené - říkejme ji oblast vnitřní. Vnořená funkce může obsahovat svou další vnořenou funkci, případně může vnější funkce obsahovat více vnořených funkcí se stejnou úrovní zanoření.

Jmennými oblastmi jsou tedy tyto oblasti (anglicky): built-in, global, local a inner

3.7.1   Všechno je objekt

Objekty jsou přístupné prostřednictvím jmen, které na ně odkazují. Vázání objektu ke jménu je realizováno:

  1. aktem přiřazení hodnoty ke jménu:
    mamoj = "František"
    
  2. deklarací záhlaví a těla funkce (třídy), což představuje připojení objektu funkce (třídy) k jejímu jménu:
    def fun(m)                        # záhlaví
        print(m*m)                    # tělo
    

Jméno je na předním místě v pořadí důležitosti entit. Rozdělení zdrojového kódu do výše uvedených oblastí (vestavěná, globální, lokální, vnitřní) je důležité při rozlišování stejných jmen v programovém prostoru.
V jedné oblasti se nemohou současně vyskytovat dvě stejná jména, odkazující na různé hodnoty:

mam = "Alena"    # deklarace přiřazení
mam = "Petr"     # změna přiřazení, předchozí deklarace neplatí

Mohou se však bez kolize vyskytovat současně v různých oblastech - v různých jmenných prostorech.

3.7.2   Globální a lokální proměnná

Globální proměnná je tvořená v globální oblasti skriptu a je přímo dostupná z globální i z lokální oblasti (funkce) - interpret hledá jméno tam, kde je žádané, případně pokračuje v hierarchicky nadřazené oblasti.

glob_lok.py
def fun():
# lokální prostor funkce v glob. prostoru skriptu:    
    s = "Toto je lokální 's'"     # lokální proměnná   
    print(s)
# globální prostor interpreta Pythonu:

>>> s = "Toto je globální 's'"    # globální proměnná
>>> fun()                         # invokace funkce fun()
Toto je lokální 's'
>>> print(s)                      # invokace funkce print()
Toto je globální 's'

Vidíme, že jedinečnost jména je dána jeho umístěním. Příkaz print(s) v globálním prostoru ignoruje přiřazení jména v lokálním prostoru, neboť toto jméno je mu přímo nedostuné.

3.7.3   Jmenný registr

Přítomnost jména ve jmenném prostoru je zapsána ve jmenném registru, jenž má formu slovníku (dict). O existenci tohoto slovníku nás přesvědčí jednoduchá ukázka. Mějme soubor global.py v němž uplatníme vestavěné funkce globals() a locals() jimiž lze zobrazit jmenné registry příslušných jmenných prostorů:

# global.py                    # globální prostor skriptu

a = [1, 2, 3, 4, 5]            # globální proměnná skriptu

 
def foo():                     # globální objekt foo()
    b = 11                     # lokální proměnná funkce
    print(locals())
>>> foo()
 {'b': 11}               # registr lokálního prostoru foo()
 
>>> print(globals()) 
# registr globálního prostoru skriptu 'global.py':    
{'__name__': '__main__', '__doc__': None, '__package__': None, 
'__loader__': <class '_frozen_importlib.BuiltinImporter'>,
'__spec__':none, '__annotations__': {}, 
'__builtins__': <module 'builtins' (built-in)>,
'__file__': 'F:\\Codetest\\python\\Ukázky\\scopes\\glob_loc.py',
'a': [1, 2, 3, 4, 5], 'foo': <function foo at 0x0000023E6039CFE0>} 

Všimněme si, že se proměnná b v globálním registru nevyskytuje, neboť patří do lokálního prostoru, potažmo do lokálního registru, kde ji vidíme.

Odchylka:
Zavedené označení výše popsaného slovníku je jmenný prostor. Nepodařilo se mi pochopit, proč má mít jeden termín dva významy (slovník a lexikální prostor) a proto jsem si dovolil uvést termín jmenný registr, JR.

3.7.4   Scope jména

Jmennými oblastmi jsou oblasti (anglicky): built-in, global, local a inner. Tyto oblasti tvoří uspořádanou kompetenční hierarchii (BGLI) - od nejvýše k nejníže postavené.

Souhrn lexikálních oblastí, z nichž je jméno přímo dostupné, se nazývá scope tohoto jména. Do tohoto souhrnu patří jmenný prostor, v němž je jméno deklarováno a všechny níže postavené jmenné prostory. Obecně lze řící, že z výše postavené oblasti nelze volat jméno v níže postavené oblasti.

Dostupnost jmen si ukážeme na příkladu funkce fun_lok(), jež obsahuje vnitřní funkci fun_inn().
Následující soubor glob_local.py představuje globální prostor, v němž se vyskytují vestavěné funkce print() z prostoru built_in:

# glob_local.py                       # globální prostor

glo = "Živijó"                        # globální proměnná

def fun_lok():                        # globální proměnná
    lok = "lokální"                   # lokální proměnná
    print("glo, lok = ", glo, lok, sep=", ")  # built_in  
    def fun_inn():                    # lokální proměnná
        inn = "inner"                 # vnitřní proměnná
        print("glo, lok, inn = ", glo, lok, inn, sep=", ")           
        print(locals())               # built_in       

    fun_inn()                         # invokace vnořené funkce
>>> fun_lok()                  # invokace glob. fce 
glo, lok = Živijó, lokální     # výstup lok. fce 'print()' 
glo, lok, inn = Živijó, lokální, inner  # výstup 1. vnitřní fce 'print()'
{'inn': 'inner', 'lok': 'lokální'}      # výstup 2. vnitřní fce 'print()'; lokální JR      

Na příkladu tedy vidíme, že:


3.8 Proměnná __name__

Proměnná __name__ je speciální proměnná, jíž je při realizaci skriptu či souboru automaticky přiřazena buď hodnota '__main__' nebo 'název_souboru' a to v závislosti na tom, jak byl skript či soubor spuštěn:

  1. byl-li spuštěn v otevřené konzole Pythonu nebo příkazem v systémové konzole ( > python my_file.py), potom __name__ == '__main__'
  2. byl-li importován jako modul (viz kap. 7.5), potom __name__ == 'název souboru' (bez přípony '.py')
>>> __name__            # --> '__main__'   - invokace ad a)
>>> import my_file
>>> my_file.__name__    # --> 'my_file'    - invokace ad b)

Případ si ilustrujme na podrobnější ukázce. Mějme dva soubory, prvnímu budeme říkat modul, protože jej chceme importovat do jiného souboru:

# cosi_name.py

def cosi():  
    print('Hodnota proměnné __name__  jest: ', __name__)
	
if __name__ == "cosi_name":
    print("Nekapišto.")	

Provedením modulu v Thonny zjistíme, že je ticho po pěšině - deklarovaná funkce nebyla volána, idiom "if __name__ == 'cosi_name' " je nepravdivý, tudíž se příkaz print('Nekapišto.') nerealizuje:

>>> %Run cosi_name.py
>>> 

Napíšeme si další soubor ...

# other_name.py

import cosi_name                          # import modulu
print('Pro cosi_name je __name__ =', cosi_name.__name__)
print('Pro other_name je __name__ =', __name__)

if __name__ == "cosi_name":
    print("Kapišto?")	

... a po jeho spuštění zjistíme, že nám na prvním místě figuruje výstup z importovaného souboru cosi_name.py:

>>> %Run other_name.py

 Nekapišto.                                  # naše záhada
 Pro cosi_name je __name__ = cosi_name       # jasná páka
 Pro other_name je __name__ = __main__       # jasná páka
Rozuzlení:

Importovaný soubor cosi_name.py změnil status. Jeho atribut __name__ má nyní hodnotu '__cosi_name__' - proto mohlo dojít k vytištění řetězce 'Nekapišto.' a to ještě před výstupy ze souboru other_name.py.


3.9 Introspekce Pythonu

Introspekce Pythonu je akt zkoumání obsahu a vlastností jednotlivých jeho objektů. Za tímto účelem disponuje Python řadou vestavěných funkcí, z nichž některé již známe.


Dalším introspekčním nástrojem je t.zv. docstring (dokumentační řetězec), což je krátká informace ve formátu řetězce (''' ~ '''), uvedená v záhlaví souborů, funkcí a tříd - viz kap. 6.11.


3.10 Testování s 'doctestem'

Při rozvoji programu se s oblibou provádí testování vybraných úseků zdrojového kódu. Pro toto testování poskytuje Python moduly doctest a unittest.

Popíšeme si práci s modulem doctest. Zkoumané vzorky kódu se umístí do dokumentačního řetězce pod záhlavím funkce. V každém vzorku je na prvním řádku kód, jakoby zadaný v interaktivním režimu, na druhém řádku je očekávaná odezva.

Modul doctest automaticky spustí příkaz začínající >>> a jeho výstup porovná s následujícím řádkem.

def is_divisible_by_2_or_5(n):
    """
    >>> is_divisible_by_2_or_5(8)
    True
    >>> is_divisible_by_2_or_5(7)
    False
    >>> is_divisible_by_2_or_5(5)
    True
    """ 

    return n % 2 == 0 or n % 5 == 0

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Poslední tři řádky spouští celou parádu. Umisťujeme je na konec každého souboru, který obsahuje doctesty.

Pokud procedura zjistí shodu mezi zkoumanými vzorky a zadanými výsledky, reaguje "mlčením" nebo je-li v konzole evokována funkce "doctest.testmod()", vrátí stručný komentář o průběhu doctestu.

>>> doctest.testmod()
TestResults(failed=0, aclr-ttempted=3)

Pokud narazí na rozpor, spustí rozsáhlé chybové hlášení.

Spuštění neúspěšného skriptu vyprodukuje například následující výstup:

Failed example:
    is_divisible_by_2_or_5(7)
Expected:
    True
Got:
    False
***********************************************************
1 items had failures:
   1 of   3 in __main__.is_divisible_by_2_or_5
***Test Failed*** 1 failures.

3.11 Glosář

přírůstkový rozvoj (incremental development)
Postupné rozšiřování programu se záměrem testovat postupně jenom menší objem kódu.
brlení (scaffolding)
Kód, použitý při rozvoji programu, který není součástí konečné verze.
záznam (trace)
Sledování toku výpočtu, při kterém se zaznamenávají hodnoty proměnných a výstupů.
zpětný záznam (traceback)
Seznam prováděných funkcí, který se zobrazí na obrazovce, když se vyskytne chyba při běhu programu. Zpětný záznam je také uváděn jako záznam zásobníku, protože probírá funkce v pořadí, ve kterém byly do zásobníku běhu programu zařazeny.
zapouzdřit (encapsulate)
Zabalit část programu do funkce.
 jednotkové testování (unit testing)
Automatická procedura používaná k ověření správné činnosti jednotlivých úseků kódu. V Pythonu se pro tento účel používá vestavěný modul doctest.

3.12 Cvičení

  1. Napište jediný řetězec, který
     stvoří
     tento 
     výstup
    
  2. Přidejte volání funkce print k fci sqrt definované v Kap. 5.11 tak, že vytiskne hodnotu better ve smyčce po každé aproximaci. Volejte upravenou funkci pro argument 25 a zapište výsledky.
  3. Zaznamenejte průběh výpočtu poslední verze fce print_mult_table (odstavec 5.8) a zamyslete se nad jeho postupem.
  4. Do souboru triangularNumbers.py napište funkci print_triangular_numbers(n), která vytiskne součet prvních n členů aritmetické posloupnosti (1 3 6 10 ... trojúhelníková čísla). Voláním funkce dostaneme následující výstupy:
     1     1
     2     3
     3     6
     4     10
     5     15
    
  5. Do souboru testPrime.py vložte funkci test_prime, která vyhodnotí zadané celé číslo a vrátí True pro argument, který je prvočíslem a False pro argument, který prvočíslem není. Při vývoji funkce používejte "doctesty".
    Soubor s doctesty končí tímto kódem:
    if __name__ == '__main__':
        import doctest
        doctest.testmod()
    
  6. Funkce num_digits(n) v odstavci 5.3 nepracuje správně pro n < 0. Doplňte ji tak, aby pracovala správně pro všechna celá čísla (včetně nuly).

    def num_digits(n): 
        """
        >>> num_digits(12345)
        5
        >>> num_digits(0)
        1
        >>> num_digits(-12345)
        5
        """
    
  7. Do souboru numEvenDigits.py přidejte následující kód a tělo příslušné funkce:
    def num_even_digits(n): 
        """
        >>> num_even_digits(123456)
        3
        >>> num_even_digits(2468)
        4
        >>> num_even_digits(1357)
        0
        >>> num_even_digits(2)
        1
        >>> num_even_digits(20)
        2
        """
    
  8. Do souboru printRevDigits.py přidejte následující kód a tělo přislušné funkce:
    def print_digits(n): 
        """
        >>> print_digits(13789)
        9 8 7 3 1
        >>> print_digits(39874613)
        3 1 6 4 7 8 9 3
        >>> print_digits(213141)
        1 4 1 3 1 2
        """
    
    Nutno zajistit, aby mezi číslicemi výstupu byly mezery a aby za poslední číslicí žádná mezera nebyla. Numerický argument funkce nutno před manipulací přetvořit na string (viz 2.6), jehož délku určíme funkcí len (viz 2.10).
    Pokud se vám nepodaří aby za poslední číslicí žádná mezera nebyla, nevyjádří doctest souhlas mlčením. Jednotlivá funkce ale může pracovat bez závady - protože mezera za poslední číslicí jinak nevadí.
  9. Do souboru sumofSquares.py přidejte funkci sum_of_squares(n), která spočítá součet čtverců číslic zadaného celého čísla. Například, sum_of_squares(72) by mělo vrátit 53, protože 7**2 + 2**2 == 49 + 4 == 53.
    def sum_of_squares_of_digits(n): 
        """
        >>> sum_of_squares(1)
        1
        >>> sum_of_squares(9)
        81
        >>> sum_of_squares(11)
        2
        >>> sum_of_squares(121)
        6
        >>> sum_of_squares(987)
        194
        """
    
    Svá řešení ověřte pomocí doctestů.

pre up next title end end