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.11 2005/10/20 20:52:28 petr Exp $