2016. május 16., hétfő

09 - Kommentek, fájlba írás-olvasás

Ma két, egymástól teljesen független dologgal fogunk foglalkozni, a forráskód kommentelésével és a fájlok írásával valamint olvasásával. Lássunk is neki, mert jól el vagyok maradva a bejegyzéseimmel mostanában.

Kommentek

A forráskód kommentelése nem egy nagy téma, de mivel eddig nem volt róla szó, viszont szeretném már használni, ezért időszerűenk találtam bemutatni végre.

A forráskódban minden egyes utasításunk végrehajtásra kerül. Ez teljesen tiszta, ezért írunk programot. Lehetőségünk van viszont olyan sorokat megjelölni a forráskódban, amit nem szeretnénk ha lefutna. Ide aztán tetszőleges szöveget írhatunk majd, ahova például nagyon hasznos kiegészítéseit írhatjuk az algoritmusnak a könnyebb érthetőség végett. Ez a forráskód kommentelése.

Pythonban a # karakter kezdi a kommentet. Minden ami ettől a karaktertől jobbra áll a sorban az nem kerül többé értelmezésre. Írhatunk ilyet a sor elejére, a végére, ahova tetszik:

# Ez a program összead két számot

a = float(input('a: '))  # Az első szám az 'a' változóba kerül
b = float(input('b: '))  # A másik pedig a 'b'-be

print(a + b)

A fenti forráskód színezése jól kiemeli a kommenteket, így azokat könnyű megkülönböztetni a kód többi részétől.

Többsoros kommentet vagy úgy írhatunk, ha a komment minden sorát #-el kezdjük, vagy ha a triple-quoted stringet használunk.

Tripple-quoted string

Sajnos nem tudom ennek a nyelvi elemnek a hivatalos magyar elnevezését, úgyhogy az angolt használom végig.

A str típusról már többször is volt szó. Tudjuk hogy vagy ' vagy " karakterek közé kell írni. Bár akkor ezt nem írtam, de ezek a stringek a természetüknél fogva egysorosak voltak (a forráskódban egy sort foglaltak el).

Létezik a stringek egy másik felírási módja is, ilyenkor tripla ' vagy " jelek közé írjuk a szöveget. Az így definiált string használható többsoros kommentelésre is:

"""
Ez a program összead két számot

Slapec - 2016
"""

a = float(input('a: '))
b = float(input('b: '))

print(a + b)

Szigorúan véve ez nem számít kommentnek, mivel ez a kifejezés egy teljesen helyes str-t definiált, de általánosan elfogadott a programozók közt a használata.

Én a többsoros kommenteket nem szoktam triple-quoted stringbe írni.

A triple-quoted string kezdő- és záró elemei közé beírt szöveg pontosan úgy jelenik meg, ahogy az a forráskódban látható. Ez azt jelenti, hogy ha behúzást vagyunk kénytelenek használni, mert a string egy blokkban került definiálásra, akkor a string része lesz a behúzás is, ezt pedig általában el akarnánk kerülni:

def hello():
    print("""
    Python says:

    Hello World!
    """)

hello()

Ez a forráskód a szemnek elég kellemes, viszont a konzolban ez jelenik majd meg:


    Python says:

    Hello World!

Igen így, egy sorral lejjebb, 4 szóközzel beljebb.

Ahhoz hogy ez normálisan jelenjen meg a kimeneten, így kell átírni a stringet:

def hello():
    print("""Python says:

Hello World! """)

hello()

Most őszintén, hogy néz már ez ki?

Olyan esetekben, amikor nem embernek írunk str-t (pl.: egy adatbázis szervernek), akkor ez nem érdekel senkit, viszont ha konzolba, akkor zavaró tud lenni. Így tehát egyelőre csak maradjunk a kommentelésnél.

Fájlba írás-olvasás

Eddig minden esetben szöveget írunk ki, és azt is a konzolba. Ez az ilyen egyszerű scriptekhez teljesen jó volt: jön a program, kiír valamit, vagy elolvassuk vagy nem, és már megy is tovább. Tudunk szerencsére olyan programot is írni, ami képes fájlt létrehozni és írni bele, vagy meglevőt beolvasni, sőt, nem csak szöveges de bináris adatokkal is dolgozhatunk, és hát ha valakik, a számítógépek imádják a bináris adatokat.

A filozófia

Az írás-olvasás (továbbiakban: fájlművelet) egy több lépésből álló folyamat. Ilyenkor valamelyik lemezre írunk majd, mint ahogy minden más program is ami a számítógépen fut.

A fájlművelet legalább két lépésből fog állni: a megnyitásból és bezárásból. Bezárt fájlt értelem szerűen nem lehet majd írni, és a nyitva felejtett fájlokat nem módosíthatja majd más program, így fordulhat elő olyan, hogy nem tudunk letörölni egy fájlt a számítógépről, mert valamelyik program nyitva tartja azt.

A fenti két kötelező lépésen túl elsősorban az olvasó és író utasításokat fogjuk használni, de azért majd ennél kicsit mélyebbre is leásunk.

Az első fájl megírása

Itt az ideje beleírni valamit az első fájlunkba, hogy utána majd olvashassunk is belőle. Lássuk a kódot:

file = open('hello.txt', 'w')
file.write('Hello from Python!')
file.close()

Ezzel a munkakönyvtárban, azaz ahol a programot futtatjuk, létrejön egy hello.txt nevű fájl, amiben a Hello from Python! üzenet lesz majd látható.

Nézzük sorba hogy mi is történik:

  1. Az open függvénnyel nyithatunk meg egy fájlt. A függvény első argumentuma a fájlnév lesz, ami akár teljes elérési útvonalat is tartalmazhat. A második argumentum a fájl megnyitásának módja.
    • A fenti példában a 'w' a write-ot jelenti, azaz a fájlt írásra nyitjuk meg. Az írás mindig a fájl elején kezdődik, tehát ha meglevő fájlt nyitunk meg írásra annak a tartalma azonnal elvész.
      A fájlba ilyenkor csak írni lehet, olvasni közben nem. Olvasáshoz a 'r'-t kell használni (ami a read-et jelenti), ami közben az írás nem lehetséges. A fájl megnyitható még hozzáfűzésre is, amikor a meglevő tartalom végére kerülnek az új sorok. Létezik még mód arra hogy egyszerre írjunk és olvassunk a fájlból illetve a fenti műveleteknek létezik bináris formája is, de ezekkel egyelőre nem foglalkozunk.
  2. A file változóban egy fájl objektum van. Ennek az egyik metódusa a .write(), ami beírja az argumentumául kapott str-t a fájlba
  3. A fájlművelet végén a változó .close() metódusával zárható le a fájl.

Ilyen egyszerű az egész.

Fájlból olvasás

Az előbbi fejezetben létrejött egy fájl, úgyhogy lesz min kísérletezni. Kezdjük a fájl tartalmának beolvasásával.

file = open('hello.txt', 'r')

content = file.read()
print(content)

file.close()

A kód első sorában megnyitottuk a hello.txt fájlt olvasásra (r argumentum). Ez után a file objektum .read() metódusával betöltjük a content nevű változóba a fájl teljes tartalmát. A 4. sorban egyszerűen kiírjuk a változót és végül bezárjuk a fájlt.

Szeretném hangsúlyozni, hogy a .read() a fájl teljes tartalmát betölti a memóriába. Ez nekünk most egyáltalán nem gond, de ha akkora a fájl hogy az nem fér be a RAM-ba, akkor más módszerekhez kell folyamodni.

A close() elhagyása - a with utasítás

Mivel a fájl kinyitása és bezárása mindig együtt jár, ezért (oké, nem pont ezért, de többek közt e miatt is) létezik egy olyan utasítás a Pythonban, ami automatikusan be tudja zárni a fájlt, amikor elhagyja a program futása az utasítás blokkját. Ez a with.

Írjuk át a fenti kódot:

with open('hello.txt', 'r') as file:
    print(file.read())

A with utasítás fejébe két kifejezés kerül:

  1. Rögtön a with után következik az open(), ami megnyitja a fájlt.
  2. Az első és második kifejezés közé mindig be kell írni az as szót, és utána kell írni annak a változónak a nevét, amivel nevesítjük az objektumot, amit az első kifejezés eredményezett.

Így tehát, az open() által megnyitott fájlt a file néven érhetjük el a blokkon belül.

A példából talán úgy tűnhet hogy a with-et csak open()-el lehet használni, de ez nem így van, viszont mi egyelőre csak az open()-t ismerjük.

A with utasítást context manager-nek is szokták hívni. A context kifejezés a programozásban valamilyne környezet létezését jelenti. A fenti példában, ameddig a with blokkjában vagyunk, addig egy olyan környezetben fut a forráskód, amiben létezik a file változó.

A with gondoskodik a környezet megnyitásáról és lezárásáról, ami fájl esetén a fájl megnyitása és bezárása. A bezárásra viszont különös figyelmet fordít: az objektumot akkor is bezárja, ha a blokkon belül hiba történik. Ezzel idáig nem foglalkoztunk, de csak gondoljuk végig: kézzel zárnánk be a fájlt, de még mielőtt a bezárásra kerülne a program futása, korábban egy hiba keletkezett. Így sose jut a program a bezáró utasításra.

Ezen a blogon mindig a with utasítással kezelem a fájlokat.

További sorok beírása

Gondolom elég triviális a megoldás: több sor fájlba írásához többször kell meghívni a .write()-ot. Nézzük hogy mi is történik ilyenkor:

with open('hello.txt', 'w') as file:
    file.write('Line 1')
    file.write('Line 2')

with open('hello.txt', 'r') as file:
    print(file.read())
    file.close()

Ha futtatjuk a fenti kódot, akkor a konzolban a Line 1Line 2 üzenet jelenik meg. Azaz bár a forráskódban a 'Line 1' és 'Line 2' szövegek jól láthatóan különböző hívásokkal íródtak a fájlba, a visszaolvasásnál mégis egymás mellett jelennek meg. Ez egyébként teljesen természetes viselkedés: senki se mondta, hogy a fájlban legyen egy enter a két sor között, tehát hogy azok ne egy sorba íródjanak ki.

Vezérlő karakterek

Létezik néhány olyan, úgynevezett vezérlő karakter, amiket ugyan úgy be lehet írni egy str-be mint bármelyik másik betűt, de ezek nem jelennek majd meg a szövegben, hanem valamilyen primitív szövegszerkesztési műveletet hajtanak végre. Az egyik ilyen karakter hatására a fájlban új sor kezdődik, ez lesz majd a sortörés (vagy sor emelés, esetleg új sor).

Úgy is hívják ezeket a karaktereket hogy nem nyomtatható karakterek (non printable characters).

Két vezérlő karaktert mutatnék be egyelőre:

  • \n
    Ez a karakter új sort kezd
  • \t
    Ez pedig ugyan azt a karaktert jelöli, ami akkor keletkezik amikor a Tab billentyűt lenyomjuk

Oké, de ha ezek karakterek, akkor mégis miért állnak két betűből?

Ahhoz hogy megkülönböztethesse a Python (meg úgy általában a programozási nyelvek) a vezérlő karaktereket a mezei betűktől, a vezérlő karaktereket a \ betűvel kell kezdeni. Ezt kiléptető karakternek (escape character) nevezik, és a tőle jobbra álló karaktereket a Python megpróbálja vezérlő utasításként értelmezni.

A vezérlő karakterek használata igen egyszerű: csak be kell írni őket a stringbe:

with open('hello.txt', 'w') as file:
    file.write('Line 1\n')
    file.write('Line 2')

with open('hello.txt', 'r') as file:
    print(file.read())

Az egyetlen különbség hogy a 'Line 1'-ből 'Line 1\n' lett. Ezzel a kis módosítással a Line 1 szöveg végén új sort kezdünk, így a következő szöveg kiírása már abban a sorban folytatódik.

Ha futtatjuk a programot, akkor látszik is hogy egymás alá kerültek a felíratok:

Line 1
Line 2

Maradjunk még kicsit a vezérlő karaktereknél. Mi van akkor, ha azt akarom, hogy megjelenjen a \ jel a kiírt szövegben? Honnan tudja a Python hogy kéléptető karakterként akarom használni vagy sem?

A válasz az hogy nem tudja. A \ mindent kilépet, de ha önmagát léptetjük ki vele, akkor megjelenik a \ jel. Ezt sokkal egyszerűbb kipróbálni mint elmagyarázni:

with open('hello.txt', 'w') as file:
    file.write('Line 1\\Line 2')
    file.close()

with open('hello.txt', 'r') as file:
    print(file.read())

Az újdonság a 2. sorban látható. A konzolban most a Line 1\Line 2 felírat jelent meg. Nem is fűznék hozzá több kommentárt.

Fájl feldolgozása soronként

Gyakori feladat hogy a fájlból egy teljes sort akarunk beolvasni. A .read()-el ez kicsit körülményesen kivitelezhető:

with open('hello.txt', 'w') as file:
    file.write('Line 1\nLine 2')

with open('hello.txt', 'r') as file:
    lines = file.read().split('\n')
    print(lines)

Az 5. sorban beolvassuk a fájlt a .read()-el. A visszatérési érték egy str, aminek a .split('\n') metódusa szétvágja egy tömbbé a szöveget. A '\n' karakternél történik a vágás, ami ugye a sor végét jelenti, tehát a tömbben minden elem egy sora lesz a fájlnak. Látszik is a konzolban: [‘Line 1’, ‘Line 2’]

Szerencsére, ha a fájl objektumot elkezdjük egy for ciklussal iterálni, akkor minden iterációval a fájl egy sorát kapjuk meg:

with open('hello.txt', 'w') as file:
    file.write('Apple\nOrange')

with open('hello.txt', 'r') as file:
    for line in file:
        print('Fruit:', line)

A 2. sorban beírtuk két gyümölcs nevét a fájlba. Az 5. sorban egyszerűen iteráljuk a file változó értékét, és a 6. sorban kiírjuk a Fruit: szót, és utána a fájl egy sorát.

Ha futtatjuk a programot akkor majdnem jó az eredmény:

Fruit: Apple

Fruit: Orange

Csak megjelent egy plusz üres sor a szövegek között. Ez azért történt, mert a sor beolvasásával együtt a sortörés is olvasásra és értelmezésre kerül. Ez még nem is lenne talán annyira nagy baj, de a kiírás során a print() is új sorban kezdi meg a kiírást, azaz egyszer új sort kezdtünk mert a fájlból ezt olvastuk meg, aztán még egy sor következik, mert a print() csinált egyett. Hát ezért van luk a két gyümölcs között.

Megmondhatnánk a print()-nek, hogy ne kezdje új sorba a kiírásokat, de hasznosabb inkább levágni a sor végéről a \n-t, mert az esetek többségében amúgy sincs rá szükség a programban:

with open('hello.txt', 'r') as file:
    for line in file:
        print('Fruit:', line.strip())

A 3. sorban a line .strip()-metódusa eltávolítja a string jobb végéről a haszontalan betűket: sortörést, és felesleges szóközöket is!

Így már helyesen ez látható a konzolban:

Fruit: Apple
Fruit: Orange

Ugrás a fájlban

Írjuk ki kétszer ugyan annak a fájlnak a tartalmát:

with open('hello.txt', 'r') as file:
    print(file.read())
    print(file.read())

Ha futtatjuk a programot, akkor baromira csak egyszer jelent meg a fájl tartalma.

Ez azért történik, mert a fájlok nagyon primitív objektumok (a primitívet nem úgy értem hogy buták, hanem hogy egyszerű a működésük). A fájl a programozási nyelv szempontjából olyan mint egy szalag. Létezik egy mutató, ami tárolja hogy épp melyik pontján állunk a fájlnak. Az olvasás és írás során ez a mutató előre halad, és ha csak nem rakjuk kézzel hátrébb a mutatót, akkor az ott is marad a helyén.

Így tehát az történt, hogy a .read()-el a kiíródott a fájl tartalma, és a mutató a fájl végén maradt. A .read() második hívásával a mutató a fájl végétől kezdte az olvasást ahol értelem szerűen … nem volt semmi. Ahhoz hogy menjen a kiírás, vissza kell tekerni a fájlt. Ezt a .seek() metódussal tehetjük meg:

with open('hello.txt', 'r') as file:
    print(file.read())
    file.seek(0)
    print(file.read())

A .seek() argumentuma az a pozíció ahova a fájlt tekerni akarjuk. Logikusan a 0 a fájl elejét takarja. A 3. sorban a file.seek(0)-val újra a fájl elejére mutat a Python, így a .read() sikeres lesz, és újra meg is jelenik a fájl tartalma a konzolban.

A .tell() metódus megmondja, hogy mi a fájl-mutató aktuális pozíciója:

with open('hello.txt', 'r') as file:
    print(file.read())
    print(file.tell())

Ha a hello.txt tartalma még mindig a 'Apple\nOrange' szöveg, akkor eredményül 12-t kapunk. Egyébként itt jól látható: ha megszámoljuk, ebben a stringben 13 betű van, de a \n egynek számít, és így ki is jön a 12.

A print() és a fájlok kapcsolata

A Unixos/Linuxos filozófia szerint a konzol, amibe megjelenik a szöveg, nagyon hasonló azokhoz a fájlokhoz amik a merevlemezen megjelennek. Ezért van az, hogy a \n vezérlőkarakter ugyan úgy új sort kezd a konzolban, mint ahogy a fájlban is tette.

Létezik még egy vezérlő karakter, a \r (kocsivissza vagy carriage return) aminek hatására a kurzor visszakerül a sor elejére, tehát minden betű ugyan abban a sorban, de előről íródik ki a továbbiakban, azaz felül tudjuk írni a szöveget. Ha ezt a karaktert egy fájlba írjuk, akkor semmi se történik. Ha konzolba, akkor viszont sokkal érdekesebb az eredmény:

print('Hello World\rBye')

Így a (nem túl értelmes) Byelo World szöveg lesz látható majd a konzolban. Ahogy feljebb írtam tehát:

  1. Szépen kiíródnak egyesével a betűi a stringnek
  2. A \r-nél a kurzor visszaugrik a sor elejére
  3. A sor elejétől folytatódik a kiírás, de oda már oda volt írva hogy Hello World, így hát a Hel-rész szépen felülíródik

Egyelőre ez maradjon csak érdekességnek, de később hasznos lehet, például ha egy betöltő csíkot akarunk kirajzolni.

Csak hogy teljes legyen a kép: a print() egyik opcionális argumentumban megadható, hogy milyen fájlba történjen az írás, ez a file=:

with open('hello.txt', 'w') as file:
    print('Hello World', file=file)

Ha nem használjuk a file= argumentumot akkor hova írja a print() a szöveget? Hát a standart outputra, ami a konzol. De ezt a kiírásról szóló részben már meséltem.

Vége

Kicsit elhúzódott ez a bejegyzés, viszont most egy nagyon fontos részén estünk túl a Python nyelvnek. Azt hiszem, hogy ennyi rész után már elég tudással rendelkezünk a Python működéséről ahhoz, hogy az emelt informatika érettségi programozási feladatát meg tudjuk oldani, így a következő bejegyzés erről fog szólni.

Ez a blog nem az érettségire készít fel, de érdekesnek találtam a kérdést, hogy elég-e ez a 9 rész a probléma megoldására.

Az elején úgy gondoltam, hogy a bináris fájlokkal is lesz most idő, de már 14 ezer karakternél járok, úgyhogy majd legközelebb.

Ez a bejegyzés a Python tutorialom egyik része. Az összes rész listája itt fellelhető.

-slp

Nincsenek megjegyzések:

Megjegyzés küldése