További vezérlési szerkezetek

2. Elemek kihagyása: a continue utasítás

Ciklus folytatása a következő iterációval: continue

┌─▸ while vannak_elemek:
│       if ez_nem_kell:
└─────────  continue

        ciklustörzs ...
┌─▸ for tároló:
│       if ez_nem_kell:
└─────────  continue

        ciklustörzs ...

A continue utasítás visszaugrik a ciklustörzs elejére, a ciklusfeltétel ellenőrzéséhez. Általában az if utasítással együtt használjuk, a ciklustörzs további részének kihagyásához. Működik while és for ciklus esetén is. Ezzel néha egyszerűbbé, áttekinthetőbbé lehet tenni a vezérlési szerkezetünket.


Példa: elemek kihagyása

szamok = [1, 2, 0, 4]
i = 0
while i < len(szamok):
    if szamok[i] != 0:
        print(1/szamok[i])
    i += 1
szamok = [1, 2, 0, 4]

for x in szamok:
    if x == 0:
        continue
    print(1/x)

A fenti példa egy egyszerű ciklust mutat, amely a listában tárolt számok reciprokát írja ki. Ebben figyelni kell arra, hogy nullával ne osszunk – másképp fogalmazva, a 0-t ki kell hagyni.

A kód egyszerűsítése a whilefor transzformációval kezdődött. A for ciklus esetén nem nekünk kell törődni a listán iterálás megvalósításával, hanem az x változóban egyből a lista elemeit kapjuk.

Az átalakítást a „ha nem nulla, akkor kiírjuk” kódrészlet cseréjével fejezzük be. Az if x == 0: continue sor hatására a lista 0 értékű elemei kimaradnak, mivel ilyenkor a ciklus folytatódni fog a következő elemmel. Vagyis a ciklustörzs végét, azaz a print()-et kiagyjuk, a 0-val osztást elkerüljük.

Miért jobb így? Leginkább azért, mert a tényleges teendő (a reciprok kiírása) egy behúzással kintebbre kerül – pont eggyel bentebb van a ciklushoz képest, nem kettővel. Nagyobb kódnál ennek jobban kijön az előnye:

for x in szamok:
    if x != 0:
        sok...
        teendő...
        az...
        eltárolt...
        elemekkel...
for x in szamok:
    if x == 0:
        continue
    sok...
    teendő...
    az eltárolt...
    elemekkel...

3. Ciklus megszakítása: a break utasítás

Ciklus megszakítása: break

    while feltétel:
       ...
       if megvan:
┌───      break
│      ...
│
└─▸ ciklus után...
    for x in tároló:
        ...
        if megvan:
┌───       break
│       ...
│
└─▸ ciklus után...

A break utasítás megszakítja a ciklust, és a végrehajtás folytatódik a ciklustörzs után álló első utasítással. Bár ez nem hivatalos elnevezés, az ilyet szoktuk „középen tesztelő” ciklusnak is hívni, ugyanis a kiugrás tulajdonképp egy ciklusfeltételnek is felfogható. Viszont a while normál ciklusfeltételével szemben ez bárhol lehet a ciklustörzsben.

Ezt az utasítást is az if-fel együtt használjuk, mert másképp nincs értelme. Elég gyakran keresést vagy valamiféle várakozást megvalósító programokban jelenik meg: addig keresünk/várunk, amíg meg nem találtuk a kívánt elemet. Vagyis ha megtaláltuk az elemet vagy megtörtént az esemény, akkor megszakítjuk a várakozást.

4. Break utasítás az eldöntés tételében

Feladat: meghatározni, hogy szerepel-e egy adott tulajdonságú elem a listában.

Erre az eldöntés tételét használtuk:

szamok = [1, 2, 3, 4, 5]
vantalalat = False
i = 0
while i < len(szamok) and not vantalalat:
    if szamok[i] % 2 == 0:
        vantalalat = True
    i += 1

if vantalalat:
    print("Van páros szám")
else:
    print("Nincs páros")

A példában arra vagyunk kíváncsiak, van-e páros szám a listában. A programrész kimenete a vantalalat nevű változó értéke: ha abban True lesz a ciklus végére, akkor található páros szám a sorban, amúgy pedig nem.

A ciklusnak összetett feltétele van. Akkor kell futnia a ciklusnak, ha még van vizsgálandó szám ÉS még nem találtunk semmit (mindkettőnek teljesülnie kell). Emlékezzünk vissza a DeMorgan azonosságra is. A ciklus kilépési feltételét ezen két feltétel tagadásával és az ÉS művelet VAGY műveletre cserélésével fogalmazhatjuk meg. Akkor lépünk ki a ciklusból, ha nincs már több elem VAGY találtuk páros számot (elég az egyiknek teljesülnie). A ciklus után lévő if utasításban tulajdonképp azt is ellenőrizzük, hogy miért állt meg a ciklus. vantalalat == False esetén csak akkor állhatott meg, ha elfogytak a vizsgálandó számok.

vantalalat = False
i = 0
while i < len(szamok):  # egyszerűbb!
    if szamok[i] % 2 == 0:
        vantalalat = True
        break       # ciklus megszakítása
    i += 1

Tudjuk azt, hogy találat esetén már nem kell tovább vizsgálnunk a számsort. Ha legalább egy páros számot találtunk, akkor végeztünk. Mert nem az volt a kérdés, hogy hány ilyen van, hanem csak annyi, hogy van-e egyáltalán, ahhoz pedig elegendő egyetlen egyet találni. Vagyis találat esetén megszakíthatjuk a keresést a vantalalat = True sor után írt break utasítással. Miért egyszerűbb ez így? Azért, mert a ciklus feltételét így egyszerűsíthetjük. Mivel a break is megállítja a ciklust, ezért az and not vantalalat kódrészlet fölöslegessé válik, örökké igaz lenne.

Ezzel együtt egyébként a listát bejáró ciklus úgymond „szabályossá” vált, akár for ciklusra is cserélhető:

vantalalat = False
for x in szamok:
    if x % 2 == 0:
        vantalalat = True
        break

Felmerül a kérdés az előbb említett continue kapcsán, hogy nem cseréljük-e ki ezt az if-et is egy if ...: continue-ra. A válasz az, hogy nem, mert egy nagyon furcsa kódrészlet keletkezik belőle. Annyira furcsa, hogy csak áthúzva mutatjuk meg:

vantalalat = False
for x in szamok:
    if x % 2 != 0:
        continue
    vantalalat = True
    break

Miért furcsa ez? Azért, mert itt a vantalalat == True és a break sorok is mintha végrehajtódnának a számsor minden elemére: a ciklustörzs szintjén vannak. De ez nem igaz, nem hajtódnának végre, csak egyszer. A program teljesen félrevezető mások számára.

5. Lineáris keresés

Feladat: meghatározni, hogy hol van az első, adott tulajdonságú elem a listában.

A lineáris keresés nagyon hasonlít az eldöntéshez. Itt is egy adott tulajdonságú elemet keresünk. De nem az a kérdés, hogy van-e olyan, hanem hogy hol találjuk meg annak első előfordulását.

Az eldöntésnél nem törődtünk vele, hogy az i ciklusváltozó, amelyet listaindexnek használtunk, milyen értéket tartalmaz a ciklus megállása után. Helyette csak a van nevű változóval foglalkoztunk. Most viszont az i változó is fontos lesz: arra vagyunk kíváncsiak, hogy hol áll meg a ciklus.

Induljunk ki az előbbi eldöntés algoritmusból!

van = False
i = 0
while i < len(szamok) and not van:
    if szamok[i] % 2 == 0:
        van = True
    i += 1

Ezzel a klasszikus megvalósítással a keresés szempontjából az a gondunk, hogy találat esetén is még megnő a ciklusváltozó eggyel. Tehát ha van = True, akkor a találat az i - 1-edik helyen van. Tegyük ezért a tulajdonság vizsgálatát (páros-e) közvetlenül a ciklusfeltételbe!

i = 0
while i < len(szamok) and not szamok[i] % 2 == 0:
    i += 1

A not szamok[i] % 2 == 0 kifejezést kicsit egyszerűsítve ehhez jutunk:

i = 0
while i < len(szamok) and szamok[i] % 2 != 0:
    i += 1

if i < len(szamok):
    print("Az első páros szám helye:", i)
else:
    print("Nincs páros, végigment a ciklus")

Figyeljük meg: a ciklusból itt is két okból léphetünk ki. Egyik, hogy elfogynak a számok, nem találtunk semmit. Másik, hogy az épp vizsgált szám páros. A ciklusfeltétele ezeket tartalmazza, természetesen tagadva, mivel a while-nak belépési feltétele van, nem pedig kilépési feltétele. Belépünk a ciklusba, ha van még vizsgálandó szám, és a mostani szám nem megfelelő. A ciklustörzs lényegében üres, csak a következő elemre ugrást tartalmazza, mert oda csak akkor megyünk be, ha az aktuális szám nem jó.

71334523121
A ciklus megállása, ha van páros szám: megáll a találatnál
71339513171
A ciklus megállása, ha nincs páros szám: túlmegy a lista végén

A ciklust befejezve még rá kell jönnünk, hogy az összetett feltétel melyik oldala miatt állt meg. Ezt legegyszerűbben úgy dönthetjük el, hogy újból megvizsgáljuk az i < len(szamok) kifejezés igazságértékét. Ha ez igaz, akkor nem értünk a lista végére, tehát volt páros szám. Vagyis szamok[i] % 2 != 0 vált hamissá, mert az i-edik elem páros. Ha nem volt páros szám, akkor végigmentünk a listán, és i == len(szamok). (A szamok[i] % 2 == 0 kifejezést ilyenkor nem is szabadna kiértékelnünk, mivel túlindexelés lenne.)

Tudjuk ezt a break segítségével egyszerűsíteni? Igen, mert azzal a ciklus belsejéből is ki tudunk ugrani, közvetlenül a találat helyén. Így:

szamok = [1, 7, 5, 4, 5]
i = 0
while i < len(szamok):
    if szamok[i] % 2 == 0:
        break
    i += 1

if i < len(szamok):
    print("Az első páros szám helye:", i)
else:
    print("Nincs páros, végigment a ciklus")

Mivel a break segítségével kiugrunk a ciklusból, az i index növelése ugyanúgy elmarad. A célunk pedig ez volt.

6. Python különlegesség: while–else, for–else

Mi történik, ha a fenti kód kiírásait (üzeneteit) megpróbáljuk beépíteni a találat helyére?

A Python megengedi, hogy a while ciklusnak is legyen else ága:

    szamok = [1, 7, 5, 4, 5]
    i = 0
    while i < len(szamok):        ────────────────┐
        if szamok[i] % 2 == 0:                    │
            print("Az első páros szám helye:", i) │
┌───        break                                 │
│       i += 1                                    │
│   else:                                       ◂─┘
│       print("Nincs páros, végigment a ciklus")
└─▸

A találat helyének kiírását egyértelműen betehetjük a break elé. A „nincs találat” szöveg egy különleges helyre kerülhet, a while ciklus else ágába. Ez akkor fut le, ha nem ugrottunk ki break-kel a ciklusból. (Ez egyben azt jelenti, hogy break nélküli while után else-t írni értelmetlen.)

A whileelse és forelse ciklusok Python különlegességek, más programozási nyelvben nem is nagyon vannak. Vegyük észre, hogy ebből következik is valami: az, hogy minden probléma megoldható enélkül. Semmiképp ne erőltessük a használatát, csak akkor, ha tényleg egyszerűbb kódot kapunk általa!

7. Lottószámok

Feladat: generáljunk lottószámokat!

A programozás nyelvére fordítva: hozzunk létre egy listát, öt különböző számmal 1 és 90 között.


Az első ötlet

Hol a hiba?
import random

huzott = []
i = 1
while i <= 5:
    huzott.append(random.randint(1, 90))
    i += 1

print("Az e heti nyerőszámok:", huzott)

Hol a hiba? Ott, hogy a véletlenszámgenerátor adhatja ugyanazokat a számokat is. Előfordulhat, hogy az öt egymás után sorsolt szám között lesznek egyformák. Így azokat valahogy ki kellene szűrni. Próbálhatjuk úgy, hogy betesszük a számot a listába, aztán kivesszük, ha nem jó – de ilyet úriember nem csinál. Ennél sokkal jobb ötlet eleve be se tenni a számot a listába, ha már volt olyan .

A kérdés tehát ez: benne van a szám a listában? Implementáljuk az eldöntés algoritmusát!

Hol a hiba?
import random

huzott = []
i = 1
while i <= 5:
    sorsolt = random.randint(1, 90)
    # eldöntés
    found = False
    for x in huzott:
        if x == sorsolt:
            found = True
            break
    # ha nincs benne...
    if not found:
        huzott.append(sorsolt)
    i += 1

print("Az e heti nyerőszámok:", huzott)

És most hol a hiba? Ott, hogy mindig 5 számot próbálunk generálni, de nem mindig tesszük be őket a listába. Tehát előfordulhat, hogy 5-nél kevesebb szám lesz a végeredmény! Nem ötször kellene futtatni a ciklust, hanem addig, amíg öt szám nem lett a listában. Az átalakítás érdekessége, hogy az i változótól is meg tudunk szabadulni általa:

import random

huzott = []
while len(huzott) < 5:
    sorsolt = random.randint(1, 90)
    # eldöntés
    found = False
    for x in huzott:
        if x == sorsolt:
            found = True
            break
    # ha nincs benne...
    if not found:
        huzott.append(sorsolt)

print("Az e heti nyerőszámok:", huzott)

Ez már így jó. Már csak egy kicsit szépíteni kellene. Egyik, hogy az eldöntés algoritmusát valójában meg tudjuk spórolni. Pythonban van egy in nevű operátor, amely megadja nekünk, hogy egy elem szerepel-e egy listában:

in
operátor
huzott = [8, 12, 27, 45, 57]
print(57 in huzott)    # True
print(32 in huzott)    # False

Az in operátor által adott IGAZ/HAMIS érték tagadható is: not 57 in huzott. Ez valójában not (57 in huzott)-t jelent, tehát a vizsgálat eredményét tagadjuk. De nem szoktuk így írni, mivel a not in is használható operátornak. Például:

huzott = [8, 12, 27, 45, 57]
print(57 not in huzott)    # False
print(32 not in huzott)    # True

(Ne feledjük, ezzel az operátorral csak pontos egyezést tudunk keresni. Ha az lenne a kérdés, van-e negatív szám a listában, akkor kénytelenek lennénk saját algoritmust készíteni.)

Ezzel a kódunk sokkal rövidebbé válik:

import random

huzott = []
while len(huzott) < 5:
    sorsolt = random.randint(1, 90)
    if sorsolt not in huzott:
        huzott.append(sorsolt)

print("Az e heti nyerőszámok:", huzott)

Hogy tudjuk még szépíteni az eredményt? A nyerőszámokat növekvő sorrendben szokás megadni. A rendezést könnyen elvégezhetjük a sorted() nevű függvénnyel. Ez nem változtatja meg a neki paraméterként adott listát, hanem újat hoz létre. Legegyszerűbb felülírni a meglévő listánkat, mert bármit csinálnánk vele később, máskor is a rendezett formával lenne érdemes dolgozni. A végeredmény:

import random

huzott = []
while len(huzott) < 5:
    sorsolt = random.randint(1, 90)
    if sorsolt not in huzott:
        huzott.append(sorsolt)

huzott = sorted(huzott)
print("Az e heti nyerőszámok:", huzott)

8. Nem véges algoritmus?

Nem véges futási idő?

Vajon hányszor fut le a következő ciklus törzse?

Lépésszám = ?
huzott = []
while len(huzott) < 5:
    sorsolt = random.randint(1, 90)
    if sorsolt not in huzott:
        huzott.append(sorsolt)

Legalább öt lépés, de ez az algoritmus nem véges.

Mit jelent ez? Ha pongyolán fogalmazunk, azt mondjuk, hogy minden iterációban hozzáfűzünk egy számot a listához. Valójában ez nem igaz: minden iterációban legfeljebb egy számot fűzünk a listához. Legtöbbször egyet, de az is lehet, hogy egyet sem. Ha a véletlenszámgenerátor olyan számot sorsol, ami már van, akkor nem történik semmi, hanem újra megpróbáljuk. Általában olyan szám fog jönni, ami még nincs a listában, mert 90 közül 5-öt választunk csak ki. Az első lépés biztosan új számot ad (100% eséllyel). A második már csak 98,9% eséllyel (1/90, hogy rossz), a harmadik 97,8% eséllyel (2/90, hogy már volt), és így tovább.

Az iterációk maximális számát nem tudjuk garantálni, mert előfordulhat, hogy a véletlenszámgenerátor „rakoncátlankodik”, és sokszor ugyanazt a számot adja. Ez persze valójában nem rakoncátlankodás, hiszen teljesen normális az, ha többször ugyanaz a szám jön – mindegyiknek ugyanolyan esélye van, mint az összes többinek. De emiatt az iterációk számának legfeljebb a várható értékét tudjuk megmondani a valószínűségszámítás segítségével.


Lottósorsolás véges lépésszámban?

Van olyan megoldás erre a problémára, ami véges? Ötletek:

  1. Sorsoljunk listából, és vegyük ki.
  2. Keverjük meg a listát, és aztán az első öt.

9. Vegyük ki a listából!

Lista a számokkal 1-től 90-ig:

range()
osszes = []
i = 1
while i <= 90:
    osszes.append(i)
    i += 1

Az ilyen számlálásos ciklust egyszerűsíthetjük a range(1, 91) függvénnyel. Ez egy ún. range objektumot ad, amelyik előállítja az [1, 91) számsort (balról zárt, jobbról nyílt intervallumban). Ennek részleteibe nem fogunk belemenni a félévben, lényeg hogy úgy viselkedik, mint egy lista (az elemek tényleges előállítása nélkül), és így for ciklusban használható:

osszes = []
for i in range(1, 91):
    osszes.append(i)

De a generátor által adott 1...90 számsor könnyen listává is alakítható:

osszes = list(range(1, 91))

Ebből kivéve a számokat:

random.sample()
osszes = list(range(1, 91))
huzott = []
for i in range(0, 5):
    idx = random.randint(0, len(osszes)-1)
    huzott.append(osszes[idx])
    del osszes[idx]

A ciklustörzs első sorában generálunk egy véletlenszerű indexet a listához. Figyelni kell rá, hogy a lista egyre zsugorodik majd, úgyhogy az aktuális méretet kell mindig figyelni: len(osszes). És figyelni kell arra is, hogy a random.randint(min, max) függvény mindkét végén zárt intervallumban generál véletlenszámot; tehát pl. random.rand(1, 10) esetén az 1 és a 10 is lehetséges. Bezzeg a range(), ott balról zárt, jobbról nyílt az intervallum. Ez van, senki nem ígérte, hogy itt minden logikus és konzisztens lesz. ☺ Ha ez nagyon zavaró, használhatjuk esetleg a random.randrange() függvényt. A direkt úgy van kitalálva, hogy a paraméterezése megegyezik a range() függvényével; balról zárt, jobbról nyílt intervallum. Tehát a fenti kód kicsit egyszerűbben:

osszes = list(range(1, 91))
huzott = []
for i in range(0, 5):
    idx = random.randrange(0, len(osszes)) # !
    huzott.append(osszes[idx])
    del osszes[idx]   # elem törlése

Ha megvan a véletlen index, akkor áttesszük a huzott listába a számot, és ki is töröljük azt az összes közül. Ez a del lista[idx] utasítással lehetséges. Egyébként létezik del lista[ettől:] és del lista[:eddig] is. Ezekkel a lista eleje és vége törölhető. del lista[:] hatására a teljes lista kiürül.

Listából véletlenszerű elemeket választani a random.sample() függvény is tud. Vagyis ez éppen azt az algoritmust valósítja meg, amit mi is megírtunk az előbb. Ezzel a függvénnyel:

import random

osszes = list(range(1, 91))
huzott = random.sample(osszes, 5)

huzott = sorted(huzott)
print("Az e heti nyerőszámok:", huzott)

Még rövidebben?

Mivel lényegében nincsen szükség a változókra, a legrövidebb megoldás tulajdonképpen az alábbi. Bár itt a tömörítés már igencsak az olvashatóság kárára ment; az ilyesmit nem érdemes erőltetni.

import random

print("Az e heti nyerőszámok:", sorted(random.sample(range(1, 91), 5)))

10. Keverjük meg a listát!

A Fisher–Yates algoritmus (Durstenfeld verziója)

Hogy lehet egy listát megkeverni? Ehhez a következőt kell tenni:

  • Minden lépésben megfogni a sorozat egy véletlenszerűen választott elemét.
  • Kivenni a sorozatból.
  • Betenni egy másik sorozatba.
  • Ismételni ezt addig, amíg el nem fogy az első sorozat.

Ez az algoritmus előállítja az eredeti sorozat egy permutációját, sorrendezését. Amennyiben az áttett elemet egyenletes eloszlással választjuk ki (nem részesítjük előnyben a választásnál egyiket sem, egyenletes valószínűséggel választunk), úgy az előállított permutációk is mind egyformán valószínűek az összes lehetséges permutáció közül.

Az algoritmus megvalósításában most nem két listával fogunk dolgozni, hanem csak egyetlen eggyel, és annak cserélgetjük az elemeit. Az oszd meg és uralkodj elvet használjuk: a listának az első felét tekintjük kevertnek, a fennmaradó részéből válogatunk. Végigmegyünk a listán, és minden elemet megcserélünk a lista fennmaradó részéből választott elemmel.


Pszeudokód:

CIKLUS i = 0-tól n-2-ig:
    j = véletlenszám i ≤ j < n között
    csere: lista[i] ↔ lista[j]

Pythonban:

for i in range(0, len(lista)-1):
    j = random.randrange(i, len(lista))
    temp = lista[i]
    lista[i] = lista[j]
    lista[j] = temp

Vigyázat: A lista utolsó elemét már nem cseréljük semmivel (ezért volt a pszeudokódban „0-tól n-2-ig”). A cseréhez kiválasztott elem viszont lehet az utolsó elem is, aminek indexe len(lista)-1. De mindezeket a gondolatokat a range() és a random.randrange() függvénnyekkel fogalmazzuk meg, amelyek balról zárt, jobbról nyílt intervallumot várnak. Tehát az i-s ciklus a len(lista)-1 elemmel már éppen nem dolgozik, a j változó viszont még éppen fölveheti a len(lista)-1 értéket. Az i == j eset is lehetséges, hiszen az is a lehetséges keverések között van, hogy egy elem nem mozdul el a helyéről.

A cserét a háromlépéses csere algoritmussal végezzük. A középső sorban (lista[i] = lista[j]) látszik, hogy szükségünk van egy segédváltozóra. A lista i-edik elemét félre kell tennünk, különben felülírnánk, mindkét elem egyformává változna. Tehát az i-ediket félretesszük; aztán az i-edikbe mehet a másik, és végül a j-edikbe megy a félretett elem.

Mivel a lista ilyenkorra meg van keverve, a lottószámok sorsolása egyszerűen a lista első öt elemének kiválasztása.

import random

osszes = list(range(1, 91))

for i in range(0, len(osszes)-1):
    j = random.randint(i, len(osszes)-1)
    temp = osszes[i]
    osszes[i] = osszes[j]
    osszes[j] = temp

huzott = sorted(osszes[:5])
print("Az e heti nyerőszámok:", huzott)

Egyébként a random.shuffle() függvény pont erre képes: meg tud keverni egy listát. Most már értjük, hogy működik. Ezzel megvalósítva a lottósorsolást:

import random

osszes = list(range(1, 91))
random.shuffle(osszes)

huzott = sorted(osszes[:5])
print("Az e heti nyerőszámok:", huzott)

Kivételek kezelése

12. Kivételek keletkezése

Hiba keletkezése

x = int(input("Írj be egy számot: "))
print("Duplája:", 2*x)
Írj be egy számot: alma

Traceback (most recent call last):
  File "q.py", line 1, in <module>
    x = int(input("Írj be egy számot: "))
ValueError: invalid literal for int() with base 10: 'alma'

A szó nem alakítható int-té. Mindenképp megáll a program?

Valahogy kezelni kellene az ilyen hibát – pl. hogy újra meg lehessen próbálni a műveletet. Egy ilyen apróságtól nem kellene összeomlania a programnak!

13. Kivételek elkapása

A hiba keletkezésének pillanatában egy kivétel (exception) típusú objektum jön létre (an exception is raised). Ezt a kivételt el tudjuk kapni (catch), és így a hibát kezelni tudjuk (handling an exception). A kivétel elkapása a tryexcept kulcsszópárral történik. A try blokkba tesszük azt az utasítássorozatot, ahol probléma lehet. Hiba keletkezése esetén az except kulcsszóval megjelölt blokknál folytatódik a program.

Az alábbi ábrán a nyilak mutatják a lehetséges végrehajtási utakat. Ha sikerül a beolvasás, és a beolvasott adatot sikerül számmá alakítani, akkor a következő sorban folytatódik a végrehajtás, kiíródik a szám kétszerese. Ha nem, akkor viszont a hibaüzenet kiírásánál. Akármelyik is történt a kettő közül, végül a „program vége” szöveg ki fog íródni.

    try:
┌───    x = int(input("Írj be egy számot: "))    ────┐
└─▸     print("Duplája:", 2*x)                       │
    except:                                          │
        print("Nem számot kaptam.")                ◂─┘
    print("Program vége.")

Vagyis az elágazáshoz hasonlóan, a hibakezelés után a blokkok utáni közös résznél folytatódik tovább a végrehajtás. Ez természetesen azt is jelenti, hogy ha van olyan teendő, amit csak akkor szeretnénk végrehajtani, amikor minden rendben volt, az a try blokkban kell legyen. Ez nagyon fontos: épp ezért került a szám dupláját kiíró sor a try blokkba. Arra csak akkor szabad lépni, ha sikerült a beolvasás, és számot írt be a felhasználó.

Lentebb látható a program működése, mint egy nyomkövetőben.

Program

    try:
        x = int(input("Írj be egy számot: "))  
        print("Duplája:", 2 * x)  
    except:
        print("Nem számot kaptam.")  
    print("Program vége.")  

Működés

x: 

Látszik, hogy a kivétel elkapásával kezelni tudjuk a hibát. Nem áll le a programunk, hanem megadhatjuk, mi történjen.

14. A kivételobjektum

try:
    sor = input("Írj be egy számot: ")
    szam = int(sor)
    print("Duplája:", 2*szam)
except ValueError as e:     # volt adat, de nem értelmezhető
    print("Hiba:", e)
except EOFError:            # nem volt adat
    print("Fájl vége jel.")

Írj be egy számot: almafa
Hiba: invalid literal for int() with base 10: 'almafa'
Írj be egy számot: 
Fájl vége jel.

Az except kulcsszó után megadhatjuk, hogy milyen típusú hibákat szeretnénk elkapni (ezekről kicsit később), és hogy a hibát leíró kivételobjektumot milyen nevű változón keresztül szeretnénk elérni.

A kivételobjektum információkat tartalmaz a hibáról, és a keletkezés helyéről is. Jelen esetben, mivel egy számként értelmezhetetlen szöveget próbáltunk meg egész számmá alakítani, azt mutatja, hogy az int() függvény próbálta meg tízes számrendszerben értelmezni. Ezt jelképezi a ValueError típus.

Nem csak ez az egyetlen hiba, ami történhet. Hanem az is, hogy egyáltalán nincs mit beolvasni: EOFError. Ilyet akkor kapunk a programban, ha az input()-ban várakozás közben fájl vége jelet nyomunk a billentyűzeten (Windows: Ctrl+Z, Linux: Ctrl+D, vagy átirányítás esetén a bemeneti fájlnak lett vége. (Az átirányításokról később lesz szó.)

Látható, hogy az eltérő típusú hibákhoz különböző hibakezelő programrészeket adhatunk meg: csak egymás után fel kell sorolni őket, mindegyiket except kulcsszóval. Ha a kivételobjektumra nincs szükségünk, az as + változónév rész elmaradhat.

Ha többféle hibát is szeretnénk egy blokkban kezelni, akkor zárójelben kell a típusokat felsorolnunk: except (ValueError, EOFError). Ha pedig minden hibát el szeretnénk kapni, akkor except Exception-t kell írni:

try:
    x = int(input("Írj be egy számot: "))
    print("Duplája:", 2*x)
except Exception as e:
    print("Hiba:", e)

Hogy ez miért van így, annak objektumorientált programozással kapcsolatos vonatkozásai vannak. Erről most az első félévben még nem lesz szó, hanem majd második félévben, a Java kapcsán.

15. Kivételek típusai

Futási idejű hibák (általában)

  • ValueError: konverziók esetén, pl. int("almafa")
  • TypeError: hibás adatok a művelethez, pl. "alma" * "körte"
  • ZeroDivisionError: nullával osztás, pl. 1/0
  • IndexError: túlindexelés
  • EOFError: fájl vége
  • ...

Programozási hibák (általában)

  • ModuleNotFoundError: import nemlétezőmodul esetén
  • NameError: nem létező változó esetén
  • ...

A hibák kapcsán gyakran megkülönböztetjük a futási idejű hibákat és a programozási hibákat. A futási idejű hibák legtöbbször helytelen bemenet hatására keletkeznek, és a programkóddal nincsen baj. A programozási hibák pedig olyanok, amelyeket valahol a kódban javítani kellene. Ez persze csak pongyola definíció, és nem választható el a kettő egymástól élesen: beírhatjuk a programunkba, hogy print(1/0), és bár a nullával osztást általában futási idejű hibának tekintjük, ez a programsor mégsem lehet helyes soha.

Hibatípusból egyébként rengeteg van: csak a Python nyelv kb. 60-at határoz meg, de mi magunk is definiálhatunk újat. Mindenesetre legalább a fentieket érdemes fejből tudni. A további típusokról és azok kategorizálásáról itt lehet olvasni: Exception hierarchy.

16. Szám van-e a sztringben?

Feladat: adott egy sztring. Szám van benne?

Lehetőleg ne
erőltessük!
szoveg = input("Írj be valamit: ")

try:
    int(szoveg)     # az eredmény nem érdekes
    szam_volt = True
except:
    szam_volt = False

if szam_volt:
    print("Egész számként értelmezhető.")
else:
    print("Nem értelmezhető egész számként.")

Nagy ritkán előfordul, hogy szándékosan megpróbálunk egy kivételt kiváltani. Például itt: ha kíváncsiak vagyunk, a sztring számot tartalmaz-e, egyszerűen megpróbáljuk int-té alakítani azt. Ha számjegyek vannak a sztringben, akkor nem dob kivételt az int(szoveg) kifejezés, és a szam = True-hoz jutunk. Ha meg nem, akkor a szam = False-hoz.

Érdekesség, hogy az int(szoveg) kifejezés (függvényhívás) értékét nem használjuk semmire: nem tároljuk el változóban, nem is írjuk ki. Valójában nincs szükségünk rá a kérdés megválaszolásához, csak azért próbáltuk meg kiértékelni a kifejezést, hogy kiderüljön, elvégezhető-e.

Miért jelzi a cetli, hogy ne erőltessük ezt? Azért, mert a kivételkezelés arra való, hogy kivételes, váratlan helyzeteket oldjunk meg vele. Ha kíváncsiak vagyunk arra, hogy egy változó 0-e, akkor annak nem az a módja, hogy megpróbálunk osztani vele. Úgyhogy csak áthúzva:

x = int(input())
try:
    1/x
    print("A szám nem nulla.")
except:
    print("A szám nulla.")
:-(
x = int(input())

if x != 0:
    print("A szám nem nulla.")
else:
    print("A szám nulla.")

helyesen

Ennek semmi értelme, erre való az if. Egy másik példa, megállíthatjuk a lista bejárását úgy is, hogy elkapjuk a kivételt... De erre lenne való a len(szamok) vagy a for ciklus is. Úgyhogy szintén csak áthúzva:

szamok = [65, 98, 13, 27, 35]

i = 0
while True:
    try:
        print(szamok[i])
        i += 1
    except IndexError:
        break

:-(

szamok = [65, 98, 13, 27, 35]

i = 0
while i < len(szamok):
    print(szamok[i])
    i += 1


helyesen

Ne írjunk ilyeneket, és ha ilyet látunk, javítsuk ki!

17. Számsor beolvasása

Feladat: olvassunk be számokat, amíg sikerül!

Ez pont egy olyan helyzet, amikor várjuk, hogy a művelet előbb-utóbb sikertelen lesz. Nem tudjuk, hogy fog-e sikerülni a szám beolvasása, amíg meg nem próbáljuk. Vagyis a számsor beolvasása egy végtelen ciklusba kerül, amelyből azonban előbb-utóbb kiugrunk sikertelenség esetén:

    print("Írj be egész számokat, soronként egyet:")

    szamok = []
    try:
    ┌─▸  while True:
    │        szam = int(input())     ────┐
    └──────  szamok.append(szam)         │
    except (ValueError, EOFError):     ◂─┘
┌──      pass
│
└─▸ print(f"A beírt számok: {szamok}.")
Írj be egész számokat, soronként egyet:
1
2
x
A beírt számok: [1, 2].

Ez a ciklus meg fog állni akkor is, ha nem számot írunk be (ValueError), és akkor is, ha már egyáltalán nem érkezik adat (EOFError).

Nagyon fontos, hogy a kivétel hatására a ciklus is meg fog szakadni. Azért, mert a hibakezelő except blokk a cikluson kívül van. A végrehajtás mindig arra az except blokkra kerül, amelyik a hiba keletkezéséhez legközelebb van, de ebben az esetben az már a cikluson kívüli kódrészlet.

További újdonság a pass kulcsszó. Ezt akkor használjuk, amikor szintaktikailag a kódban szükség lenne egy utasításra, de mégsem szeretnénk semmit csinálni. Jelen esetben nincs teendő a beolvasási hiba esetén, így hibaüzenet sincsen. Vártuk is, hogy előbb-utóbb a számsornak vége legyen. Az except blokknak viszont ott kell lennie, mert az mindig párja a try blokknak.

Itt a kód még egyszer, kimásolható formában:

print("Írj be egész számokat, soronként egyet:")

szamok = []
try:
    while True:
        szam = int(input())
        szamok.append(szam)
except (ValueError, EOFError):
    pass

print(f"A beírt számok: {szamok}.")

18. Nem szám, próbáld újra!

Feladat: kérjünk egy számot. Ha nem szám, próbáljuk újra!


    while True:                                ◂──┐
        try:                                      │
            sor = input("Írj be egy számot: ")    │
            szam = int(sor)    ───┐               │
┌───        break                 │               │
│       except ValueError:     ◂──┘               │
│           print("Nem szám. Próbáld újra!")    ──┘
│   
└─▸ print(f"A beírt szám: {szam}.")
Írj be egy számot: alma
Nem szám. Próbáld újra!
Írj be egy számot: 5
A beírt szám: 5.

Ebben a vezérlési szerkezetben bár látszólag végtelen ciklus van, de ideális esetben annak a törzse csak egyetlen egyszer fog végrehajtódni. Először beolvas egy sort, aztán számmá alakítja, végül pedig meg is áll.

A ciklus törzse csak akkor fog ismétlődni, ha a beolvasott sort nem sikerült számmá alakítani. Ilyenkor az int(sor) kifejezés kivételt dob, és a hibakezelésre ugrunk. A hibaüzenet megjelenítése után pedig folytatódik a ciklus a következő iterációval, mivel abban az ágban nincsen break, a ciklusfeltétel pedig örökké igaz.

Kicsit a végtelen ciklus emiatt félrevezető. A következő előadásban meglátjuk, hogyan lehet egy ilyen kódrészletet elrejteni egy függvény belsejében.

Itt a kód még egyszer, kimásolható formában:

while True:
    try:
        sor = input("Írj be egy számot: ")
        szam = int(sor)
        break
    except ValueError:
        print(f"Nem szám: {sor}. Próbáld újra!")

print(f"A beírt szám: {szam}.")

Összetett példa

20. Autópálya

Az M7 autópályán traffipax méri az autók sebességét. Minden mérésről három adatot ad: óra:perc sebesség. Pl. 9:39 125 jelentése az, hogy 9:39-kor egy autó 125 km/h-val ment:

9:39 125

A 140 km/h feletti sebességért 30.000 Ft a bírság, a 180 km/h felettiért 100.000 Ft. Az adatsor több nap adatait tartalmazza, és a végét üres sor zárja.


Olvassa be a program ezeket az adatokat, és írja ki, hogy a nap mely órájában mennyi az összes kirótt bírság! Példa kimenet:

12:00-12:59, 60000 Ft
13:00-13:59, 230000 Ft

21. Autópálya – adatok beolvasását hogyan?

Hogyan olvassuk be az adatokat? Hát, nem így:

ora, perc, sebesseg = int(input())
9:39 125
ValueError: invalid literal for int() with base 10: '9:39 125'

Az így beolvasott sort nem konvertálhatjuk egész számmá. Pedig az int() konverzió csak ennyit tudna. Először is, nem egy szám van a beolvasott sztringben, hanem három, hiszen a teljes sort megkapjuk az input() függvénytől. Másodszor pedig, az elválasztó karakter hol kettőspont, hol meg szóköz. Nem véletlen, hogy ez a konverzió kivételt dob. De ugyanez történne az üres sor esetén is, ami a bemenet végén van: az üres sztringre hívott int() konverzióra is kivétel a jutalmunk.


Tehát előbb meg kellene néznünk, hogy üres sort kaptunk-e, és csak utána értelmezni a benne lévő adatokat. És az adatokat is úgy kell feldolgoznunk, hogy előbb feldaraboljuk a sztringet három darabra (óra, perc, sebesség), és csak az így kapott darabokat próbáljuk egész számmá konvertálni.

Beolvasás üres sorig:

  print("Írd be az adatokat, üres sorig: ")
  while True:            ◂──┐
      sor = input()         │
      if sor == "":         │
┌───      break             │
│     ... feldolgozás ... ──┘
└─▸ 

22. Autópálya – a sztring darabolása

A .split() függvény

A beolvasott sztringet a .split() függvénnyel törhetjük darabokra. Ez egy sztringből listát csinál, felvagdalva a megadott karakter mentén a sztringet:

sor = "alma körte barack"

szavak = sor.split()    # ["alma", "körte", "barack"]

print(szavak[0], szavak[1], szavak[2])

A jelenlegi formátumban viszont az egyik helyen kettőspont, a másik helyen szóköz választja el az adatokat. Ezért egymás után két .split()-et fogunk használni. Az egyik a szóköz mentén kettétöri a bemeneti sztringet, így megkapjuk külön az időpontot és a sebességet. A másik pedig az időpontot töri ketté órára és percre.

sor = "9:39 125"

idopont_sebesseg = sor.split()  # ["9:39", "125"]

idopont_sebesseg[0]             # "9:39"

ora_perc = idopont_sebesseg[0].split(":")   # ["9", "39"]

23. Autópálya – a sor beolvasása

A teljes beolvasás, kiegészítve hibakezeléssel:

    print("Írd be az adatokat, üres sorig: ")
    while True:                                    ◂──┐
        sor = input()                                 │
        if sor == "":                                 │
┌───        break                                     │
│       try:                                          │
│           idopont_sebesseg = sor.split()            │
│           ora_perc = idopont_sebesseg[0].split(":") │
│           ora = int(ora_perc[0])                    │
│           perc = int(ora_perc[1])                   │
│           sebesseg = int(idopont_sebesseg[1])       │
│       except:                                       │
│           print("Hibás bemenet:", sor)              │
│           continue                                ──│
│                                                     │
│       ... feldolgozás ...                         ──┘
└─▸ print("Összes adat beolvasva.")

A tryexcept blokk ebben az esetben minden olyan hibát elkap, ami a sor darabolása vagy a konverziók közben történik. Például ha nincs elegendő sztring, a bejövő adat nem tartalmazott kettőspontot vagy szóközt: "9 39 125"; ebben az esetben a túlindexelések miatt, mert a .split()-ek által adott listák túl rövidek lesznek. De akkor is, ha a konverzió sikertelen: "alma:körte barack" ugyan a megfelelő helyeken darabolható, de az eredmény sztringekből nem lesz szám.

Vannak egyébként olyan hibák, amelyeket ilyen módon nem sikerül érzékelni. Vajon mik lehetnek azok?

Megoldás

Azt, ha túl sok adat van, például "9:39 125 alma". Ilyenkor minden darabolás és konverzió sikeres. Vagy például a "9:39:57 125" sztring is ilyen. Legjobb lenne ellenőrizni, hogy pontosan akkora lett-e a kapott lista, amekkora kell:

        if len(idopont_sebesseg) != 2 or len(ora_perc) != 2:
            raise ValueError()

És ekkor még mindig nem vizsgáltuk, hogy nem kaptunk-e negatív percet vagy sebességet. A bemenet ellenőrzése általában nem könnyű feladat, és gyakran azt vesszük észre, hogy a sok hibalehetőség kezelése hosszabb programkódot eredményez, mint a megoldandó feladat, ha tényleg minden hibalehetőségre fel akarunk készülni. Később lesz szó más eszközökről, amelyekkel az ilyen problémák egyszerűbben megoldhatóak.

24. Autópálya – az adatszerkezet

Lássuk újra a feladat szövegét!

[...] A 140 km/h feletti sebességért 30.000 Ft a bírság, a 180 km/h felettiért 100.000 Ft. [...]
[...] írja ki, hogy a nap mely órájában mennyi az összes kirótt bírság [...]

12:00-12:59, 60000 Ft
13:00-13:59, 230000 Ft

Mire utal ez? Az első fontos dolog, amit észre kell vennünk, hogy egy összegzést kell csinálni. A kimeneten csak a bírságok összegére vagyunk kíváncsiak, ezért a bemenő adatsort nem fogjuk eltárolni. Másik pedig, hogy a bírságokat óránként, vagyis órás bontásban kell meghatároznunk. Méghozzá a nap 24 órájában; ugyan az nincs odaírva, hogy a nap 24 órából áll, de ezt mindenki tudja.


Az adatszerkezetünk egy olyan tömb lesz (fix méretű lista), amelyet kezdetben 24 darab nullával töltünk fel.

idősávbírságok
00:00–00:590
...
12:00–12:5960000
13:00–13:59230000
...

A leképezés módja ebben az esetben egyszerű: az óra adja a tömbindexet. Vagyis a birsagok[ora] kifejezéssel érjük majd el a keresett adatot.

25. Autópálya – feldolgozás és eredmény

birsagok = [0] * 24

while True:
    ... adatok beolvasása ...
    
    if sebesseg >= 180:
        birsagok[ora] += 100000
    elif sebesseg >= 140:
        birsagok[ora] += 30000
    else:
        pass

for ora in range(0, 24):
    print(f"{ora:02}:00-{ora:02}:59, {birsagok[ora]} Ft")
12:00-12:59, 60000 Ft
13:00-13:59, 230000 Ft

Néhány megjegyzés a kóddal kapcsolatban:

Mit jelent a pass utasítás?
Ez az ún. „üres utasítás”, amelyik nem csinál semmit. Az egész else ág elhagyható lenne, mivel tulajdonképp üres. Most csak azért van ott, mert így kicsit szebb a kód: egyértelműsítve van, hogy átgondoltuk azt az esetet is, amikor 140-nél kisebb a sebesség, csak olyankor nincs teendő.
Kell az elif a 140-nél, vagy mindegy?
Fontos, hogy ez a 180-as elágazásnak a hamis ágába kerüljön. Mert különben pl. egy 190-es sebességnél kirónánk a 180 felettieknek járó 100 ezer forintot, és a 140 felettieknek járó 30 ezret, vagyis összesen ilyenkor 130 ezer adódna. A feladat pedig nem ezt kérte.
Mit csinál a {ora:02}az f sztringben?
Ez teszi ki az ún. vezető nullákat a formázásnál: f"{23:05}" értéke "00023". Összesen 5 karakter, és az üres helyek nullákkal vannak kitöltve.
Ami nincs odaírva: hol használjuk fel a perc adatot?
Sehol. Van ez így, hogy nincs minden ismert adatra szükség. Viszont a sztring darabolásakor foglalkoznunk kell vele, hiszen a bemeneten mindenképp ott lesz.

A teljes program letölthető innen: autopalya.py.