previous up next hi end end

5. Postupy a techniky

  1. Zápis běhu programu
  2. Rozvoj programu
  3. Počítání číslic
  4. Tabelární data
  5. Tabulky
  6. Zapouzdření a zobecnění
  7. Další zapouzdření
  8. Další zobecnění
  9. Newtonova metoda
  10. Introspekce Pythonu
  11. Testování s doctestem
  12. Glosář
  13. Cvičení

5.1 Zápis běhu programu

K efektivnímu psaní počítačových programů potřebuje programátor vyvinout schopnost zaznamenat průběh běžícího programu. Znamená to "stát se počítačem" a sledovat průběh provádění, zaznamenávajíc stavy všech proměnných a všechny výstupy, které program generuje po provedení jednotlivých instrukcí.

Abychom porozuměli tomuto procesu, sledujme volání funkce sequence(n) z kapitoly 4.1. Abychom měli tuto funkci na očích, vytvořte v IDLE soubor sequence.py, který si k dalšímu výkladu otevřete.

Průběh volané funkce pro n = 3 si rozepíšeme do tabulky, v níž záhlaví sloupců tvoří jednotlivé výrazy funkce.
V prvním sloupci jsou postupně posuzované hodnoty n, ve třetím sloupci jsou postupně tištěné hodnoty n:

   n     n!=1    print(n)    n%2==0     n/2     n*3+1   
  ---    ----    --------    ------    ----    ------  
   3       T         3          F        -       10       
  10       T        10          T        5        -
   5       T         5          F        -       16
  16       T        16          T        8        -
   8       T         8          T        4        -
   4       T         4          T        2        -
   2       T         2          T        1        -
   1       F         -          -        -        -

Správnost naší sestavy si ověříme spuštěním souboru a voláním funkce pro n=3, jejíž výstup je tento:

>>> sequence(3)
3, 10, 5.0, 16.0, 8.0, 4.0, 2.0,

Provádění záznamu může být únavné a náchylné k chybám, je ale nezbytnou dovedností programátora. Z tohoto záznamu se dozvíme mnohé o tom, jak náš kód pracuje. Můžeme si povšimnout, že jakmile se n stane mocninou 2, potřebuje tělo smyčky log2(n) cyklů ke svému ukončení. Také vidíme, že konečná hodnota 1 se nevytiskne.


5.2 Rozvoj programu

Nyní bychom měli být schopni poznat pouhým pohledem na zápis funkce co má provádět. Pokud jsme prováděli cvičení, sami jsme několik menších funkcí napsali. Při psaní větších funkcí nám mohou nastat potíže kvůli významovým chybám a chybám při běhu programu.

Abychom se vyrovnali s rostoucí složitostí programů seznámíme se s technikou, zvanou přírůstkový rozvoj. Jeho cílem je vyloučení dlouhých seancí při odstraňování chyb tím, že se postupně přidávají a testují krátké úseky kódu.

Jako příklad předpokládejme, že chceme nalézt vzdálenost dvou bodů, daných souřadnicemi (x1, y1) a (x2, y2). Vzdálenost podle Pythagorovy věty je:

Nejprve musíme uvážit jak by měla funkce distance vypadat. Jinými slovy, jaké budou vstupy (parametry) a jaký bude výstup (hodnota return)?

V našem případě musíme na vstupu zadat čtyři souřadnice dvou bodů. Výstupní hodnotou bude jejich vzdálenost s hodnotou typu float.

Již můžeme psát obrys funkce:

def distance(x1, y1, x2, y2):
    return 0.0

Je zřejmé, že tato verze žádnou vzdálenost nepočítá, neboť vždycky vrátí nulu. Je ale skladebně (syntakticky) správná, poběží a můžeme jí testovat před tím než ji zkomplikujeme.

Abychom ji otestovali, zavoláme ji pro jednoduché hodnoty:

>>> distance(1, 2, 4, 6)
0.0

Hodnoty jsme vybrali tak, aby vodorovná vzdálenost bodů byla 3, svislá 4 a výsledná vzdálenost 5 (což je přepona pravoúhlého trojúhelníka). Při testování funkce je užitečné znát správný výsledek předem.

V této chvíli jsme se přesvědčili, že funkce je syntakticky správná a můžeme začít přidávat řádky kódu. Po každé malé změně funkci znovu otestujeme. Objeví-li se v kterémkoli místě chyba, budeme vědět kde musí být – v posledním přidaném řádku.

Prvním logickým krokem ve výpočtu bude nalézt rozdíly x2-x1 a y2-y1. Tyto hodnoty uložíme do dočasných proměnných dx, dy a vytiskneme je.

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    print("dx is", dx)
    print("dy is", dy)
    return 0.0

Je-li funkce v pořádku, měly by výsledky být 3 a 4. Jestliže ano, ověřili jsme si, že náš dílčí program pracuje správně. Pokud ne, potřebujeme prověřit jenom několik málo řádků.

Nyní sečteme součet čtverců pro dx a dy.

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    dsquared = dx**2 + dy**2
    print("dsquared is: ", dsquared)
    return 0.0

Všimněme si, že jsme odstranili evokace funkce print, které jsme zapsali v předchozím kroku. Takovéto dočasné části kódu říkáme brlení (scaffolding), protože pomáhá při sestavení programu, ale není součástí finálního výsledku.

Opět necháme proběhnout program a zkontrolujeme výstup (což by mělo být 25).

Konečně, s použitím zlomkového exponentu 0.5 pro nalezení odmocniny, můžeme spočítat výsledek:

def distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    dsquared = dx**2 + dy**2
    result = dsquared**0.5
    return result

Pokud nám to chodí správně, jsme hotovi.

Když začínáme, měli bychom přidávat jen po jednom či dvou řádcích. Po získání jisté zkušenosti můžeme přidávat po větších dávkách. Přírůstkový rozvoj nám v každém případě ušetří mnoho času při odstraňování chyb.

Klíčové aspekty tohoto postupu jsou:

  1. Začneme s fungujícím programem a děláme malé přírůstkové změny. V každém okamžiku víme přesně kde může být případná chyba.
  2. Použijeme dočasných proměnných k uložení mezivýsledků, takže je můžeme vytisknout a zkontrolovat.
  3. Když program pracuje bezchybně, můžeme odstranit nepotřebné brlení, případně uspořádat některé příkazy do složených výrazů, ovšem tak, aby program zůstal dostatečně přehledný.

Stejný tvar funkce pro určení vzdálenosti dvou bodů jsme vytvořili již v kapitole 3.8:

import math
def distance(x1,y1, x2,y2):
    return math.sqrt((x2-x1)**2 + (y2-y1)**2)

5.3 Počítání číslic

Následující funkce spočítá počet číslic kladného celého čísla (>= 0):

def num_digits(n):
    if n == 0: return 1     # specielní případ

    count = 0               # počítadlo	
    while n:            
        count += 1         
        n = n//10           # celočíselné dělení
    return count

Volání num_digits(710) vrátí 3. V rámci cvičení (13.6) si tuto funkci upravíme pro všechna celá čísla.

V této funkci vidíme často používaný objekt, zvaný počítadlo (count). Proměnná count je zavedena s hodnotou 0 a potom zvětšována o 1 při každém splnění podmínky smyčky. Když smyčka skončí, obsahuje count celkový počet splnění podmínek, což je zde počet číslic zadaného čísla.
Postupně prováděné celočíselné dělení vypouští poslední číslici zadaného čísla.

Kdybychom chtěli počítat jenom číslice 0 nebo 5, pomůžeme si rozšířením podmínky před zvyšováním stavu počítadla:

def num_0_and_5_digits(n):
    if n == 0: return 1             # nula je také číslice

    n = abs(n)
    count = 0                       # počítadlo
    while n:
        zbytek = n%10               # další pomocná proměnná
        if zbytek == 0 or zbytek == 5:      
            count = count+1         # případně count += 1
        n = n//10                   # celočíselné dělení
    return count
Přesvědčte se, že  num_0o_and_5_digits(1055030250) vrací 7.


5.4 Tabelární data

Jedním z vhodných použití smyčky je generování tabelárních dat. Předtím, než byly počítače běžně přístupné, musely být logaritmy, siny, kosiny a jiné funkce čísel počítány ručně. Pro ulehčení výpočtu obsahovaly matematické příručky dlouhé tabulky s uspořádanými hodnotami těchto funkcí. Vytváření tabulek bylo zdlouhavé, nudné a občas plné chyb.

Když se na scéně objevily počítače, jedna z prvních reakcí byla: "Sláva! Můžeme tvořit tabulky na počítači, který nedělá chyby." To se ukázalo být (většinou) pravdivé, ale i krátkozraké. Brzy nato se staly kalkulátory a počítače tak všudepřítomné, že se tabulky staly zbytečné.

Tedy téměř. Pro některé operace používají počítače tabulky hodnot jako přibližnou mezihodnotu, kterou potom v dalším výpočtu upřesňují. Někdy byly chybné právě tyto tabulky; nejproslulejší jsou ty, které Intel Pentium používal k výpočtu dělení v plovoucí řádové čárce.

Při sestavování tabulek se prováděly opakované výpočty pro monotónně měněné vstupní hodnoty. Příkladem opakovaného výpočtu budiž následující program, který vytvoří posloupnost hodnot v levém sloupci a příslušné mocniny čísla 2 v pravém:

x = 1
while x < 13:
    print(x, '\t', 2**x)
    x += 1

Řetězec '\t' zastupuje znak tab, neboli tabulátor. Zpětné lomítko označuje začátek "escape" sekvence, což je pořadí několika určených znaků, které nahrazují neviditelné znaky např. pro tab nebo nový řádek ('\n').

"Escape" sekvence se může použít kdekoliv v řetězci; v našem příkladě je v řetězci pouze znak pro tab. (Jak zapíšeme zpětné lomítko v řetězci?)

Jak jsou postupně znaky a řetězce zobrazovány na obrazovce, jsou kontrolovány neviditelným hlídačem, zvaným kurzor, který zaznamenává jejich polohu.

Znak pro tabulátor posune kurzor doprava k nejbližší pomyslné zarážce. Tabulátory jsou vhodné pro tvorbu uspořádaných řádků, jako má tento výstup z předchozího programu:

>>>
1       2
2       4
3       8
4       16
5       32
6       64
7       128
8       256
9       512
10      1024
11      2048
12      4096
>>>

Protože mezi sloupci máme znaky tab, není pozice druhého sloupce závislá na počtu číslic v prvním sloupci.


5.5 Tabulky

Tabulka je uspořádání údajů, ve kterém je odečítáme v průsečíku řádku a sloupce. Dobrým příkladem je tabulka násobení. Řekněmež, že chceme zobrazit tabulku pro násobení čísel od 1 do 6.

Dobrým začátkem je zápis smyčky, která vytiskne všechny násobky dvou na jeden řádek:

i = 1
while i <= 6:
    print(2*i, "\t", end="")
    i += 1
print()

První řádek vytvoří proměnnou i, která působí jako počítadlo, neboli proměnnou smyčky. Při procházení smyčkou se hodnota i zvětšuje od 1 do 6. Při každém průchodu se vytiskne hodnota 2*i, následovaná mezerou tabelátoru.

Přechod na nový řádek ve funkci print potlačí instrukce end=" ". Po ukončení smyčky vytiskne funkce print() nový řádek.

Výstup programu je tento:

>>> 
2   4   6   8   10   12
>>>

Výborně, jen tak dál. Dalším krokem bude zapouzdření a generalizace.


5.6 Zapouzdření a zobecnění

Zapouzdření je proces zabalení kódu do funkce, umožňující jeho snadné opakované použití. Příklad zapouzdření jsme viděli v části   4.2.2 :   print_parity.

Zobecněním se míní přetvoření určité funkce, jako je například tvorba násobků dvou, tak aby tiskla násobky jakéhokoliv čísla.

Následující funkce zapouzdří předchozí smyčku a zobecní ji tak aby vracela násobky  n.

def print_multiples(n):
    i = 1
    while i <= 6:
        print(n*i, "\t", end="")
        i += 1
    print()

Všechno, co jsme pro zapouzdření museli udělat bylo to, že jsme přidali záhlaví funkce s parametrem  n.

Zavoláme-li tuto funkci pro argument 2, dostaneme stejný výstup jako předtím. Pro argument 3 obdržíme:

>>>
3    6    9    12   15   18
>>>

Pro argument 4 je výstup:

>>>
4    8    12    16   20   24
>>>

Nyní patrně už umíme odhadnout jak vytvořit tabulku pro násobení - opakovaným voláním print_multiples pro různé argumenty. Vlastně můžeme použít ještě jednu smyčku:

i = 1
while i <= 6:
    print_multiples(i)
    i += 1

Všimněme si, jak podobná je tato smyčka té, která je uvnitř print_multiples. Výměna funkce print voláním funkce print_multiples bylo vše, co jsme učinili.

Výstup tohoto programu je tabulka pro vzájemné násobení čísel 1- 6 :

>>>
1    2    3     4    5    6 
2    4    6     8    10   12
3    6    9     12   15   18
4    8    12    16   20   24
5    10   15    20   25   30
6    12   18    24   30   36
>>>

5.7 Další zapouzdření

Abychom si ještě jednou předvedli zapouzdření, vezměme kód z předcházejícího odstavce a zabalme jej do funkce:

def print_mult_table():
    i = 1
    while i <= 6:
        print_multiples(i)
        i += 1

Tento proces je běžná rozvojová metoda. Vytvářený kód zapisujeme do řádků mimo funkci, případně jej zkoušíme v IDLE či konzole interpreta. Jakmile nám chodí, zabalíme jej do funkce.

Tato metoda je zvláště užitečná, když na počátku ještě nevíme, jak program do funkcí rozdělit. Program tak vzniká postupným vývojem.


5.8 Další zobecnění

Pro další příklad zobecnění předpokládejme, že chceme program, který by vytiskl tabulku násobení jakékoliv velikosti, nikoliv pouze šest krát šest. Můžeme přidat parametr do funkce print_mult_table:

def print_mult_table(high):
    i = 1
    while i <= high:
         print_multiples(i)
         i = i + 1

Hodnotu 6 jsme nahradili parametrem high. Zavoláme-li print_mult_table(7), zobrazí se:

>>>
1    2    3    4    5    6 
2    4    6    8    10   12
3    6    9    12   15   18
4    8    12   16   20   24
5    10   15   20   25   30
6    12   18   24   30   36
7    14   21   28   35   42
>>>

To je prima, až na to, že asi chceme, aby byla tabulka čtvercová - se stejným počtem řádků jako sloupců. Zajistíme to přidáním dalšího parametru do print_multiples abychom určili, kolik sloupců by tabulka měla mít.

Tento parametr schválně nazveme high, abychom předvedli, že různé funkce mohou mít parametr se stejným jménem (stejně jako lokální proměnné). Zde je celý program:

def print_multiples(n,high):
    i = 1
    while i <= high:
        print(n*i, "\t", end="")
        i += 1
    print()

def print_mult_table(high):
    i = 1
    while i <= high:
        print_multiples(i, high)
        i += 1

Povšimněme si, že s přidáním nového parametru jsme museli změnit první řádek funkce (záhlaví funkce), a také jsme museli změnit místo, kde je funkce v print_mult_table volána.

Dle očekávání generuje program čtvercovou tabulku 7x7:

>>>
1    2    3    4    5    6    7
2    4    6    8    10   12   14
3    6    9    12   15   18   21
4    8    12   16   20   24   28
5    10   15   20   25   30   35
6    12   18   24   30   36   42
7    14   21   28   35   42   49
>>>

Provedeme-li zobecnění vhodným způsobem, často dostaneme program se schopnostmi, které jsme nezamýšleli. Například si lze všimnout, že v důsledku toho, že ab = ba, všechny hodnoty se v tabulce opakují dvakrát. Mohli bychom ušetřit inkoust kdybychom tiskli jen polovinu tabulky. Zařídíme to pouhou změnou jednoho řádku v print_mult_table. Změňme

print_multiples(i, high)

na

print_multiples(i, i)

a obdržíme

>>>
1
2    4
3    6    9
4    8    12   16
5    10   15   20   25
6    12   18   24   30   36
7    14   21   28   35   42   49
>>>

5.9 Newtonova metoda

Smyčky jsou často užívány v programech, které spočítají výsledek tak, že začnou s přibližnou hodnotou a opakovaným výpočtem výsledek zpřesňují.

Například, jedním ze způsobů počítání druhé odmocniny čísla je Newtonova metoda. Při určení odmocniny čísla n začneme vpodstatě libovolnou aproximací (přibližnou hodnotou), kterou zpřesníme pomocí tohoto vzorce:

better = (approx + n/approx)/2

Výpočet se opakuje tak dlouho, až se zpřesněná hodnota téměř neliší od předchozí. Pro provádění výpočtu napíšeme funkci:

def sqrt(n):
    approx = n/2.0
    better = (approx + n/approx)/2.0
    while better != approx:
        approx = better
        better = (approx + n/approx)/2.0  
    return approx
Volejme funkci pro argument 25, abychom se přesvědčili, že výsledek je 5.0.

Newtonova metoda je příkladem algoritmu - obecného řešení určitého problému (v našem případě počítání druhé odmocniny).


5.10 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.


5.11 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.

Následující ukázku si uložte do souboru ch05_doctest.py, který spustíte z příkazového řádku konzoly:

# ch05_doctest.py

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". Pokud narazí na rozpor, spustí rozsáhlé chybové hlášení, zejména doplníme-li evokaci skriptu atributem -v (verbose).

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

> python ch05_doctest.py 
******************************************************************
File "ch05_doctest.py", line 3, in __main__.is_divisible_by_2_or_5
Failed example:
    is_divisible_by_2_or_5(8)
Expected:
    True
Got nothing
******************************************************************
1 items had failures:
   1 of   1 in __main__.is_divisible_by_2_or_5
***Test Failed*** 1 failures.

5.12 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 (runtime stack).
zapouzdřit (encapsulate)
Zabalit část programu do funkce.
rozvojová metoda
Postup vývoje programu. V této kapitole jsme si předvedli způsob, založený na vytváření kódů pro jednoduché, specifické úkoly; kódy jsou potom zapouzdřeny a generalizovány.
 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.

5.13 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 odstavci 5.9 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ů.
previous up next hi end end