A program és a külvilág

2. Fájlok kezelése: szövegfájl írása

A fájlok kezelése az open függvény által visszaadott, IOBase típusú objektum segítségével lehetséges (input/output base). Nézzük a legegyszerűbb esetet!

f = open("hello.txt", "wt")  # megnyitás

f.write("Helló, világ!\n")
for i in range(1, 10 + 1):
    f.write(f"{i} ")

f.close()                     # bezárás
Helló, világ!
1 2 3 4 5 6 7 8 9 10
  • Megnyitás módja: írás (w) vagy olvasás (r).
  • A fájl típusa: szöveges (t) / bináris (b).

A fájlt az open() függvénnyel lehet megnyitni. Ennek a két legfontosabb paramétere egy-egy sztring. Az első paramétere a fájl neve, második pedig a használat módja, amelyben betűk kódolják, hogy mit szeretnénk csinálni. Jelenleg egy szövegfájlt (t) nyitunk meg, hogy írjunk bele (w).

A fájl objektumot eltároljuk egy változóban, amely itt f névre hallgat. Ha végeztünk a feladatunkkal, az objektum .close() függvénnyel zárhatjuk be a fájlt. Ez kötelező! Ha nem tesszük meg, akár adatot is veszíthetünk; előfordulhat, hogy a legutóbbi írási műveletek eredménye nem kerül ténylegesen a fájlba.

A fájlba írni annak .write() függvényével tudunk. Ügyelni kell arra, hogy ennek egyetlen egy paramétere lehet csak, amely egy sztring. Nem kaphat több adatot, mint a print() függvény; sőt a sorokat elválasztó \n karaktereket is nekünk kell jeleznünk. Legegyszerűbb ezért egy szöveget tartalmazó fájl előállításakor sztringformázást használni. Ha olvassuk az adatokat a fájlból, akkor a .readline() függvényt használjuk; erről lentebb lesz szó.

Előbb viszont még néhány apróság az open() függvény paraméterezéséről. Először is, ügyelni kell a fájlnevek megadására. Windowson az elérési útban \ az elválasztó: C:\Windows\hatter.bmp, Unixon / van: /usr/bin/firefox. Az open() mindig elfogadja a /-t. Ha a \-hez ragaszkodunk, azt viszont \\-nek kell írni a sztring belsejében, mivel a \ önmagában a speciális karaktereket jelöli (pl. \n).

A második paraméter a megnyitás módját mutatja. Ebből az első betű mindig azt jelöli, hogy a fájlba írni szeretnénk, most hozzuk létre; w = write. Vagy esetleg egy meglévő fájlból olvasnánk: r = read. Az olvasáson (r) és íráson (w) kívül létezik még két további megnyitás mód is:

  • hozzáfűzés (a). Ilyenkor a fájlt írásra nyitjuk meg, de a meglévő tartalmát meghagyva. Az írás mutató a fájl végére mutat, vagyis a meglévő tartalomhoz hozzáadva lehet folytatni az írást.
  • írás-olvasás (+). Ilyenkor írni és olvasni is lehet. Más betűkkel együtt használjuk: pl. r+ azt jelenti, hogy a fájl tartalma megmarad, de írni is lehet bele.

Az írás vagy olvasás kiválasztása mellett meg kell adnunk azt is, hogy szöveges vagy bináris fájlról beszélünk. A kettő közötti különbség leginkább úgy érthető meg, ha a kettőt egy példán keresztül összehasonlítjuk, ugyanis ugyanaz az adat eltárolható szöveges és bináris fájlban is.

Tegyük fel, hogy adott egy int változónk, legyen a benne tárolt érték 12345. Ha ezt szövegesen írjuk egy fájlba, akkor az 1, 2, 3, 4, 5 karakterek ASCII kódja kerül a fájlba (összesen 5 bájt). Ha binárisan, akkor pedig annyi bájtot írunk ki, ahány bájtot a számítógépünkon az int változók foglalnak; és pontosan azokat a bájtokat, amik a változó memóriaterületén vannak. Ezek lehetnek pl. az 57, 48, 0, 0 bájtok (ilyen sorrendben), mert 57 + 48*256 + 0*65536 + 0*16777216 = 12345.

Lényegében tehát, a szövegfájl „emberi fogyasztásra is alkalmas”, a bináris fájl pedig nyers memóriaképet tárol.

3. Fájlok kezelése: szövegfájl olvasása

Tegyük fel, hogy adott egy szövegfájl, benne egy futóverseny eredményeivel. Minden sor egy helyezést tartalmaz, azon kívül pedig a nevet.

Az alábbi kód az első sor beolvasását mutatja.

Egy sor beolvasása részleteiben:

1  Am Erika
2  Dil Emma
3  Break Elek
...
10 M. Ágnes
11 Ó Pál
...
f = open("verseny.txt", "rt")

sor = f.readline()          # "1 Am Erika\n"

sor = sor.rstrip("\n")
darabok = sor.split(" ", 1) # ["1", "Am Erika"]
v = Versenyzo()
v.helyezes = int(darabok[0])
v.nev = darabok[1]

f.close()

Először is, az előbb ismertetett módon megnyitjuk a fájlt: olvasunk belőle, r = read, és szövegfájlról van szó, t = text. Bezárni is ugyanúgy kell majd a fájlműveletek végén. Másodszor pedig, a fájlból egy sort a .readline() függvénnyel kapunk meg. Ezt fel kell dolgozni, vissza kell nyerni belőle az adatokat, mert az eredeti adatainkat a fájlba íráskor mind sztringgé alakítottuk.

  • A kapott sztring az újsor karaktert is tartalmazza. A beolvasott sztring így néz ki: "1 Am Erika\n". Bár a .readline() függvény egy sort olvas, mégis beteszi a sztring végére az elválasztó karaktert. Ennek két oka van: 1) a fájl végén, a legutolsó sorban már lehet, hogy nincs ilyen, és 2) a .readline() függvénynek opcionális paraméterben megadhatjuk azt, hogy maximum hány karaktert olvasson. Ha a sor hosszabb, akkor nem lesz \n a végén.
  • Lényeg a lényeg, hogy ezt a karaktert le kell vágnunk, mert nem tartozik a névhez. Legegyszerűbben ez az .rstrip() (right-strip) függvénnyel tehető meg, amely az összes entert eldobná a sztring végéről. Ezután a sztringünk már ilyen: "1 Am Erika".
  • Következő lépésként különválasztjuk a helyezést és a nevet egymástól. Ezt a .split() függvénnyel tehetjük meg, amelyik a sztringet a megadott karakterek mentén darabokra töri, és a darabokat visszaadja egy listában. A levágott darabok számát is megadhatjuk paraméterként. Ez fontos is, mert szóköz nem csak a helyezés után van, hanem a névben is. Sőt a név több szóból is állhat (pl. két keresztnév). A darabok nevű lista tartalma ezek után: ["1", "Am Erika"].
  • Végül pedig, ügyelnünk kell arra, hogy a helyezést a programunk egész számként kell kezelje, a fájlból visszaolvasva viszont sztringünk van. Ezért az int() konverzióval egész számmá alakítjuk, mielőtt a versenyző objektumba írnánk.

A fájl végére érve egyébként a .readline() függvény üres sztringet ad. Ez nyilvánvalóan különbözik a fájl közepén lévő üres sortól, amit az egyetlen újsort karaktert tartalmazó sztring reprezentál: "\n". Így a kettőt meg tudjuk különböztetni egymástól.

Általában a fájlban több adatsor van:

for sor in f:               # iterálható, sorokat ad
    sor = sor.rstrip("\n")
    ... sor feldolgozása ...

Ha a fájlból több egyforma adatsort szeretnénk beolvasni (egészen addig, amíg a végére nem érünk), akkor legegyszerűbb, ha iterálunk rajta. A for ciklus ugyanis működik fájlokon is; a fájl sorait kapjuk meg általa a ciklusváltozóban. Persze ezek a sorok ugyanúgy tartalmazzák az enter karaktert, amit le kell vágni.

Ez a módszer csak akkor tud működni, ha soronként kell haladnunk.

4. Fájlok – hibakezelés

Feladat: Egy fájl egész számokat tartalmaz. Adjuk össze ezeket!

78
345
99
37
1268
77
85
    try:
        f = open("szamok.txt", "rt")
        osszeg = 0
        while True:
            sor = f.readline() # "", ha vége
            if sor == "":
                break           ───┐
┌───        osszeg += int(sor)     │
│       f.close()               ◂──┘
│       print("Az összeg:", osszeg)
│   except ValueError as e:
└──▸    f.close()
        print("Hibás bemenet:", sor)

A feladat megoldásának fájlkezelés része viszonylag egyszerű. Szövegfájlról van szó, azaz "rt" módban nyitjuk meg a fájlt. Ezek után végighaladunk rajta soronként. Üres sor esetén kijövünk a ciklusból, hogy kiírjuk az összeget; amúgy pedig hozzáadjuk az osszeg nevű akkumulátorhoz. (A sor végéről lecsapott enterrel itt nem kellett foglalkozni, mivel az int() konverziót az nem zavarja.)

Képzeljük el, hogy a fájl tartalmaz egy hibás sort, amelyben nincs szám, vagy nem szám van benne. Ekkor az int() konverzió hibát dob. Hogy a hibát tudjuk jelezni (és a hibás sort is meg tudjuk mutatni a felhasználónak), a hibát elkapjuk. A normál kiíráshoz, az összeg megjelenítéséhez csak akkor jutunk, ha nem dobódott kivétel. Ez örömteli, mert hibás fájl esetén úgysem lenne értelme a részösszeg kiírásának.

A gond csak ott van, hogy a fájlt be kell zárnunk. Ez kötelező, elmaradhatatlan művelet, mivel a nem bezárt fájl foglalja az operációs rendszer erőforrásait (resource). Ez talán akkor még megengedhető lenne, ha ezzel vége is lenne a programunknak, de ha fut tovább, akkor biztosan helytelen. Mivel a kivétel keletkezése miatt az első f.close()-t is átugrottuk, ezért a hibakezelés helyén meg kell ismételnünk a sort, hogy akár volt hiba, akár nem, a fájlt biztosan bezárjuk.

Kérdés az, hogy nincs-e erre egyszerűbb megoldás. Nem tudjuk-e valahogy azt jelezni a nyelvnek, hogy normál esetben, ha minden rendben volt, akkor is szeretnénk futtatni az f.close() sort; és hiba esetén, vagyis kivétel keletkezése esetén is.

5. Kivételkezelés: a finally blokk

Szerencsére van ilyen. Az opcionális finally blokk egy tryexcept hibakezelő blokkot zárhat le. Ide olyan programrészt írhatunk, amelyik lefut mindkét esetben: ha volt hiba, akkor is, és ha nem volt, akkor is.

finally:
mindig lefut
    try:
        f = open("szamok.txt", "rt")
        osszeg = 0
        while True:
            sor = f.readline()
            if sor == "":
                break               ───┐
┌───        osszeg += int(sor)         │
│       print("Az összeg:", osszeg) ◂──┘
│                                   ───┐
│   except ValueError as e:            │
└──▸    print("Hibás bemenet:", sor)   │
┌───                                   │
│   finally:                           │
└──▸    f.close()                   ◂──┘

A finally blokk épp erre való: hogy olyan „takarítás” jellegű műveleteket végezzünk el benne, amit mindenképpen, hibától függetlenül el kell végezni.

A bal oldalon látható nyilak mutatják a vezérlési utat hiba esetén. Ha nem sikerül az int(sor) konverzió, akkor előbb a hibaüzenet kiírásához jutunk, onnan pedig a finally blokkba. Ha minden rendben van, akkor a ciklusból a break utasítással jövünk ki; megjelenik az összeg, és onnan pedig megyünk a finally blokkhoz. Akárhogy is, a fájlt mindenképp bezárjuk.

A finally blokkról még két dolgot érdemes tudni. Egyik, hogy ez önmagában is állhat, nem szükséges except blokknak megelőznie. Másik pedig, hogy ez a blokkból kilépés módjától függetlenül le fog futni. Ezekre mutat példát a következő kódrészlet, amelyik ugyanezt a feladatot oldja meg, de paraméterezhető függvény formájában; a paraméter a fájl neve:

def fajlban_szamok_osszege(fajlnev):
    try:
        f = open(fajlnev, "rt")
        osszeg = 0
        for sor in f:
            osszeg += int(sor)
        return osszeg   # !
    finally:
        f.close()

Ez a függvény nem ír semmit a kimenetre (nem dolga). Csak visszaadja az összeget, vagy hiba esetén kivételt dob. Annak ellenére, hogy nincs except blokk, sőt annak ellenére, hogy látszólag az f.close() később van, mint a return osszeg, a fájl mégis bezáródik. A finally blokkba beleértjük még az ilyen esetet is. Ez még akkor is így történik, ha egy except blokkban másik kivételt dobunk.

Valójában már a fájl megnyitása is dobhat kivételt; pl. FileNotFoundError-t akkor, ha nincs is olyan fájl, vagy PermissionError-t akkor, ha ugyan létezik, de nincs rá olvasási jogunk. Ezért a fájl megnyitását ezen try blokkon kívül kell elvégezni, mert ha nem sikerült a megnyitás, akkor a bezárásnak sincs értelme (sőt, akkor az f változó sem jött létre). Vagyis a fenti függvény professzionális megvalósítása ilyen:

def fajlban_szamok_osszege(fajlnev):
    osszeg = 0
    f = open(fajlnev, "rt")
    try:
        for sor in f:
            osszeg += int(sor)
    finally:
        f.close()
    return osszeg

Látszik, hogy most tényleg csak a fájlból olvasós rész van a hibakezelésbe zárva; azok az utasítások, amiket akkor használunk, amikor a fájl nyitva is van.

6. Kivételkezelés: a with blokk

Észrevehetjük, hogy a fájl megnyitása és bezárása egy keretet kell adjon a fájlkezelésnek. Biztos ott kell lennie a blokk elején a megnyitásnak, és biztosan ott kell lennie a végén a bezárásnak is.

Az ilyen helyzetek kezelésére találták ki a with kulcsszót, az ún. context manager-t. Egy with blokk megadásával a fájl bezárását és a hibakezelést automatikussá tehetjük, anélkül hogy külön tryexcept, vagy tryfinally blokkot kellene írnunk.

A with blokk bevezetésével az alábbi formát ölti a függvényünk:

context
manager
def fajlban_szamok_osszege(fajlnev):
    osszeg = 0
    with open(fajlnev, "rt") as f:
        for sor in f:
            osszeg += int(sor)
    return osszeg

Miért jó ez?

  • Nem kell f.close() – automatikus.
  • Nem kell tryfinally sem.

Hogyan kell ezt használni? Nagyon egyszerű. A fájl megnyitását, az open(fajlnev, "rt") kifejezést egy with ... as f blokkba kell tenni. Ennek hatására a megnyitott fájlt reprezentáló objektum az f változóba kerül. A with blokk belsejében ezt ugyanúgy használhatjuk, ahogy eddig. A fájl bezárását végző f.close() sort teljesen el is kell hagynunk; ahogy kiléptünk a with blokkból, az automatikusan megtörténik.

Milyen vezérlési utak vannak, milyen hibalehetőségek esetén mi történik? Először is, ha minden rendben van, akkor lefut a with blokk, bezáródik a fájl és visszatérünk a függvényből. Másodszor, ha valamelyik int() konverzió hibát dobott, akkor azt a kivételt nem kapjuk el; viszont a with blokk miatt a függvény elhagyása előtt az f.close() még meghívódik automatikusan. Harmadszor pedig, ha esetleg már a fájl megnyitása sem sikerül (vagyis ha az open() dobja a kivételt), akkor már a with blokkba se megyünk be – viszont akkor bezárni sem kell a fájlt, mert meg sem nyílt.

A működés hátterében két függvényhívás áll. A fájl osztálynak van ugyanis egy __enter__ és egy __exit__ nevű függvénye, amelyek meghívódnak a with blokkba belépéskor és a blokkból kilépéskor. Más osztályok is támogatnak ilyen működést. Tipikusan azok, amelyekhez valamilyen erőforrás tartozik, pl. megnyitott hálózati kapcsolat, képernyőn megjelenített ablak és így tovább. Ez a témakör ebben a félévben csak említésként szerepel; __enter__ és __exit__ függvényeket nem kell tudni írni, csak a with blokkot használni.

7. Hogy néz ki egy szövegfájl?

A szövegfájlok azokat a karaktereket tartalmazzák, amelyeket a print() a képernyőre is írna. Azt gondolnánk, hogy a szövegfájlok könnyedén átvihetők egyik számítógépről / operációs rendszerről a másikra, azonban lehetnek apró különbségek. Ha a fentebbi, helló világos programot lefuttatjuk Windowson és valamilyen Unix operációs rendszeren, akkor két különböző fájlt kapunk. Egyes rendszerek máshogy jelzik a szövegfájlokban a sorok végét (\n). Windowson két bájt, CR LF (0x0D 0x0A), Unixokon csak LF (0x0A).

Unixokon (pl. Linux):

48 65 6C 6C   6F 2C 20 76   69 6C 61 67   21 0A 30 0A   Hello, vilag!.0.
31 0A 32 0A   33 0A 34 0A   35 0A 36 0A   37 0A 38 0A   1.2.3.4.5.6.7.8.
39 0A 31 30   0A 31 31 0A   31 32 0A 31   33 0A 31 34   9.10.11.12.13.14
0A 31 35 0A   31 36 0A 31   37 0A 31 38   0A 31 39 0A   .15.16.17.18.19.

Windowson:

48 65 6C 6C   6F 2C 20 76   69 6C 61 67   21 0D 0A 30   Hello, vilag!..0
0D 0A 31 0D   0A 32 0D 0A   33 0D 0A 34   0D 0A 35 0D   ..1..2..3..4..5.
0A 36 0D 0A   37 0D 0A 38   0D 0A 39 0D   0A 31 30 0D   .6..7..8..9..10.
0A 31 31 0D   0A 31 32 0D   0A 31 33 0D   0A 31 34 0D   .11..12..13..14.
0A 31 35 0D   0A 31 36 0D   0A 31 37 0D   0A 31 38 0D   .15..16..17..18.
0A 31 39 0D   0A                                        .19..

Szöveges módban nyitott fájlnál ezt elfedi nekünk a nyelv. Kezelés: f = open(név, "...t"), f.write("sztring\n"), f.readline().

A "t"-vel, szöveges módban megnyitott fájl olvasásakor és írásakor az újsor konverziót a fájlkezelő függvények automatikusan elvégzik. Vagyis Unixon pl. a \n sortörést változatlanul kiírják a fájlba, Windowson viszont az f.write("\n") hatására nem egy, hanem két bájt kerül a fájlba. Az automatizmus miatt ezzel nekünk nem kell foglalkozni, csak annyiban, hogy "t" módban kell megnyitni a fájlt, ha szöveges formátumot szeretnénk.

A szövegfájlokat általában lineárisan kezeljük, nem ugrunk benne ide-oda fájlműveletek közben. Bár elvileg lehetséges, de nehéz megvalósítani az adott sorra ugrást: ki kellene számolnunk a bájtban megadott pozíciót. Azt meg nem ismerjük, amíg nem olvastuk be a sorokat, mert minden sor különböző hosszúságú lehet.

A fenti apróságtól eltekintve a szövegfájlok sokkal inkább hordozhatóak, hiszen a bennük tárolt adatok nem függenek a számábrázolás módjától, amit az adott géptípus hardvere határoz meg. Ez az oka annak, hogy az utóbbi években egyre inkább terjednek a szöveg alapú formátumok:

  • szöveges dokumentumok: HTML, RTF.
  • adatok, adatbázisok: JSON, XML.

8. Hogy néz ki egy bináris fájl?

A bináris fájlok azok, amelyek nem szöveget tartalmaznak. Ezekbe bájtokat írunk; legtöbbször bájtról bájtra kiírunk valamilyen memóriaterületet. Ilyeneket általában a Python bytes osztályának segítségével kezelünk.

Bináris fájl írása (egyetlen egy int):

i = 123456
f = open("file.dat", "wb")
f.write(i.to_bytes(4, byteorder='little'))
f.close()

A keletkező fájl tartalma (bájtok):

40 E2 01 00

A bináris fájlba jelen példában egy 4 bájtos integert írtunk bele, little endian bájtsorrendben – azaz előbb a kicsi helyiértékek vannak, utána a nagyok (lásd a Hardver alapok tárgyon tanultakat). Így jelentek meg a fájlban a 40 E2 01 00 bájtok, mert 123456 tízes számrendszerben felírva 0x0001E240.

A fájl olvasása:

f = open("file.dat", "rb")
bytearr = f.read(4)
i = int.from_bytes(bytearr, byteorder='little')
f.close()
print(i)    # 123456

A fentihez hasonló módon bináris módban nyitjuk meg a fájlt: b = binary. Innen tudja a Python, hogy most karakterkódolásokkal, sorvégek jelölésével nem kell foglalkoznia: nyers adatot fogunk olvasni, bájtról bájtra azt szeretnénk látni, amit eltároltunk.

A beolvasás a .read() függvénnyel történik. Ennek most pontosan megmondjuk, hogy hány bájtra vagyunk kíváncsiak, mivel itt nincsenek határolók, mint egy szövegfájlban. A beolvasás által egy bytes típusú objektumot kapunk. Jelen példában ezt alakítjuk vissza int típusú adattá.

Bináris fájlokat használunk akkor, ha 1) tömör, kicsi fájlt szeretnénk létrehozni (pl. ha számokat kell tárolni, akkor ez kisebb fájlt eredményez), illetve ha 2) nagy mennyiségű adatot kell gyorsan kezelni (mivel szövegfájloknál a szöveggé alakítás jelentős időbe telik).

9. Fájlok – szabványos adatfolyamok

import sys

sys.stdout.write("Írj be egy sort: ")   # print()
sys.stdout.flush()

line = sys.stdin.readline()             # input()
text = line.rstrip("\n")

if text != "":
    sys.stdout.write(f"|{text}|\n")
else:
    sys.stderr.write("Hiba: nem írtál be semmit!\n")
  • sys.stdin – szabványos bemenet (standard input)
  • sys.stdout – szabványos kimenet (standard output)
  • sys.stderr – szabványos hibakimenet (standard error)

A szabványos kimeneti és bemeneti csatornákat (adatfolyamokat, stream) is fájlként látjuk. A normál print(…) függvény nagyjából egyenértékű egy sys.stdout.write(…) hívással, az input(…) pedig egy sys.stdin.readline() hívással. Az stdin neve szabványos bemenet (standard input), az stdout-é szabványos kimenet (standard output). Ezek általában a billentyűzet és a konzol ablak; az operációs rendszer építi rá azokra ezt az absztrakciót, hogy fájlként is tudjuk kezelni őket.

Létezik egy második kimenet is, a sys.stderr, ez az ún. szabványos hibakimenet. A szabványos hibakimenet (stderr) a normál kimenethez hasonló a programunk számára. A kettő közötti különbség az, hogy a normál kimenetre a program által előállított eredményt, kimeneti adatot szokás írni, a hibakimenetre pedig a hibaüzeneteket. Így elkerülhető, hogy a kettő egymással keveredjen, ha a kimeneti adatokat egy fájlba szeretnénk irányítani, vagy egy másik programnak átadni.

10. Programok futtatása parancssorból

A Python programjaink parancssorból is indíthatóak. Ehhez a python3 nevű programnak paraméterként át kell adnunk a forrásfájl (valami.py) nevét:

Futtatás parancssorból:

C:\> type program.py
# Üdvözlet
print("Helló, világ!")

C:\> python3 program.py
Helló, világ!

Meg lehet oldani azt is, hogy ne kelljen kiírni a python3 parancsot. Itt több operációs rendszertől függő dolog van, amit most csak megemlítünk.

  • Windows esetén, ha helyes a Python csomagunk telepítése, a program önmagában is indítható: program.py nevet kell beírni a parancssorban.
  • Linux esetén, a programot futtathatóvá kell tenni: chmod 755 program.py, és az elejére kell egy komment, ún. „shebang”, amelyből a rendszer tudja, hogy melyik Python értelmezőt kell hozzá elindítani. Az első sor így kell kinézzen: #!/usr/bin/env python3.

A program bemenete és kimenete fájlból/fájlba átirányítható. Például ha a kimenetét a képernyő helyett egy fájlba szeretnénk átirányítani:

Átirányítás fájlba:

C:\> python3 program.py >udvozlet.txt

C:\> type udvozlet.txt
Helló, világ!

A bemenet átirányítása ugyanígy történik, csak másik irányú kacsacsőrrel, pl. <bemenet.txt. Ilyenkor a program nem tudja, hogy át van irányítva; mindkét esetben a program a szabványos bemenetére és a szabványos kimenetére ír, és az operációs rendszer a háttérben megoldja, hogy a képernyő vagy billentyűzet helyett egy fájlt lásson. Erről operációs rendszerek tárgyon lesz szó részletesebben.

11. A parancssori argumentumok

Az elindított programoknak is lehet paramétereket adni:

C:\> notepad.exe szoveg.txt_

A Python programunk a sys.argv listában kapja meg ezeket. A paraméterek eléréséhez importálnunk kell a sys modult. Ezután érjük el az argv nevű listát, amelyik az egyes paramétereket tartalmazza. Ennek tradicionálisan lett ez a neve; arg, mert az argumentumok vannak benne, és v, mert vektor (emlékezzünk vissza, így is szokás a tömböket nevezni).

Például:

C:\> teszt.py  elso  "masodik szo"
       ↑        ↑        ↑
    argv[0]   argv[1]  argv[2]
import sys

print(sys.argv)  # ["teszt.py", "elso", "masodik szo"]

A sys.argv[] lista különlegessége, hogy a nulladik eleme, azaz argv[0] a program nevét tartalmazza. A tényleges paraméterek csak ezután jönnek. Ez okozza azt, hogy az első tényleges paraméter (a fenti példában az "elso") nem a lista nulladik, hanem első indexű helyén van. A paraméterek számába a program saját neve is beleszámít, ezért a fenti példában len(argv) értéke nem kettő, hanem három. Ha a programban a kapott parancssori paraméterek számát ellenőrizni szeretnénk, ezt figyelembe kell venni.

12. A program visszatérési értéke

A programot befejezve vissza tudunk adni egy egész számot. Ezt átveszi az operációs rendszer, és átadja a programnak, amely elindította a mienket.

Visszatérés hibakóddal:

import sys

if len(sys.argv) - 1 != 2:
    print("Két paraméter kell!")
    sys.exit(1)     # hibakód

#
# a program tényleges feladatai ...
#

sys.exit(0)         # vagy egyszerűen semmi

Itt lényegében egy hibakódot tudunk visszaadni. A kódokat mi magunk adjuk meg, kicsi egész számoknak kell lennie (legfeljebb 7 bites). A 0 jelenti azt, hogy nincs hiba, bármi más pedig szokásjog alapján hibát jelez. A 0 értékű hibakódot, vagyis a program sikeres futását egyébként nem kötelező sys.exit(0)-val jelezni. Ha azt lehagyjuk (ahogy eddig is tettük, minden egyes programunkkal az eddigi heteken), akkor az sikeres futást jelez.

Vegyük észre, hogy a sys.exit() függvény hívásával azonnal befejeződik a programunk! Vagyis bármi is van alatta, bármilyen függvényhívás mélyén voltunk, a programunk nem folytatódik tovább. Erre utal a függvény neve is egyébként, ezért lett ez exit().


Mire jó ez?

rm szoveg.txt || echo "Nem sikerült törölni a fájlt!"

A visszatérési értékek lehetővé teszik, hogy a parancssorban összekössük az egymás után futó programokat aszerint, hogy sikeres volt-e a végrehajtásuk. A Unix parancssorában például az || operátor csak akkor hajtja végre a második parancsot, ha az első sikertelen volt. Jelen példában ez azt jelenti, hogy ha az rm program 0-tól különböző kóddal tért vissza a maga main() függvényéből, akkor hajtódik csak végre az echo program, amelyik a hibaüzenetet kiírja. Amúgy ez elmarad, mert ha sikeres volt a törlés, nincs szükség hibaüzenetre.

Példa az argumentumokra és a main() visszatérési értékére:

import sys

if len(sys.argv)-1 != 2:
    print(f"{sys.argv[0]}: két számot adj meg!")
    sys.exit(1)

try:
    osszeg = int(sys.argv[1]) + int(sys.argv[2])
except ValueError:
    print("A paraméterek nem egész számok!")
    sys.exit(2)

print(f"Az összegük: {osszeg}")

Fontos emlékezni arra, hogy a sys.argv sztringeket tartalmaz. Ha számokat veszünk át paraméterként, azt is sztringként kapjuk! Ilyenkor int() konverzióra van szükség.

Nagyobb program tervezése

14. Felsorolt típus: meghatározott értékek

Felsorolt típus

Olyan típus, amelynek értékkészlete egy nevekkel megadott, véges értékhalmaz.


Kártya színe

♠ ♣ ♥ ♦

Tic-tac-toe

Tic-tac-toe játék

Közlekedési lámpa

Közlekedési lámpa

15. Definíciója és használata

lampa = 1

if lampa == 3:
    print("Mehet az autó!")


# Mit jelent a 3?
# Mit jelent az 1?
PIROS = 1
PIROSSARGA = 2
ZOLD = 3
SARGA = 4

lampa = PIROS
if lampa == ZOLD:
    print("Mehet az autó!")

Hasonlítsuk össze a két fenti kódot! Ugyanazt csinálja mind a kettő, mégis a jobb oldalon látható egy sokkal jobb megoldás.

Mi is történt a bal oldalon? Négy állapota van egy közlekedési lámpának: piros, piros és sárga együtt, zöld, aztán sárga. Ezeket az állapotokat számokkal helyettesítettük: a négy állapotnak négy int típusú érték felel meg, 1, 2, 3 és 4. Na de melyik mit jelent? Ki fog erre emlékezni? A 4 volt a zöld? Vagy a 2? Miért a 3, miért nem 1?

A jobb oldalon létrehoztunk négy változót. Ezek nem változók, konstansnak tekintjük őket; egyszer kaptak egy értéket, amit aztán később soha nem írunk felül. A 4 változót PIROS, PIROSSARGA, ZOLD, SARGA néven nevezzük el, és ezeknek adjuk értékül azokat a számokat, amikkel a négy különböző állapotot, fázist reprezentáljuk. Ezek után bárhol használhatjuk ezeket a neveket: lampa = PIROS, if lampa == ZOLD, gondolkodás nélkül tudjuk, miről van szó.

A konstansok nevét egyébként illik csupa nagybetűvel írni, de ha már felsorolásról van szó, lássunk egy még jobb megoldást!

Jobb megoldás
class Lampa:
    piros = 1
    pirossarga = 2
    zold = 3
    sarga = 4

lampa = Lampa.piros
if lampa == Lampa.zold:
    print("Mehet az autó!")

Legjobb ötlet, ha a konstansokat csoportosítjuk egy osztály segítségével. Ezt a fenti szintaxissal lehet elvégezni. Így az osztály egy névteret (namespace) ad a konstansaink számára; nem csak simán megjelennek azok a globális névtérben, hanem az osztály kifejezi az összetartozásukat is. Innentől kezdve osztályneve.konstansneve formában érjük el az egyes értékeket. Mivel ezeket az osztály neve már csoportosítja, ilyenkor nem szokás már csupa nagybetűvel írni őket.

Létezik egy enum nevű modul (import enum), amelyik plusz támogatást nyújt a felsorolt típusokhoz. Ezt a félévben nem fogjuk használni, csak megemlítjük, hogy van ilyen. A neve a felsorolások angol nevéből jön (enum – enumeration).

16. Tic-tac-toe: Feladatspecifikáció

Tic-tac-toe játék

Tic-tac-toe játék
  • Tároljuk a pályát
  • Tároljuk a játékosok neveit
  • Új játékot kezdünk
  • Kirajzoljuk a pályát
  • Lépünk egy játékossal
  • Ellenőrizzük, nyerésre áll-e valamelyik játékos
Letölthető:
tictactoe.zip.

Teendők a program megírásához

  • Definiáljunk típusokat
  • Írjuk meg a játékot vezérlő függvényeket
  • Definiáljunk fájlformátumot
  • Mentsük, töltsük vissza az állást

17. Tic-tac-toe adatok: a játék elemei

A pálya egy cellája

A pálya egy cellája csak ezt a három értéket veheti fel. Ezért ezt egy felsorolt típussal ábrázoljuk (Babu), amelyekből a pályához majd 3×3-as, kétdimenziós tömböt építünk (egymásba ágyazott listákkal).

class Babu:
    ures = 0
    kor = 1
    iksz = 2

Melyik játékos következik?

Leírjuk a fenti típussal.

A két játékos felváltva léphet. A programnak erre is emlékeznie kell majd. Ugyan a két állapot megtévesztő, a logikai típus, bool eszünkbe juthat miatta, de ez rossz döntés lenne. A „melyik játékos következik?” nem eldöntendő kérdés, értelmetlen „igen” vagy „nem” választ adni rá. A „körrel játszó” és az „x-szel játszó” a helyes válaszok. Bár igaz az, hogy mindkét játékos a saját bábujával lép, de ez a két dolog mégsem ugyanaz: nincs olyan, hogy az üres cellával lépő játékos következik. Ezért ezt reprezentálhatnánk egy külön típussal is, de egyszerűbb, ha a bábu típust használjuk újra.

A felsorolt típus felesleges tánclépésnek tűnik, mert tudjuk, hogy a bábukat, játékosokat végeredményben egész számok fogják reprezentálni. De mégis érdemes megtenni, mert később olvashatóbb lesz tőle a programunk, és a programkód jelentését függetleníteni tudjuk attól, hogy valójában melyik szám reprezentálja melyik bábut. Gondoljunk egy szövegre és a karakterkódokra: az "alma" sztring a memóriában a 97, 108, 109, 97 számokkal reprezentálódik. De ez mindegy, mert amíg a karaktereket látjuk, lényegtelen, hogy a hozzájuk tartozó kód micsoda.

18. Tic-tac-toe adatok: a játékállás

A játék adatszerkezet

A játék állásához, állapotához rögzíteni kell a pálya állapotát, a következő körben lépő játékost, és a játékosok neveit.

Az összetartozó adatokhoz létrehozunk egy osztályt:

class Jatek:
    def __init__(self, kor_nev, iksz_nev):
        self.palya = []
        for _ in range(3):
            self.palya.append([Babu.ures] * 3)  # 3×3, üres
        self.kovetkezo = Babu.kor
        self.kor_nev = kor_nev
        self.iksz_nev = iksz_nev

A játékállást kezelő függvények

Miért jó ez? Azért, mert az egyes függvényeink rengeteg paraméterrel rendelkeznének, ha ez nem lenne. Így viszont egyetlen paramétere lesz mindegyiknek: a játék objektum, amelyben minden lényeges információ megtalálható.

j1 = Jatek("Aladár", "Kriszta")
jatek_lep(j1, Pozicio(1, 1))
jatek_lep(j1, Pozicio(2, 2))
jatek_kirajzol(j1)

Figyelem: emlékezzünk vissza, hogy mi volt a helyzet a paraméterátadásokkal, és a paraméterként átadott objektumok módosításával. Egy függvény nem tudja módosítani a neki átadott változót, mert csak a változóban tárolt referenciát tudja módosítani. Jelen példában a Jatek osztály konstruktora létrehoz egy új objektumot, és ahhoz a j1 változóban tárolt referenciát kötjük az értékadással. Hiába próbálnánk jatek_uj(j1)-et írni, az nem működne. A lépést végző függvény viszont egy meglévő objektum referenciáját kapja; nem a j1 változót fogja módosítani, hanem a j1 referencia által mutatott objektumot.

Vegyük észre azt is, hogy a jatek_lep() függvény nem kapja paraméterként, hogy melyik játékos következik, csak a pozíciót, hogy hova lépett. Hogy ki jön, az eltárolódik a játék objektumban: j1.kovetkezo. Nyilvánvaló, hogy a jatek_lep() függvény ezt is frissíti majd, így alakul ki az, hogy az (1;1) pozícióba kör kerül, a (2;2) pozícióba pedig iksz.

Új játék: az inicializálása

class Jatek:
    def __init__(self, kor_nev, iksz_nev):
        self.palya = []
        for _ in range(3):
            self.palya.append([Babu.ures] * 3)
        # ...

Az új játék létrehozásakor beállítjuk az adattagokat a szokásos módon. Utána pedig létrehozunk egy kétdimenziós listát: 3×3-as listát. Vagyis egy olyan 3 elemű listát, ami maga is 3 elemű listákat tartalmaz. Emlékezzünk vissza, erre nem alkalmas a [[0]*3]*3 alakú kifejezés, mert az ugyanazon egyetlen listához kötne 3 referenciát. Helyette mindhárom sort külön létre kell hoznunk.

A pálya kirajzolása

def jatek_kirajzol(j):
    babu_kep = {
        Babu.ures: '.',
        Babu.kor: 'o',
        Babu.iksz: 'x',
    }
    
    print("+---+")
    for y in range(3):
        print("|", end="")
        for x in range(3):
            print(babu_kep[j.palya[y][x]], end="")
        print("|")
    print("+---+")

Kirajzolás közben az egyes lehetséges cella értékekhez hozzá kell rendelni a megfelelő karaktert. Itt ehhez egy dict típusú tárolót, egy szótárat (dictionary) használunk. Ez úgy használható, mint egy lista, csak indexelni bármivel tudjuk, nem csak számokkal; jelen esetben a cellaértékekkel. Később lesz a dict-ről részletesebben szó. Most elég annyit tudni róla, hogy kapcsos zárójelekkel lehet létrehozni: { név: érték }, felsorolva az egyes neveket (name), amikkel indexelni szeretnénk azt, és elérni az értékeket (value). Ezek után pedig az indexelő operátorral érjük el azokat, pl. babu_kep[Babu.kor] == 'o'.

Ez lenne az a függvény, amit lecserélhetnénk abban az esetben, ha más megjelenítést használnánk. Például Pygame alapú grafikát, vagy esetleg színes konzolos megjelenítést.

A következő játékos lépése

def jatek_lep(j, p):
    j.palya[p.y][p.x] = j.kovetkezo
    j.kovetkezo = ki_a_kovetkezo(j.kovetkezo)

Ha a körrel játszó játékos jön, kört teszünk az adott pozícióra. Ha az iksszel játszó, akkor ikszet. Itt kihasználjuk, hogy ugyanaz a felsorolt típus reprezentálja a játékost, és az általa használt bábut. Hogy ki a következő játékos, azt pedig a ki_a_kovetkezo() függvény. Ezek olyan egyszerűek, hogy már nem is részletezzük.

19. A játékállás fájlba mentése

A Jatek struktúrát kigondolva a dolgunk egyszerű: az abban lévő adatokat mentjük egy fájlba. Például így:

jatekallas.txt
Tictactoe v1
Aladár
Kriszta
1
0 0 0
0 1 0
1 0 2

Mit tartalmaz ez a fájl, miért így találtuk ki?

  • Egyrészt egy bevezető sort: Tictactoe v1.
  • Utána a két játékos neve jön.
  • Ez folytatódik annak rögzítésével, hogy hol tart a játszma, ki lép következőnek.
  • Végül pedig a játékállás.

Jegyezzünk meg pár érdekességet!

Miért jó a bevezető sor? Egyrészt ebből látszik, hogy a fájl milyen adatokat tartalmaz, tehát ha esetleg megnyitjuk jegyzettömbben, vagy más fájlkezelő programban, akkor lehet sejtésünk, mire való. Másrészt ez kaphat egy verziószámot is. Ha később továbbfejlesztjük a programot, és változik a formátum, akkor ebből kiderül, mikori program készítette a fájlt. Így némi többletmunkával megoldhatjuk, hogy az új program felismerje a régebbi formátumokat, kompatibilis legyen azokkal is.

A pálya állapotának rögzítésekor a felsorolt típus értékét írjuk ki, az egész számot, amivel reprezentáljuk azt. Csak arra kell majd vigyáznunk, hogy ezeket az értékeket ne változtassuk meg. Persze ezt megoldhatnánk úgy is, hogy a fájlban is pont, o és x karaktereket használunk a cellák jelölésére, ahogy a fenti egyszerű megjelenítésben tettük.

Végül pedig, a következőnek lépő játékos rögzítése tulajdonképp felesleges (ami a nevek után található egész szám). Ha kevesebb x van a pályán, akkor az x-szel játszó játékos jön, amúgy pedig a körrel játszó. Ezt tulajdonképp ki lehet találni a játékállás vizsgálatával. De inkább eltároljuk, mert egyszerűbb lesz tőle a programunk, az az egy karakter pedig már igazán nem számít.


A játék mentése (lényegi rész, a többi letölthető):

f.write("Tictactoe v1\n")
f.write(j.kor_nev + "\n")
f.write(j.iksz_nev + "\n")
f.write(str(j.kovetkezo) + "\n")    # "1\n"
for y in range(3):
    for x in range(3):
        f.write(str(j.palya[y][x]) + " ")   # "2 "
    f.write("\n")

Ahogy a képernyőre írásnál is tennénk, itt is ügyelünk arra, hogy a számok kiírása után tegyünk elválasztó karaktert: szóközt vagy entert. Ez teszi lehetővé, hogy a számot vissza tudjuk majd olvasni a szövegfájlból. Mert pl. "1 0 2" ez három kiírt szám, ugyanakkor viszont "102" ez csak egy, amit aztán nehezebb visszaolvasni.

Az állás betöltése (szintén csak a lényegi rész):

verzio = f.readline()   # itt ellenőrizhetnénk
j.kor_nev = f.readline().rstrip("\n")
j.iksz_nev = f.readline().rstrip("\n")
j.kovetkezo = int(f.readline())
for y in range(3):
    sor = f.readline().split(" ")
    for x in range(3):
        j.palya[y][x] = int(sor[x])

Itt talán csak a pályát beolvasó rész az, amiről érdemes külön beszélni. Ebben ugyanúgy soronként haladunk, mint ahogy a pálya kirajzolásával is. Azért jó, hogy a pálya minden sorát a fájlban is külön sorba tettük, mert így a visszaolvasó ciklusunk is soronként, egyszerű f.readline() függvényhívásokkal tud haladni. A sorokban lévő számokat szóközzel választjuk el, tehát egy .split(" ") darabokra tördeli azt; végül mindegyiket int-té kell alakítani.

A beolvasás közben rengeteg hiba történhet. Mi történik akkor, ha hibás a fájl, és túl hosszú nevet olvasnánk be? Túlindexelődik a lista. Mi történik akkor, ha a felsorolt típusok értékei helyett egy olyan szám jelenik meg, amilyen értéket az adott típus nem vehet föl? Például a Babu típus üres, kör, iksz értékeit a 0, 1 és 2 számok ábrázolják a memóriában. Mi történik, ha az egyik cella helyén 97214 van a fájlban? Erre is helytelenül fog működni a továbbiakban a programunk. Ezért az összes beolvasott adatot ellenőrizni kellene. Ezek most az egyszerűsített példánkban elmaradtak.

Többmodulos programok

21. Nagy projektek: egy fájl? több modul!

„MLOC project”
million lines of code

Egy közepes projekt néhány tízezer sorból áll, egy nagy projekt több százezer, millió sorból.



Ha a projekt egyetlen, nagy forrásállományban lenne megírva:

  • akkor áttekinthetetlen lenne,
  • a szerkesztő programok nehezen/lassan kezelnék,
  • nehézkes lenne többen egyszerre dolgozni rajta,
  • egy-egy újrafordítás akár órákat vehetne igénybe.

Ha szerkezeti egységekre bontjuk a programot:

  • ezek önállóan kezelhetőek, a program ezek összeépítéséből keletkezik,
  • egy csapat különböző tagjai egymástól függetlenül dolgozhatnak,
  • az egyedi fordítások gyorsan lezajlanak.

Ez az egyedül fejlesztett programoknál is nagyon hasznos: az egyes modulok újra felhasználhatóak más projektekben. Érdemes a funkcionális egységeket általános módon megírni, hogy minél könnyebben fel lehessen használni őket más feladatok megoldásában.

22. Főprogram és modulok

Melyik függvény
hova való?
def main():
    j1 = Jatek("Aladár", "Kriszta")

    ………

    while ………:
        jatek_kirajzol(j1)
        p = pozicio_beolvas()
        if jatek_lep(j1, p):
            nyert = True
            break

    ………

    jatek_ment(j1, "tictactoe.txt")
    jatek_betolt(j1, "tictactoe.txt")

A programot logikusan több modulra lehet bontani. Az első a fő programmodul. Ez tartalmazza a main() függvényt, amely a programot vezérli. A másik modul a játékhoz tartozó programrészekből áll össze: ez definiálja a pálya típust, és pl. a játékszabályt, a játék működését leíró függvényeket. A harmadik modul az, amelyik a megjelenítésért, a felhasználóval való kommunikációért felel: a pálya kirajzolásáért és a pozíció beolvasásáért.

Az egyes modulok nagyjából önállóak. A megjelenítésért felelős modul lecserélhető lenne egy olyan változatra, amely nem konzollal dolgozik (print(), input()), hanem színes, grafikus megjelenítést használ, és a pozíciót egérkattintásból nyeri. A megjelenítésért és a játékszabályokért felelős modul nem kommunikál egymással; a fő programmodul dolga az, hogy a két almodul által adott programrészekből, azok függvényeinek hívásából egy működő, egész programot állítson össze.

23. A modulok forrásfájljai: *.py

A program egyes részfeladatait, függvényeit az őket tartalmazó modul szerint különválasztjuk az egyes forrásfájlokba. Így jön létre a jatekallas.py, a megjelenites.py, amelyek lentebb láthatóak. Ezen felül lesz egy main.py fájlunk is, a főprogrammal.

jatekallas.py:
a játék menete
class Babu: ………
class Jatek: ………
class Pozicio: ………

def harmas(): ………
def nyert_e(): ………
def mivel_jatszik(): ………
def kovetkezo(): ………
def lep(): ………
def ment(): ………
def betolt(): ………

megjelenites.py:
felhasználói felület
def jatek_kirajzol(): ………
def pozicio_beolvas(): ………

24. A modulok használata

main.py
import jatekallas
import megjelenites


def main():
    j1 = jatekallas.Jatek("Aladár", "Kriszta")
    
    # 9 lépéssel megtelik a pálya
    for i in range(9):
        megjelenites.jatek_kirajzol(j1)
        p = megjelenites.pozicio_beolvas()
        if jatekallas.jatek_lep(j1, p):
            nyert = True
            break
    else:
        nyert = False
    
    ………

Ezeket a modulokat a félév elején megismert import kulcsszóval tudjuk majd felhasználni a másik modulban. Mivel innentől kezdve az egyes függvényeket a modul nevével együtt kell hivatkoznunk, pl. jatekallas.lep(), ezért a nevek prefixelése feleslegessé válik. Például a fájlba mentő függvény jatek_ment() helyett egyszerűen lehet ment(), és azt a főprogramból jatekallas.ment() néven tudjuk majd elérni. Látszik a fenti példán, hogy mindez igaz a függvényekre is, pl. megjelenites.pozicio_beolvas(), és az osztályokra is: jatekallas.Jatek.

A modul neve egyébként meg kell egyezzen a fájl nevével. Ha import jatekallas-t írunk a kódban, akkor a Python a jatekallas.py fájlt fogja keresni.

Az egyes modulok egymást is elérik; például a megjelenítésért felelős modul is import jatekallas sorral indít, mert neki is tudnia kell, hogy mi az a Babu, és mi az a Jatek.

Programok életciklusa

26. Egy program életciklusa

  1. Specifikáció
    • Elvárások, képességek (feature)
    • Mit fog tudni a program
  2. Fejlesztés
    • fejlesztői eszközök, technológiák megválasztása,
    • a rendszer magas szintű megtervezése:
      • modulok és kapcsolatuk,
      • be-, és kimeneti formátumok,
    • algoritmusok, adatszerkezetek megtervezése,
    • implementáció elkészítése (kód dokumentálása párhuzamosan),
    • tesztelés, hibajavítás,
    • dokumentáció elkészítése.
  3. Támogatás, továbbfejlesztés

27. Dokumentáció I.

A dokumentáció szintjei:

  1. Fejlesztői (programozói) dokumentáció
    • adatszerkezetek dokumentációja,
    • algoritmusok dokumentációja,
    • a kód szerkezeti áttekintése,
    • a kód részletes dokumentációja.
  2. A forráskód
    • kommentezés, ha szükséges
  3. Felhasználói dokumentáció
    • a program használatának a leírása
  4. Tesztelési dokumentáció
    • a tesztelés körülményeit, eredményeit írja le

28. Dokumentáció II. – Docstring

A tic-tac-toe
forráskódja
végig ilyen!
def prim(szam):
    """Megmondja, hogy egy szám prímszám-e."""
    for oszto in range(2, szam):
        if szam % oszto == 0:
            return False
    return True

A Docstring-eket több eszköz is kezeli:

  • A beépített help() függvény.
  • PyDoc és hasonló programok

29. Dokumentáció III. – Kommentek

Feladatunk megkeresni egy szöveg első szavának utolsó karakterét.

Milyen a jó komment?

i = 0
while szoveg[i] != " ":
    i += 1
# lecsökkentjük i-t
i -= 1

Biztosan nem ilyen. Ez semmivel nem mond többet, mint az általa magyarázott sor. Mindenki tudja, hogy a -= 1 utasítás lecsökkenti a változóban tárolt értéket. Ha kitöröljük ezt a kommentet, nem lesz kevesebb információ a kódban, ezért nincs semmilyen haszna. Sőt valójában negatív a haszna, konkrétan árt: hosszabb lett tőle a kód.

i = 0
while szoveg[i] != " ":
    i += 1
# visszalépünk az utolsó karakterre
i -= 1

Ez már sokkal jobb. Ez kifejezi a szándékát annak a sornak: nem csak azt tudjuk, hogy mit csinál az a sor, hanem már azt is, hogy miért! De tudunk ennél jobbat is...

i = 0
while szoveg[i] != " ":
    i += 1
utolso = i-1

Nem kell a komment! A kódban a változó neve elmondja, hogy mi volt a célja az előző ciklusnak, és mi a célja a -1-nek: megtalálni az utolsó karaktert a szóköz előtt, amelynek az indexe ezentúl az utolso nevű változó tárolja. Ez a kód többi részére is jó hatással lesz: nem a semmitmondó i néven kell elérjük ezt az információt.

utolso = szoveg.find(" ") - 1

Ez pedig a legjobb: a ciklust is megmagyarázza, hogy a szóköz megkeresése volt annak a célja. Azzal, hogy a részműveletet külön függvénybe tettük (történetesen most volt olyan a sztring osztályban is), elértük, hogy el tudtuk nevezni azt is.

30. Hibalehetőségek és tesztelés

Szintaktikai hiba
Nyelvtanilag hibás kód – le sem fut.
Pl. for x range(10)
Szemantikai hiba – bug
Nyelvileg helyes, de logikailag hibás program.
  • Hibás algoritmus, hibás kódolás…
  • Lista negatív indexelés…
Futási idejű hibák: hibás bemenet
A program jó, de a külső körülmények nem.
  • Felhasználó rossz adatot gépel
  • Hibás a beolvasott fájl

„Kétféleképpen lehet hibátlan programot írni. A harmadik módszer az, ami működik.”
– Alan Perlis

A tesztelés sokkal fontosabb és nehezebb lépés, mint gondolnánk!

  • Egy egyszerű programot könnyű letesztelni.
  • Egy komplex rendszer összes funkcióját leellenőrizni minden lehetséges bemenet és belső állapot esetén szinte lehetetlen.

A kis részektől a nagy felé haladva érdemes tesztelni.

  • Minden függvényt külön is tesztelni kell.
  • Minden lehetséges ágat (elágazások, ciklusok) ki kell próbálni.

31. Tesztelés I. – Hogyan ellenőrizzük?

„Trace”-elés

if DEBUG:
    print(x, file=sys.stderr)

A tesztelt program extra kimenete: a változó értéke mindig megjelenik a képernyőn, ha elér erre a pontra.


Nyomkövetővel (debugger)

laboron
már volt
  • Változó értékének megfigyelése (watch)
  • Töréspont (breakpoint)
  • Lépésenkénti végrehajtás

32. Tesztelés II. – assert, automatikus tesztek

assert: programozói hibák megtalálására való.

def lnko(a, b):
    assert a > 0 and b > 0
    ...

Az assert nyelvi elem utasításként használandó. Ez megállítja a program futását, ha a megadott feltételek nem teljesülnek. Például akkor, ha a legnagyobb közös osztót meghatározó függvényünk nem pozitív számokat kap.

Az ilyen jellegű hibák kapcsán eszünkbe juthatnak a kivételek is. Valóban, ez a függvény kivételt is dobhatna akkor, ha nullának vagy negatív számoknak a legnagyobb közös osztóit keressük.

Kivételeket akkor szoktunk használni, amikor a programot a futás közben előálló hibákra szeretnénk felkészíteni – olyan körülményekre, amik előfordulhatnak üzemeltetés közben. Az assert annyiban különbözik ettől, hogy azzal programozói hibákat próbálunk megfogni. Ha kivételt dobunk hiba esetén, az az jelenti, hogy az adott eset előfordulhat, a hibát kezelni kell. Ha assert-et használunk, azzal pedig azt, hogy ha az adott eset előfordult, akkor a programkódot javítani kell, mert hibás.


Automatikus tesztek:

tabla = halmaz_letrehoz()

halmaz_betesz(tabla, "alma")
halmaz_betesz(tabla, "körte")
assert(halmaz_benne_van(tabla, "alma"))
assert(halmaz_benne_van(tabla, "körte"))

halmaz_kivesz(tabla, "alma")
assert(not halmaz_benne_van(tabla, "alma"))
assert(halmaz_benne_van(tabla, "körte"))

Ha jól körülhatárolt a feladat, akkor valószínűleg könnyű automatikus teszteket is írni hozzá. Például veszünk egy halmazt. Betesszük az alma és a körte sztringeket. Ezek után a benne van-e? kérdésre igazzal kell válaszolnia mindkettőre. Aztán kivesszük az almát, így arra a szóra már hamis kell legyen a válasz. Viszont nem tűnhetett el a körte, mert azt nem szedtük ki stb.

Bár úgy tűnik, a tesztek írásával csak megy az idő, valójában viszont az a tapasztalat, hogy gyorsítják, hatékonyabbá teszik a fejlesztést! Ha vannak tesztek, jobban letisztázódik, mi a konkrét feladat, mik az elvárások. Ha tudjuk a programot automatikusan tesztelni (és ez egy kattintásunkba telik!), akkor bátran módosíthatjuk bármelyik részüket – ha elrontottunk valamit, a tesztek jelezni fogják.

Gyakran a program és a tesztek írását konkrétan megfordítják: előbb írják meg a teszteket, és csak azután a programot. Ez az ún. TDD, test driven development. Ez a szoftverfejlesztési metodika azt javasolja, hogy neki se álljunk a program megírásának előzetesen megírt tesztek nélkül.

Ha vannak tesztjeink, hibák felfedezésekor bővíthetjük is azokat. Ha minden megtalált hibánál így teszünk, azzal biztosítjuk az is, hogy a hiba soha többé ne jöhessen elő újra. Mert azt a hibát onnantól kezdve mindig meg fogják fogni a tesztek. Minderről későbbi programozás tárgyakon lesz szó részletesen.

33. Tesztelés után – Ha maradna benne hiba…


It's not a bug! It's a feature!
This behaviour is by design.