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 while
→for
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
kihagyjuk, 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...
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.
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.
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ó.
7 | 1 | 3 | 3 | 4 | 5 | 2 | 3 | 1 | 2 | 1 |
↑ |
7 | 1 | 3 | 3 | 9 | 5 | 1 | 3 | 1 | 7 | 1 | |
↑ |
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.
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 while
–else
és for
–else
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!
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
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!
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:
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)
Nem véges futási idő?
Vajon hányszor fut le a következő ciklus törzse?
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:
- Sorsoljunk listából, és vegyük ki.
- Keverjük meg a listát, és aztán az első öt.
Lista a számokkal 1-től 90-ig:
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, a 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:
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)))
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)
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!
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
try
–except
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.
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.
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.
Feladat: adott egy sztring. Szám van benne?
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!
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}.")
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}.")
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
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 ... ──┘
└─▸
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"]
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 try
–except
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.
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áv | bírságok |
---|---|
00:00–00:59 | 0 |
... | |
12:00–12:59 | 60000 |
13:00–13:59 | 230000 |
... |
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.
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.