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.
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). Adarabok
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.
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.
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.
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.
É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:
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.
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.
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).
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.
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.
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.
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.
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!
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).
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
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
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.
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.
A Jatek
struktúrát kigondolva a dolgunk egyszerű: az abban lévő adatokat
mentjük egy fájlba. Például így:
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.
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.
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.
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.
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(): ………
felhasználói felület
def jatek_kirajzol(): ………
def pozicio_beolvas(): ………
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
.
- Specifikáció
- Elvárások, képességek (feature)
- Mit fog tudni a program
- 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.
- Támogatás, továbbfejlesztés
A dokumentáció szintjei:
- 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.
- A forráskód
- kommentezés, ha szükséges
- Felhasználói dokumentáció
- a program használatának a leírása
- Tesztelési dokumentáció
- a tesztelés körülményeit, eredményeit írja le
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.
- 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
– 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.
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.
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.