Příkazy cyklu — aneb umění opakování se

O čem si budeme povídat?

Na konci předchozí kapitoly jsme si vytiskli část tabulky násobků čísla 12. Ale stálo nás to mnoho psaní a pokud bychom potřebovali tabulku rozšířit, bylo by to velmi časově náročné. Naštěstí existuje lepší způsob a na něm si ukážeme opravdovou sílu, kterou nám programovací jazyky nabízejí.

Cyklus typu FOR

Nyní si ukážeme, jak v programovacím jazyku zapíšeme, že se něco má opakovat. Budeme dosazovat hodnotu proměnné a při každém opakování ji současně budeme zvyšovat. Zápis v jazyce Python vypadá takto:

>>> for i in range(1, 13):
...     print "%d x 12 = %d" % (i, i*12)
...

Poznámka 1: V příkazu range(1, 13) musíme použít 13, protože range() generuje čísla od dolní hranice včetně až po horní zadanou hranici vyjma. Na první pohled se vám to může zdát podivné, ale jsou pro to dobré důvody a časem si na to zvyknete.

Poznámka 2: Operátor for jazyka Python je ve skutečnosti operátorem, který bývá označován jako foreach (doslova pro každý). Následující posloupnost příkazů je provedena pro každý prvek kolekce. V našem případě je touto kolekcí seznam čísel generovaných funkcí range(). Můžete si to ověřit zapsáním příkazu print range(1, 13) na příkazový řádek interpretu jazyka Python. Uvidíte, co se vytiskne.

Poznámka 3: Řádek s příkazem print je odsazen více, než předcházející řádek s příkazem for. To je velmi důležité, protože tím překladači jazyka Python dáváme najevo, že chceme opakovat právě příkaz print. Mohou následovat i další odsazené řádky. Python bude pro každý prvek z kolekce opakovat všechny odsazené řádky. Nezáleží na tom, jak velké odsazení použijete, ale zvolenou hodnotu odsazení musíte dodržovat.

Poznámka 4: Pokud s překladačem jazyka Python pracujete v interaktivním režimu, spustí se program až po dvojím stisku klávesy Enter. Důvod spočívá v tom, že po prvním stisku překladač jazyka Python nepozná, zda chcete k posloupnosti opakovaných příkazů přidat další řádek, či nikoliv. Pokud stisknete klávesu Enter podruhé, Python předpokládá, že jste již dokončili vkládání příkazů a program spustí.

Takže jak uvedený program pracuje? Projdeme si jej krok po kroku.

Python nejdříve použije funkci range() pro vytvoření seznamu čísel od 1 do 12. Poté přiřadí proměnné i první hodnotu seznamu. Následuje provedení odsazeného kódu při použití hodnoty i = 1:

    print "%d x 12 = %d" % (1, 1*12)

Potom se Python vrátí zpět na řádek příkazem for a přiřadí i další hodnotu se seznamu, tentokrát 2. Opět se provede odsazený kus kódu, tentokrát s hodnotou i = 2:

    print "%d x 12 = %d" % (2, 2*12)

Python bude odsazenou posloupnost příkazů opakovat až do doby, kdy byly proměnné i přiřazeny všechny hodnoty seznamu. V ten okamžik, po provedení těla cyklu s poslední hodnotou v proměnné i, se provádění přesune na další příkaz, který není odsazen. V našem příkladu žádné další příkazy nemáme, takže dojde k ukončení programu.

Stejný cyklus v jazyce VBScript

<script type="text/vbscript">
For I = 1 To 12
    MsgBox I & " x 12 = " & I*12
Next
</script>

V tomto zápisu je mnohem lépe na první pohled vidět, co se děje. Hodnota I se mění od 1 až do 12 a provádí se kód uvedený před klíčovým slovem Next. V našem případě se výsledky jednoduše zobrazují v dialogovém okně. Odsazení příkazu MsgBox je nepovinné, ale díky němu se kód čte snadněji.

Poznamenejme, že ačkoliv zápis v jazyce VBScript vypadá jasněji, pythonovská verze je mnohem pružnější. Proč, to uvidíme za chvíli.

Poznámka překladatele: Zápis příkladu v jazyce Python můžeme přiblížit podobě příkladu v jazyce VBScript tím, že nevyužijeme výhod použití formátovacího řetězce:

>>> for i in range(1, 13):
...     print i, "x 12 =", i*12
...

Jednou z výhod použití formátovacího řetězce je ale například možnost předepsat, jak se má číslo naformátovat — tj. na kolik pozic se má tisknout. Vyzkoušejte:

>>> for i in range(1, 13):
...     print "%2d x 12 = %3d" % (i, i*12)
...

A totéž v JavaScript

Konstrukci for, která je běžná ve více programovacích jazycích, přebírá JavaScript z jazyka C. Vypadá takto:

<script type="text/javascript">
for (i = 1; i <= 12; i++) {
    document.write(i + " x 12 = " + i*12 + "<br>");
    }
</script>

Poznámka: Tato konstrukce má v kulatých závorkách uvedeny tři části:

Povšimněte si, že v JavaScript uzavíráme příkazy, které se mají opakovat (tělo cyklu) do složených závorek {}. Ačkoliv z technického hlediska je to vše, co musíme s tělem cyklu udělat, z praktického hlediska se považuje za vhodné, aby byl kód v závorkách odsazen. Zvyšuje se tím čitelnost zdrojového textu.

Tělo cyklu se provede pouze v případě, kdy je podmínka v testové části splněna. Každá z uvedených částí může obsahovat libovolný kód, ale výsledkem výrazu v testové části musí být boolovská hodnota.

Poznámka překladatele: Povšimněte si použití operátoru pro zvýšení proměnné cyklu o jedničku i++. V tomto případě se jedná o takzvaný postfixový operátor ++. To znamená, že je zapsán až za identifikátor proměnné. Kromě toho existuje prefixová varianta téhož operátoru ++i — operátor je uveden před identifikátorem proměnné. Oba zvýší obsah proměnné o jedničku. V čem se tedy jejich použití liší? V tomto případě v ničem, ale...

Operátory ++ a -- pocházejí z jazyka C. V něm celá řada příkazů může vystupovat jako výraz. To znamená, že vracejí nějakou hodnotu a mohou tedy být použity například na pravé straně přiřazovacího příkazu. Mimo jiné se to týká i samotného příkazu přiřazení a zmíněných operátorů pro zvýšení a snížení o jedničku. Místo jinak běžného zápisu...

a = 0;
b = 0;
c = 0;
d = 0;

...tedy můžeme psát:

d = c = b = a = 0;

V tomto okamžiku nabývá prefixová a postfixová varianta zmíněných operátorů svůj význam. Výsledkem postfixové varianty i++ je totiž původní hodnota proměnné i (nejdříve se získá hodnota a potom se provádí zvýšení o jedničku), zatímco výsledkem prefixové varianty ++i je nová hodnota proměnné i (nejdříve se provádí zvýšení o jedničku a pak se vrací hodnota proměnné i). Pokud v předchozím případě místo nuly použijeme další proměnnou, jejíž obsah upravujeme variantami operátoru ++, dostaneme po provedení příkazů na řádku v proměnných hodnoty, které jsou uvedeny v komentářích:

i = 1;
a = i++;    /* a: 1, i: 2 */
b = ++i;    /* b: 3, i: 3 */
c = ++i;    /* c: 4, i: 4 */
d = i++;    /* d: 4, i: 5 */

Při použití postfixové verze operátoru ++ musí překladač jazyka udělat jeden krok navíc — původní hodnotu proměnné si musí někam uložit (do registru nebo do jiné, skryté, pomocné proměnné) a teprve potom může provést zvýšení původní proměnné o jedničku. Pokud překladač neprování optimalizaci, pak je v případě, kdy se můžeme rozhodnout pro prefixovou nebo postfixovou variantu příkazu ++, vždy efektivnější použít prefixovou variantu, tedy ++i. Výše uvedený příklad cyklu v JavaScript bychom tedy měli psát spíše takto:

<script type="text/javascript">
for (i = 1; i <= 12; ++i) {
    document.write(i + " x 12 = " + i*12 + "<br>");
    }
</script>

Ale v jazyce Python musíme dát pozor! Ten totiž operátory ++ a -- vůbec nezná. A zatímco pro výrazy i++ nebo i-- zahlásí chybu, pro výrazy ++i nebo --i nezahlácí vůbec nic. Pokud jsme z jazyků rodiny C zvyklí používat ++i, budeme se divit, proč to nefunguje. Python jednoduše rozdělí jeden operátor na dvojici znamének. Na zápis se tedy dívá jako na výraz (+(+i)) nebo (-(-i)), což z matematického hlediska znamená prázdnou operaci.

Další informace o pythonovské konstrukci for

Pythonovský cyklus for prochází (říkáme také, že iteruje) přes všechny prvky posloupnosti. Posloupností v jazyce Python — pro případ, že byste zapoměli — je řetězec, seznam nebo n-tice. To tedy znamená, že můžeme psát cykly for, které zpracovávají libovolný ze zmíněných typů. Vytiskněme si na zkoušku jednotlivá písmena slova s využitím cyklu for aplikovaného na řetězec:

>>> for znak in 'slovo': print znak
...

Povšimněte si, že se každé písmeno vytiskne na jeden řádek. Povšimněte si také, že pokud se tělo cyklu skládá z jediného řádku, můžeme je napsat na ten samý řádek, za dvojtečku. Právě dvojtečka říká překladači jazyka Python, že bude následovat blok kódu.

Iterovat můžeme i přes n-tici:

>>> for slovo in ('jedno', 'slovo', 'a', 'zas', 'jine'): print slovo
...

Tentokrát se nám na řádcích objeví jednotlivá slova. Mohli bychom je samozřejmě při tisku spojit na jeden řádek. Využijeme triku s uvedením čárky na konci příkazu print. Pokud zde uvedeme čárku, nebude Python přecházet na další řádek a další tisk bude pokračovat tam, kde předchozí skončil. Poznámka překladatele: Každá čárka v příkazu print vygeneruje oddělovací mezeru. Platí to i pro čárku na konci příkazu print. Vypsaná slova tedy budou uvedena na jednom řádku a budou oddělena mezerou.

>>> for slovo in ('jedno', 'slovo', 'a', 'zas', 'jine'): print slovo,
...

Vidíte, jak se slova poskládala na jeden řádek?

Použití příkazu for nad seznamem jsme již viděli, protože dříve použitá funkce range() generuje právě seznam. Ale pro úplnost si uveďme příklad s přímo zapsaným seznamem:

>>> for prvek in ['jedna', 2, 'tri']: print prvek
...

S uvedeným typem cyklu, který slouží k průchodu všemi prvky, je spojen jeden zádrhel. V průběhu dostáváte kopii toho, co se v procházené kolekci nachází. Obsah kolekce nemůžete měnit přímo. Pokud kolekci modifikovat potřebujeme, musíme použít nevzhledný obrat, který do hry zatahuje indexy prvků v kolekci:

mujSeznam = [1, 2, 3, 4]
for index in range(len(mujSeznam)):
    mujSeznam[index] += 1
print mujSeznam

Uvedený program zvětšuje každou položku uvnitř mujSeznam o jedničku. Pokud bychom nepoužili trik s indexem, pak bychom pouze zvyšovali hodnoty okopírovaných prvků, ale neměnili bychom prvky originálního seznamu.

Poznámka překladatele 1: Ono to ve skutečnosti není tak přímočaré. Problematika modifikace seznamu souvisí s tím, že u některých objektů můžeme měnit hodnotu a u některých ne. Do seznamu se vždy vkládají odkazy na objekty. V uvedeném příkladu jsou těmito objekty celočíselné hodnoty, které nikdy nemůžeme měnit. Můžeme se na ně dívat jako na konstanty. Přičtením jedničky k číselné konstantě dostaneme jiné číslo — jinou konstantu, odkaz na zcela jiný objekt. Tento nový odkaz však s původním odkazem v seznamu nemá nic společného.

Pokud chceme dosáhnout toho, že se v seznamu objeví jiná čísla, musíme na příslušné pozice v seznamu uložit odkazy na jiné konstantní objekty s číslem. Situace by byla jiná, pokud bychom do seznamu zařadili objekty, které mohou během své existence měnit svůj stav. O tom ale až později.

Pokud této poznámce nerozumíte nebo vás děsí, nepropadejte panice. Je to úplně normální. Časem vám to bude jasné. Chtěl jsem jen, aby nad tím nesouhlasně nekroutili hlavou ti, kteří už tomu trochu víc rozumí.

Poznámka překladatele 2: V Pythonu verze 2.3 se objevil nový rys, kterému bychom měli při řešení podobného problému dávat přednost. Místo nepěkného obratu pro získání seznamu indexů bychom měli vždy použít mnohem elegantnější a také výkonnější verzi, využívající zabudované funkce enumerate():

mujSeznam = [1, 2, 3, 4]
for index, hodnota in enumerate(mujSeznam):
    mujSeznam[index] = hodnota + 1
print mujSeznam

Použitím funkce enumerate() se zajistí, že v každé obrátce cyklu získáme sobě odpovídající dvojici (index, hodnota). V uvedeném příkladu její složky přiřazujeme do stejnojmenných proměnných a následně používáme. (... Nepropadejte panice!)

Další problém s cykly typu for spočívá v tom, že nemůžeme rušit prvky kolekce, přes kterou procházíme. Došlo by ke zmatku. Podobá se to trochu situaci postavy ze starých grotesek, která odřezává větev, na níž sedí. K řešení podobných situací se lépe hodí jiný typ cyklu, o kterém si něco řekneme za chvíli. K porozumění problému bezpečného odstraňování prvků kolekce však budeme potřebovat znalosti z další tématické kapitoly, která je věnovaná větvení. Vysvětlení naznačeného problému tedy uvedeme později.

Od verze Python 2.2 byly do jazyka přidány další triky, které činí cyklus for ještě mocnějším. Budeme se jimi zabývat později. Prozatím stojí za to poznamenat, že i v jazycích VBScript a JavaScript existují konstrukce cyklu pro průchod všemi prvky kolekce. Detaily se zde zabývat nebudeme. Zápis konstrukce ve VBScript vypadá symbolicky takto: for each ... in .... Zápis v jazyce JavaScript vypadá takto: for ... in .... Pokud máte zájem, můžete detailní popis nalézt na odpovídajících stránkách s nápovědou.

Cyklus typu WHILE

Cykly typu FOR nepředstavují jediný možný typ konstrukce cyklu. A to je dobře, protože u cyklu FOR musíme vědět, nebo musíme být schopni předem vypočítat, počet prováděných iterací. Takže co máme dělat v případech, kdy chceme pokračovat v provádění určitého úkolu až do doby, kdy nastane určitá situace, ale když přitom nevíme, kdy k dané situaci dojde? Můžeme například chtít načítat a zpracovávat data ze souboru, ale předem nevíme, kolik datových položek soubor obsahuje. Chtěli bychom prostě pokračovat ve zpracování dat až do dosažení konce souboru. Lze k tomu sice použít i cyklus FOR, ale je to obtížnější.

K řešení tohoto problému se hodí jiný typ cyklu — cyklus typu WHILE.

Zápis v jazyce Python vypadá takto:

>>> j = 1
>>> while j <= 12:
...     print "%d x 12 = %d" % (j, j*12)
...     j = j + 1

Projděme si, co jednotlivé příkazy dělají.

  1. Nejdříve inicializujeme proměnnou j na 1. Nastavení počáteční hodnoty řídicí proměnné cyklu while představuje velmi důležitý první krok. Jeho opomenutí bývá častou příčinou chyb.
  2. Poté začneme provádět samotný příkaz while. V něm se vyhodnocuje boolovský výraz.
  3. Pokud je výsledkem výrazu hodnota True, dochází k provedení následujícího odsazeného bloku. V našem případě nabývá proměnná j hodnoty menší než 12, takže zahájíme provádění bloku kódu.
  4. Provede se příkaz print, který vytiskne první řádek naší tabulky.
  5. Na dalším řádku se zvyšuje (inkrementuje) hodnota řídicí proměnné j. V našem případě je to poslední stejně odsazený řádek, což znamená, že blok cyklu while končí.
  6. Vracíme se opět k příkazu while a provádíme kroky 4 až 6, vždy s novými hodnotami proměnné j.
  7. Uvedená posloupnost akcí se opakuje až do doby, kdy j dosáhne hodnoty 13.
  8. V tom okamžiku vrátí test cyklu while hodnotu False a provádění odsazeného bloku se přeskočí. Pokračovat se bude řádkem, který má stejné odsazení, jako řádek s příkazem while.
  9. V našem případě žádné další řádky nenásledují, takže program skončí.

V tomto okamžiku už by se vám to mohlo zdát docela jasné. Chtěl bych vás jen upozornit na jednu věc. Vidíte tu dvojtečku na konci řádku s příkazem while a před tím na konci řádku s for? Právě ta překladači jazyka Python říká, že bude následovat úsek kódu (blok). Jiné jazyky, jak uvidíme za chvíli, definují své vlastní způsoby, jak naznačit překladači skutečnost, že skupina řádků patří k sobě. Python používá kombinaci dvojtečky a odsazení.

VBScript

Podívejme se, jak vypadá zápis cyklu while v jazyce VBScript:

<script type="text/vbscript">
DIM J
J = 1
While J <= 12
    MsgBox J & " x 12 = " & J*12
    J = J + 1
Wend
</script>

Uvedený příklad produkuje stejné výsledky. Povšimněte si, že blok příkazů cyklu je tentokrát uzavřen klíčovým slovem Wend (což je samozřejmě zkratka pro While End). Až na tento rozdíl příklad funguje naprosto stejně, jako jeho pythonovská verze.

JavaScript

<script type="text/javascript">
j = 1;
while (j <= 12) {
    document.write(j, " x 12 = ", j*12, "<br>");
    j = j + 1;
    }
</script>

Jak vidíte, struktura programu je velmi podobná. Jen místo Wend (VBScript) se objevily složené závorky. Ani VBScript, ani JavaScript (na rozdíl od Pythonu) nevyžadují jakékoliv odsazování. Kód se odsazuje jen proto, aby byl čitelnější.

V JavaScript ještě stojí za to, abychom porovnali cykly for a while. Připomeňme, že cyklus for vypadal nějak takto:

for (j = 1; j <= 12; j++) {....}

Má tedy naprosto stejnou strukturu, jako cyklus while, jen s tím rozdílem, že je vše stlačeno do jednoho řádku. Jasně zde vidíme inicializační část, testovanou podmínku a část úprav pro další obrátku cyklu. Takže v jazyce JavaScript představuje cyklus for pouze kompaktnější formu cyklu while. Bez cyklu for bychom se mohli zcela obejít. Stačí nám pouze cyklus while. Některé jazyky volí právě takový přístup.

Poznámka překladatele: Podoba cyklu for je do JavaScript převzata z jazyka C. To, že kopíruje činnost cyklu while je známkou jeho nižší úrovně abstrakce. (Jazyk C je někdy nazýván vysokoúrovňovým assemblerem.) Porovnejte si stejný příklad opět s jazykem Python, kdy naopak musíme převést abstrakci jednoduchého čísla na posloupnost hodnot, abychom vůbec mohli konstrukci for použít. Pythonovský cyklus for je z jazykového hlediska modernější. Až poznáte všechny jeho možnosti, určitě nebudete ve většině případů dávat přednost cyklu while. Užitečnost cyklu pracujícího s vyššími abstrakcemi je také důvodem, proč VBScript a JavaScript definují i dříve zmíněné formy cyklu pro iteraci přes všechny prvky kolekce.

Pružnější zápis cyklů

Vraťme se zpět k naší tabulce násobení číslem 12 ze začátku této kapitoly. Cyklus, který jsme vytvořili, se pro tisk takové tabulky velmi dobře hodí. Ale jak by to bylo s jinými hodnotami? Mohli bychom cyklus upravit tak, aby produkoval tabulku násobků třeba číslem 7? Mělo by to vypadat nějak takto:

>>> for j in range(1, 13):
...     print "%d x 7 = %d" % (j, j*7)

Při úpravě jsme museli hodnotu 12 změnit na hodnotu 7 a to na dvou místech. A pokud bychom chtěli použít jinou hodnotu, museli bychom ji, opět na dvou místech, změnit znovu. Nebylo by lepší, kdybychom mohli nějakým obecnějším způsobem zadat požadovaného násobitele?

Můžeme toho dosáhnout tím, že místo konkrétní hodnoty použijeme další proměnnou. Hodnotu této proměnné nastavíme před zahájením cyklu:

>>> nasobitel = 12
>>> for j in range(1,13):
...     print "%d x %d = %d" % (j, nasobitel, j*nasobitel)

Takto získáme naši starou známou tabulku násobení číslem 12. Ale pokud nyní budeme chtít násobit sedmi, stačí, když změníme pouze hodnotu proměnné nasobitel.

Povšimněte si, že zde kombinujeme zápis posloupnosti příkazů a příkaz cyklu. Nejdříve jsme použili jednoduchý příkaz nasobitel = 12, za kterým následuje v pořadí další příkaz cyklu for.

Vnořené cykly

Použijme nyní předchozí příklad k dalšímu kroku. Dejme tomu, že chceme vytisknout všechny tabulky násobků čísel od 2 do 12 (násobení číslem 1 je příliš jednoduché než abychom se jím zabývali). Jediné, co musíme učinit, je použít proměnnou nasobitel jako součást dalšího cyklu:

>>> for nasobitel in range(2, 13):
...     for j in range(1, 13):
...         print "%d x %d = %d" % (j, nasobitel, j*nasobitel)

Všimněte si, že odsazená část uvnitř prvního cyklu for je zápisem přesně téhož cyklu, s kterým jsme začínali. Funguje to následovně:

  1. Nastavíme nasobitel na první hodnotu (2) a provedeme druhý cyklus.
  2. Poté hodnotu proměnné nasobitel změníme na následující hodnotu (3) a znovu provedeme vnitřní cyklus,
  3. a tak dále.

Tato technika je známa jako vnořování cyklů.

Drobnou nepříjemností je to, že se nám všechny tabulky spojí dohromady. Můžeme ji odstranit tím, že na konci prvního cyklu vytiskneme oddělovací čáru:

>>> for nasobitel in range(2, 13):
...     for j in range(1, 13):
...         print "%d x %d = %d" % (j, nasobitel, j*nasobitel)
...     print "-------------------"

Všimněte si, že druhý příkaz print je odsazený o stejnou hodnotu, jako řádek s druhým cyklem for — jde tedy o druhý příkaz v posloupnosti příkazů cyklu. (Prvním příkazem je zde vnořený cyklus.) Zapamatujte si, že úroveň odsazení je v jazyce Python velmi důležitá.

Jen pro porovnání uveďme, jak by to vypadalo v jazyce JavaScript:

<script type="text/javascript">
for (nasobitel = 2; nasobitel < 13; nasobitel++) {
    for (j = 1; j <= 12 ; j++) {
        document.write(j, " x ", nasobitel, " = ", j*nasobitel, "<br>");
        }
    document.write("---------------<br>");
    }
</script>

Pokuste se vytvořit oddělovač tabulek, který by říkal, jaká tabulka mu předchází — popis pod tabulkou. Nápověda: Pravděpodobně budete muset použít proměnnou nasobitel a pythonovský formátovací řetězec.

Ostatní typy cyklů

Některé jazyky umožňují více typů konstrukcí cyklu, ale obvykle podporují něco jako for a while. (Modula 2 a Oberon poskytují pouze cykly typu while, protože cykly for jimi můžeme nasimulovat — jak jsme viděli výše.) Jiné typy cyklů, se kterými se můžete setkat jsou:

do-while
Tento typ cyklu je stejný jako while, ale test se provádí až na konci, za tělem cyklu. To znamená, že se cyklus provede vždy alespoň jednou.
repeat-until
Podobá se předchozímu typu s tím, že logika testu je opačná.
GOTO, JUMP, LOOP, atd.
Lze se s nimi setkat hlavně ve starších jazycích. V kódu se obvykle definuje značka a skáče se přímo na takto označené místo.

Zapamatujte si

Pokud vás napadne, co by se dalo na překladu této kapitoly vylepšit, zašlete e-mail odklepnutím Tím budou do dopisu automaticky vloženy informace o tomto HTML dokumentu.

$Id: cztutloops.html,v 1.10 2005/07/22 22:37:19 petr Exp $