# Program és külvilág. Karbantartható programok

Czirkos Zoltán · 2025.09.06.

Program és külvilág kapcsolata: parancssori argumentumok és fájlkezelés. Többmodulos programok.

# 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ünkön 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 `try`–`except` 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 `try`–`except`, vagy `try`–`finally` 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 `try`–`finally` 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

### 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

- 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.

## 34. Összefoglalás

Az előadáson megismertek alapján:

- Tudni kell szöveges és bináris fájlokat kezelni.
- Ismerni kell a try/except/finally szerkezetet szintaktikáját és használatát.
- Tudni kell használni a context manager (with) szerkezetet.
- Ismerni kell a sys modul által biztosított szabványos adatfolyamokat és a parancssori argumentumok használatát.
- Tisztában kell lenni a többszörös összetétellel.
- Ismerni kell a felsoroló típus megvalósítási lehetőségeit Python nyelven
- Tudni kell többmodulos programokat fejleszteni.
- Tisztában kell lenni az assert használatával.