2016. november 20., vasárnap

11 - Az objektumorientált programozás - 1 - Az első osztály

Ma nagy témába kezdünk bele: az objektumorientált programozásba. Ez is egy úgynevezett programozási paradigma, úgy mint a procedurális programozás, amit a függvényekről szóló bejegyzésben ismertettem. Ahhoz hasonlóan, tegyük fel most is a legfontosabb kérdéseket:

Mi az az objektumorientált programozás?

Az objektumorientált programozás (továbbiakban OOP) lényege igen egyszerű: összezárjuk az adatot, és a rajta végezhető műveleteket (encapsulation). Sok objektummal találkoztunk már az ezt megelőző részekben, úgyhogy innen vennék egy példát.

Nézzünk egy list-et:

array = [1, 2, 3, 4]

Ha hozzá akarunk adni egy elemet a tömbhöz akkor azt így tehettük meg:

array.append(5)

Ez után ha printeljük az array-t akkor a [1, 2, 3, 4, 5] jelenik meg a kimeneten. Tehát az .append() metódussal az array-ben található adaton végeztünk egy műveletet: hozzáadtunk egy elemet. Látható: az adat, és a rajta elvégzett művelet mind ott van az array-ben.

Ez a gondolatmenet konkrétan annyira átjárja a Python programozási nyelvet, hogy a szintaxis elemein kívül minden amit csak le tudunk írni az valójából egy objektum.

Az OOP persze ennél sokkal több; több félévnyi előadást lehet belőlük tartani az egyetemen, és könyvtárakat lehet megtölteni a szakirodalommal. Mi most rövidre fogjuk a témát, mert a lényeg igazából egyáltalán nem bonyolult.

A szakirodalom és az egyetem meg foglalkozzon a speciális és tudományos esetekkel

Miért akarunk objektumorientáltan programozni?

Az egységbe zárás nyilvánvaló előnye tehát az, hogy kérdés nélkül tudjuk hogy milyen műveleteket végezhetünk az adaton, hisz ezek a műveletek ott vannak az adattal együtt. Például tudjuk, hogy a list típusú objektumoknak nincs .keys() metódusuk, mert az csak a dict objektumoknak van.

Ez az összezárás növeli a forráskód hordozhatóságát is: egy objektumban rengeteg adat és metódus elhelyezhető. Ha a függvények törzsében a forráskód egy kis izolált környezetben futott, akkor ahhoz képest egy objektumban egy konkrét izolált világ létezik.

Másik fontos dolog, hogy az objektumok működését leíró osztályok képesek a rá hogy tulajdonságokat örököljenek (inheritance) más osztályoktól. Ezt mindig valamilyen állattal szokták szemléltetni, úgyhogy vegyünk egy macskát.

A macska egy emlős állat, így tehát minden igaz rá ami úgy általában az emlősökre. Ugyan így az ember is egy emlős, tehát ha van egy rendszerünk amiben macska és ember osztályok is léteznek, akkor érdemes ezeknek az emlősök tulajdonságait örökölniük. Ennek több gyakorlati haszna van:

  • Csökkentjük a forráskód redundanciáját azzal, hogy a mindkét fajra jellemző tulajdonságokat egy ősosztályból származtatjuk, amit aztán a származtatott osztályok automatikusan meg is kapnak.
  • Ahogy a természetben, úgy a programban is, mind a macska és az ember objektumok emlősökként is fognak viselkedni. Így tehát ha a programban amit egy emlős objektummal meg lehet tenni, azt egy macskával vagy egy emberrel is lehetséges.

Sokan azt magyarázzák, hogy az OOP szemlélet közel áll a természet működéséhez, és végülis ezt beláthatjuk a fenti példákon.

A Python és az OOP kapcsolata

Bár a tudtunkon kívül eddig is folyamatosan objektumokat hoztunk létre és léptünk velük kapcsolatba a forráskódban, ettől még nem programoztunk objektumorientáltan, csak objektumokat használtunk. A Python lehetőséget nyújt többféle paradigma szerinti programozásra: lehet írni a kódot csak úgy fentről-lefelé, ahogy tetszik; vagy függvényekben, és nemsokára osztályokban is. Ezek egymás mellett jól elférnek, a Python nem kényszerít rá semmilyen szemlélet használatára.

Az OOP annyira egy sikeres szemlélet lett az évek során, hogy léteznek olyan programozási nyelvek, amikben csak és kizárólag objektumorientáltan lehet fejleszteni. Ilyen a Java vagy a C#.

Ezt elolvasva úgy tűnhet, hogy az OOP az evolúció csúcsa, de minimum az a pont a programozásba, ahova minden fejlesztő megérkezik. Mint amikor nagykorú lesz az ember. Én ezt nem teljesen gondolom így, de tény hogy ma az OOP az alap amire az iparban szükség van.

Az első osztályunk Pythonban

Osztály? Nem objektumokról volt idáig szó?

Ahhoz hogy elkerüljük a félreértéseket be kell vezetnünk néhány új fogalmat, úgyhogy jól tessék figyelni!

Az objektumok működésének leírását egy osztályban (class) végezzük. A leírás alapján gyárthatunk egy halom különböző példányt (instance). Ezeket az élő példányok hívjuk objektumoknak is (objects). Egy objektum osztálya egyben az objektum típusa is. Ahogy a [1, 2, 3] típusa list, úgy a lenti példában a my_cat típusa majd Cat osztály lesz.

Éppen ezért, na meg a szóismétlések kerülése végett, a macska objektumokat hívom majd Cat osztályúaknak vagy Cat típusúaknak is.

Elég a pofázásból, lássuk az első osztályt!

class Cat:
    def __init__(self, name):
        self.name = name

    def meow(self):
        print('Meow')


my_cat = Cat('Narancs')
print(my_cat.name)
my_cat.meow()

Ezzel most nagyjából annyi új információt zúdítottam rátok, mintha 0-ról kezdenénk a kódolást.

Bonyolultnak tűnhet, de nem kell aggódni. Ezt a fenti kódot két részre szedhetjük: a class alatti blokk az osztály definíciója. Az utolsó néhány sor a példányosítás (instantiation), és pár egyszerű interakció az objektummal.

Az osztály

Vegyük az osztály definíciót még egyszer:

class Cat:
    def __init__(self, name):
        self.name = name

    def meow(self):
        print('Meow')

Python nyelvben az osztályt a class szóval kell kezdeni, amit az osztály neve követ. A példában a macskák leírására alkalmas osztályt Cat-nek neveztem el.

Pythonban az osztályok nevét szokás szerint camel case stílusban írják.

Ez azt jelenti, hogy az osztály nagy kezdőbetűvel kezdődik, és ha több szó alkotja az elnevezést, akkor a szavak közt nincs elválasztás, hanem minden szó nagy kezdőbetűt kap és egybe írják az összeset.
Példával sokkal egyszerűbb: Cat, PhoneNumber, ValueError.

Csak hogy teljes legyen a kép: ha rövidítést használunk, és a rövidítéssel együtt négy vagy több nagybetű követné egymást, akkor csak a rövidítés első kezdőbetűje lesz nagy.
Tehát: XmlWriter, StringIO, HtmlReader

Az osztály definíció blokkjában végre ismerős kulcsszó látható, a def. Azt is mondhatnánk, hogy ez egy függvény, de mivel az osztályon bellül van ezért őt metódusnak (method) hívjuk. Ettől még ugyan úgy visszatérhetnek bármilyen értékkel, mint a függvények.

A fenti példában tehát két metódus látható, az __init__() és a meow().

Az __init__()

Az __init__() név egy különleges metódust takar az osztályon belül: ez az úgynevezett konstruktor (constructor). Ez azt jelenti, hogy a metódus automatikusan fut akkor, amikor példányosítjuk az osztályt, azaz amikor egy új objektumot készítünk az osztály alapján. Ebben a metódusban lehet megadni, hogy a létrejövő objektum milyen kezdeti értékekkel rendelkezzen.

A konstruktorból nem lehet semmilyen értékkel se visszatérni.

Arra hogy a metódus pontosan mikor és hogyan fog lefutni kicsit később térünk vissza.

Pythonban azokat a metódusokat, amik __-el kezdődnek- és végződnek, magic method-nak hívják, és rengeteg létezik belőlük.

A self

Az __init__ két paramétert vár, a self-et és a name-et. Az osztályon belül a metódusok legelső paramétere, amit itt most self-nek hívok, mindig különleges jelentőséggel bír: ez a paraméter impliciten (“magától”) értéket kap, méghozzá az objektumot magát, majd akkor, amikor példányosítottuk az osztályt. Tehát ha egyetlen paramétert se vár a metódusunk, a selfet akkor is ki kell írni, máskülönben hibát jelez a Python.

A paramétert szokás szerint self-nek hívják, de igazából akárhogy el lehet nevezni, viszont nagyon hülyén fognak rátok nézni ha nem így hívjátok.

A valóságban van egy olyan eset, amikor a legelső paraméter mégsem képvisel különleges értéket, de erről majd később írok egyszer.

A self arra való, hogy ezen keresztül tudják majd az objektum metódusai egymást és az adattagokat elérni.

Mivel a self mindig ott áll a metódus paramétersorában, ezért nem is szokás külön említeni. Így a jövőben nem azt fogom írni, hogy a konstruktor két paramétert vár, a selfet és a name-et hanem hogy a konstruktor egy paramétert vár, a name-et.

Ahogy tovább haladunk a self mellett, ugyan azok a szabályok lesznek érvényesek, amik a közönséges függvények esetében is, úgyhogy ezen nem is agyalunk tovább.

A konstruktor törzsében egy utasítás áll, a self.name = name. Itt egyszerűen a paraméterül kapott name értékét eltároljuk a self.name név alatt. Láthatólag a self.name nem létezett korábban, de ez nem probléma: a self-hez bármikor hozzáadhatunk egy új adattagot (property).

Ezek után a a meow() már egy igen egyszerű metódus: ha meghívjuk akkor kiírjuk a konzolba hogy nyávog a macsek. Itt jól látható, hogy a self-et kötelező volt kiírni, de azon kívül nincs más paramétere, azaz összességében nem vár semmilyen paramétert ez a metódus.

Nagyon-nagyon fontos megérteni, hogy a self az objektumra mutató paraméter (a self jelentése önmagam). Ebbe a self-be a példányosításkor bekerülnek az osztályban definiált metódusok, amik példányosított entitásra lesznek érvényesek, de ezen kívül kvázi üres a self objektum.

A példányosítás

Feljebb definiáltuk az osztályt. Ahogy (sokszor) írtam, ez csak egy tervrajz, az objektum működését írja le. Amikor elindul a program a Python értelmezi az osztályt de ettől még nem jön létre semmi objektum. Hasonlóan mint amikor a függvényt definiáltuk, az sem futott le addig amíg meg nem hívtuk meg.

A fenti példa utolsó két sorában készítünk egy élő példányt, egy objektumot az osztály alapján. Lássuk a kódot újra:

my_cat = Cat('Narancs')
print(my_cat.name)
my_cat.meow()

Az első sorban a Cat('Narancs') kifejezéssel hozunk létre egy objektumot. Kinézetre ez pont olyan mint ha egy közönséges függvény hívnánk meg, de tudjuk hogy a Cat most egy osztály.

Az osztály példányosítása során egy argumentumot adtunk meg, a 'Narancs' stringet. A példányosításkor a konstruktor automatikusan (impliciten) meghívásra kerül, így a Cat osztály __init__() metódusába kerül a program futása, és ahogy korábban írtam, itt eltároljuk a name paraméterben megkapott értéket ('Narancs') az objektumban önmagában, a self segítségével.

Ezzel véget is ért a konstruktor futása, elkészült az objektum amit my_cat névvel tároltunk el.

A fenti példa 2. sorában kiírjuk az objektum name property-jének értékét. Ez ugye az adattag a konstruktor lefutásával létrejön, így minden gond nélkül megjelenik a macska neve a kimeneten.

Az utolsó sorban meghívjuk az objektum meow() metódusát, és simán megjelenik a Meow a konzolban.

Ha ugyan úgy kell egy osztály példányosítani mint egy függvényt meghívni, akkor honnan tudjuk hogy mikor-melyiket hívjuk meg? Mi van akkor, ha valaki nem az ajánlás szerint nevezi el az osztályát? A válasz egyszerű: nem tudjuk. De igazából az aztán tök mindegy hogy függvényt hívogatok-e ami visszatér egy objektummal, vagy egy osztályt példányosítottam, csak menjen amit akarok.

A metódusok definiálásának sorrendje

Azt már észrevehettük hogy egyáltalán nem mindegy a forráskódban, hogy az utasítások milyen sorrendben követik egymást. Például nem hivatkozhatunk egy változóra addig, amíg azt nem definiáltuk.

Az osztály esetében egy kicsit lazul ez a szabály: az osztályban a metódusok tetszőleges sorrendben kerülhetnek definiálásra. A metódusok hívásának a sorrendje lesz az, ami számít.

Macskák a dobozban

Mivel a macskák imádják a dobozokat, ezért gond nélkül belepakolhatjuk őket a Python kódban is. Egy tömbnyi macskát eltárolhatunk például egy list-ben, így:

box = [Cat('Narancs'), Cat('Omlás'), Cat('Don Gatto'), Cat('Vörös Harry'), Cat('Félszemű Babylon')]

Ez után ha kiírjuk a tömb elemeit egymás alá:

for cat in box:
    print(cat)

Akkor (elsőre meglepően) ezt az izét látjuk:

<__main__.Cat object at 0x7ff7daec1160>
<__main__.Cat object at 0x7ff7daec16d8>
<__main__.Cat object at 0x7ff7daec1710>
<__main__.Cat object at 0x7ff7daec1748>
<__main__.Cat object at 0x7ff7daec1780>

Ha közvetlenül kiírunk egy objektumot a print()-el akkor, mivel senki se mondta meg a Pythonnak, hogy a Cat kiírása alatt mint értünk, jobb híján az objektum osztályának a nevét (“teljes nevét”, modullal együtt) és a memória címét kapjuk eredményül, amivel itt most nem megyünk sokra.

Kitérő: az objektumok kiírása

Azt, hogy az objektum hogyan írható ki, a __repr__() magic methodban tudjuk megadni. A print() ezt a metódust automatikusan meghívja majd a kiírás pillanatában.

Ez egy elég rövid fejlesztés, de azért jobb ha látjuk az egész osztályt:

class Cat:
    def __init__(self, name):
        self.name = name

    def meow(self):
        print('Meow')

    def __repr__(self):
        return '<Cat object. Name: {0}>'.format(self.name)

Az utolsó két sorban látható az újdonság. A metódus neve fix, és paramétereket se kell neki adni (ez szabály). A metódus törzsében tetszőleges utasítások állhatnak, de kötelező a végén visszatérni egy str-el. Esetünkben ez egy egyszerű kis format string.

Ezzel a kis módosítással ha újra futtatjuk a kódot akkor már ez jelenik meg a kimeneten:

<Cat object. Name: Narancs>
<Cat object. Name: Omlás>
<Cat object. Name: Don Gatto>
<Cat object. Name: Vörös Harry>
<Cat object. Name: Félszemű Babylon>

Azért ez elég fasza nem? A beépített magic methodok között rengeteg ilyen érdekes metódus van még, de erről külön bejegyzések fognak majd szólni.

Természetesen ahogy a print() meghívta a mi metódusunkat, úgy ezt mi is megtehetjük. Erre használható a repr() függvény, de akár ilyet is írhatunk: print(cat.__repr__())

A második osztályunk Pythonban

Nem kétség, hogy a list kiválóan őrzi a benne felsorolt objektumokat, mivel az egy általános tároló: bármit belerakhatunk, bármit kivehetünk belőle.

A valóságban nem biztos hogy jó ötlet lenne az amúgy is zsúfolt dobozba véletlenül egy blökit is belerakni. Szóval lehet érdemes lenne írni valami tároló objektumot, amibe csak a megadott típusú objektumok kerülhetnek bele, és a zsúfoltság elkerülése végett korlátozva van a mérete is. Ilyet egyik beépített Python tároló se tud, úgyhogy írjunk egy sajátot!

Tervezés

Nem kell aggódni, attól hogy közvetlenül a list-et nem tudjuk használni a probléma megoldására, még nem kell újra feltalálnunk a tömböt. Egy olyan osztályt írunk, ami elfed magában egy list-et. Ez azt jelenti, hogy az osztályunkban ugyan lesz egy tömb, de ahhoz csak mi saját metódusainkkal nyúlhatunk hozzá. Ide fogjuk elhelyezni az olyan ellenőrzéseket, mint hogy:

  • Az osztályhoz hozzáadott objektum Cat típusú-e?
  • Nincs-e tele az objektumunk macskákkal?

Lássuk akkor szép sorban hogy mit-és-hogyan tudjon majd az osztály:

  1. Paraméterül lehessen megadni neki, hogy mekkora legyen a tároló mérete. Az alapértelmezett érték legyen mondjuk 3.
  2. A konstruktorban hozzunk létre egy üres tömböt
  3. Legyen az osztálynak egy add_cat() metódusa. Itt mindig ellenőrizzük, hogy a paraméter Cat típusú-e, és hogy nincs-e tele a tároló
    • Ha minden rendben akkor hozzáadjuk a konstruktorban létrehozott tömbhöz a paraméterül kapott objektumot
    • Ha valami gond van, akkor dobunk egy hibát
  4. Lehessen kiírni a tároló objektumunkat, hogy lássuk hogy mi van benne

Oké, egyetlen fontos kérdés maradt: mi lesz az osztály neve? CatBox, természetesen.

Megvalósítás

class Cat:
    def __init__(self, name):
        self.name = name

    def meow(self):
        print('Meow')

    def __repr__(self):
        return '<Cat object. Name: {0}>'.format(self.name)


class CatBox:
    def __init__(self, size=3):
        self.size = size
        self.cats = []

    def add_cat(self, cat):
        if isinstance(cat, Cat):
            if len(self.cats) <= self.size:
                self.cats.append(cat)
            else:
                raise MemoryError('The box is full (it has {0} cats}.'.format(self.size))
        else:
            raise TypeError('Expected a Cat instance, got {0} instead'.format(type(cat)))

    def __repr__(self):
        return repr(self.cats)

Csak úgy ömlik az új információ, mi?

A kód tetején a Cat-ben semmi változás nincs, így ezzel nem is foglalkozunk.

A CatBox.__init__() metódusnak egy paramétere van, a size, azaz a macskás doboz mérete. A konstruktoron belül eltároljuk a kapott argumentumot azért, hogy később a macskák hozzáadásánál elvégezhessük vele szemben az ellenőrzést. Ugyan itt létrehozunk egy tömböt is cats néven. Ide kerülnek majd be a macsekok. Ezzel meg is vagyunk.

Az add_cat() metódus már combosabb, úgyhogy nézzük még egyszer:

def add_cat(self, cat):
    if isinstance(cat, Cat):
        if len(self.cats) <= self.size:
            self.cats.append(cat)
        else:
            raise MemoryError('The box is full (it has {0} cats}.'.format(self.size))
    else:
        raise TypeError('Expected a Cat instance, got {0} instead'.format(type(cat)))

Egy paramétert vár, aminek cat lesz a neve.

A 2. sorban az isinstance() segítségével történik a típus ellenőrzése. Ez a függvény két paramétert vár: az első egy objektum a második pedig egy osztály (illetve általánosabbat: egy tetszőleges típus). Válaszul azt kapjuk meg, hogy az objektum osztálya az-e ami a második paraméterében szerepel.

Ez így elsőre furcsának tűnhet, de igen, magával az osztállyal is végezhetünk műveleteket (itt például egy feltételben használtuk), ugyan úgy, mint ahogy a függvényeknél is tehettük.

Ha ez a feltétel teljesül, akkor a paraméter biztosan Cat típusú, szóval haladhatunk lefelé, viszont ha nem megfelelő osztályú az objektum, akkor egy TypeError exceptiont dobunk el.

Egy pillanatra álljunk meg az exception szövegénél, mert van ott is egy új függvény:

'Expected a Cat instance, got {0} instead'.format(type(cat))

A type() függvénnyel megkapjuk a cat objektum típusát. Hangsúlyozom, az eredmény egy típus lesz, ami a cat objektum esetén maga a Cat osztály. Az osztály kiírásával az osztály nevét kapjuk egyébként (többé-kevésbé). Így lesz egy tök csinos hibaüzenetünk, amiben látszódik hogy milyen típusú objektumot vártunk, és mit kaptunk helyette.

Lejjebb, a 3. sorban ellenőrizzük, hogy nincs-e tele a tároló. Egyszerűen csak összehasonlítjuk, hogy a konstruktorban létrehozott tömb mérete kisebb vagy egyenlő-e a konstruktorban megadott limitnél. Ha itt is tovább tudunk haladni, akkor hozzáadjuk a self.cats tömbhöz a paraméterül kapott objektumot, és készen is vagyunk.

Ha a self.cats mérete túl nagy, akkor a másik ágban eldobunk egy MemoryError exceptiont, ami paraméterül kap egy kis szöveget, csak hogy tudjuk hogy mi történt.

A MemoryError, mint a TypeError is, a Python egy-egy beépített osztálya, ezért nincsenek korábban definiálva vagy importálva sehol sem.

Azok után amit idáig tanultunk remélem feltűnt, hogy a MemoryError vagy TypeError elnevezése és használata pont olyan, mint a közönséges osztályoké. Ez nem túl meglepő: ők tényleg egyszerű, sima osztályok.

Zárásként az osztály aljára odakerült a __repr__(), amivel az objektum kiírása lehetséges lesz majd. Ezen belül az objektum saját cats tömbjére meghívjuk a repr() függvényt, és visszatérünk az így kapott értékkel. Ez a függvényhívás végigjárja a tömb összes objektumát, és meghívja azoknak a __repr__() metódusait, amit kidolgoztunk a Cat osztályban. Itt aztán jó mélyen belementünk a függvény hívogatásba, de a vége tök jó lesz, nyugi.

Ezzel nagyjából készen is vagyunk. Akkor példányosítsunk pár macskát és lássuk mi történik!

Próba

my_box = CatBox()
my_box.add_cat(Cat('Narancs'))
my_box.add_cat(Cat('Omlás'))
my_box.add_cat(Cat('Don Gatto'))

Itt bizony már nincs semmi újdonság. Három macska objektum került a my_box-ba, úgyhogy tudjuk hogy tele a tároló. Írjuk ki az objektumot még mielőtt tovább haladnánk:

print(my_box)

Ennek kell megjelennie:

[<Cat object. Name: Narancs>, <Cat object. Name: Omlás>, <Cat object. Name: Don Gatto>]

Nem mondom, elég szexi.

Próbáljuk ki, hogy jól működnek-e az ellenőrzéseink. Rakjunk be egy negyedik macskát a dobozba:

my_box.add_cat(Cat('Vörös Harry'))

Meg is kapjuk a jól megérdemelt exceptiont:

MemoryError: The box is full (it has 3 cats).

Ennek az értelmezésébe nem megyünk bele, mert a hibakezelésről szóló bejegyzésemben már meséltem ezekről a hibaüzenetekről.

Ha gyorsan összedobunk egy Dog osztályt, monduk így:

class Dog:
    def __init__(self, name):
        self.name = name

Igen, ez az osztály nem csinál semmi hasznosat se, de nem Cat, és most csak ez a lényeg.

És megpróbáljuk berakni a tárolóba:

my_box.add_cat(Dog('Scooby'))

Akkor ezt a szépséget kapjuk:

TypeError: Expected a Cat instance, got <class '__main__.Dog'> instead

Elég világos az üzenet szerintem.

Pihenő

Sikeresen elindultunk az OOP ösvény hosszú útján, de most már ideje egy kis pihenőt venni. Ezen a témán egy pár bejegyzés erejéig még csámcsogunk majd, mert igen sok elég fasza lehetőséget kínál ez a nyelvi elem, meg úgy az egész szemlélet. A következő bejegyzésben elővesszük a öröklődés fogalmát, meg a generikusságot is … talán.

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

-slp

2016. szeptember 27., kedd

10 - Modulok, csomagok

Jól kihagytam az érettségis blogbejegyzést. Nem is keresek kifogásokat, lusta voltam. Ellenben már készülőben van (konkrétan több mint a fele magvan) az ezt követő, tehát a 11. része a tutorialnak.

Modulok. Library-k. Függvénykönyvtárak.

Egyre hosszabb kódokat írogatunk és látható, hogy csak nem kéne egyetlen fájlba írni a komplett programot, mert átláthatatlanná válik a munkánk. A modulok ezt a problémát oldják meg: kirakhatjuk a forráskód egyes részeit különböző fájlokba, és ezekből a fájlokból akkor és ott töltünk be egy részt (konkrétabban: valami objektumot, függvényt, vagy csak egyszerű konstans értékeket) amikor arra szükség van.

Ahogy modulokra bontjuk a programunkat úgy a modulok hordozhatóvá is válnak: összegyűjtünk egy csokor objektumot, azok saját modulba kerülnek, és ha egy másik programban ugyan ezekre (vagy csak néhány) az objektumokra lenne szükség, akkor egyszerűen csak oda másoljuk a modult az új programhoz, és már használhatóak azok a kódok, amiket régen megírtunk.

Látható hogy így megoldható a hordozhatóság, és a kód újrahasznosítás problémája. De nem csak a saját magunknak írhatunk modulokat, hanem mások is írhatnak nekünk, és ezek tetszőlegesen terjeszthetőek. A modulok használata a szoftverfejlesztés szerver része, ugyanis rengeteg olyan modul érhető el, amik olyan funkciókat valósítanak meg, amit ha magunknak kéne implementálnunk akkor az igen sokáig tartana (de az is lehet, hogy egyáltalán nem vagyunk elég képzettek hogy kidolgozzuk a részleteket).

Tehát modulokat azért használunk, hogy a saját programunkat szétbontsuk, és azért, hogy új funkciókat érhessünk el a Pythonban, úgy, hogy azokat más programozók helyettünk már megírták.

A standard library

A Pythonra szokták azt is mondani, hogy batteries included (az elemeket tartalmazza), azaz nem csak a nyers szintaxist meg az interpretert kapjuk, hanem egy halom nagyon hasznos modul is beépítve részét képezi a Pythonnak. A beépített modulokat a standard library (stdlib) kifejezéssel szokták illetni.

Oké de milyen modulok állnak rendelkezésre? A The Python Standard Library reference oldalán egy igen hosszú listát láthatunk. Ezek közül egy pár, ami érdekesebb lehet:

  1. datetime - Dátum- és idő kezelő függvények
  2. decimal - Tetszőleges pontosságú tizedes törtek
  3. io - Műveletek be- és kimeneti adatokkal
  4. math - Matematikai műveletek
  5. multiprocessing és threading - Több szálon / processzor magon futó programok fejlesztéséhez
  6. random - Véletlenszerű értékek generálásához
  7. sqlite3 - SQLite SQL adatbázis eléréséhez
  8. subprocess - Programok indítása és vezérlése Python forráskódból
  9. tempfile - Átmeneti fájlok és könyvtárak létrehozásához
  10. time - Aktuális idő vagy program futásának szüneteltetése
  11. traceback - Python traceback-ek elérése str-ként vagy formázásuk
  12. urllib - Adatok letöltése az internetről
  13. unittest - Tesztek fejlesztéséhez
  14. zipfile - .zip kiterjesztésű fájlok írása, olvasása

Tudom hogy ezek a jellemzések igen rövidek. Általában 1-1 modulról 1-2 teljes bejegyzést lehetne mesélni. Valószínűleg ahogy véget ér ez a tutorial elkezdem a standard library részletes bemutatását. De ez még a jövő zenéje.

Ez most csak 14 modul amit így gyorsan összeszedtem, és viszonylag könnyen megfoghatónak találtam, csak a jéghegy csúcsa. Aki a Python telepítés című postom alapján telepítette a Python értelmezőjét, az installálta a pip nevű programot is, ami egy úgynevezett csomagkezelő. Ez a program képes az interneten fellelhető csomagok (modulok) telepítésére, amikből több tízezer található. De erre majd később térünk vissza.

Egy csomagban (package) több modul található, ezért ezek direkt különböző fogalmak.

Modulok használata

A modul az importálás után válik használhatóvá. Ez kétféleképp lehetséges, az import utasítással például így:

import random

E sor után a random modul elérhető lesz a forráskódban, aminek a tartalmára a random névvel hivatkozhatunk. A modulban található értékek (konstansok, függvények, osztályok) hasonlóan érhetőek el, mint ha azok a random objektum adattagjai lennének, azaz a . operátorral, például így:

import random

number = random.randint(0, 10)

A fenti sorral a random.randint() egy darab 0 és 10 közötti véletlenszerű számot generál, amit number néven tárolunk el.

Előfordulhat hogy nem akarjuk a teljes modult elérhetővé tenni, mert nem akarunk olyan sokat gépelni, vagy mert a modul betöltése túl sokáig tartana. Erre az esetre használhatjuk a from ... import ... kifejezést:

from random import randint

number = randint(0, 10)

Az 1. sorban álló kifejezés már-már egy komplett angol mondat: a random modulból importáld a randintet. Látható, hogy ilyenkor többé nem random.randint()-ként érjük el az importált metódust. A random név a forráskódban továbbra se lesz definiálva.

Az importálások általában a forráskód elejére kerülnek, néhány speciális eset kivételével, például a körkörös függőség (circular dependency) esetén.

Importált objektum átnevezése

Ez egy igen rövid történt, ezért leírom. Az as kulcsszóval át tudjuk nevezni az importált modult vagy objektumokat. Ez nagyon jól jön akkor, ha amúgy névütközés alakulna ki a forráskódban, vagy az importált objektum neve túl hosszú, vagy simán csak nem tetszik.

import random as rndm
from random import randint as random_integer

Gondoltam egy számra

Oké, lássuk ezt a gyakorlatban az ide vágó klasszikus feladattal: írjunk egy olyan játékot, hogy a számítógép gondol egy tetszőleges 0 <= x <= 100 számra és a játékosnak ki kell találnia hogy mire gondolt a gép. Ha a kipróbált szám kisebb a gondoltnál, akkor írjuk ki hogy “kisebb”, ha nagyobb akkor értelem szerűen azt hogy “nagyobb”. Addig fusson a játék amíg nincs találat.

Lássuk a kódot!

import random

secret = random.randint(0, 100)

while True:
    player_number = int(input('Your guess: '))

    if player_number < secret:
        print('Your guess is too low')
    elif player_number > secret:
        print('Your guess is too high')
    else:
        print('You guessed my number!')
        break

Igazából az érdekes részeket le is tudjuk az első 3 sorban: az 1. sorban importáljuk a random modult, a 3. sorban pedig előállítok egy véletlenszerű számot 0 és 100 között és eltárolom a secret változóban.

Ez után csak a szokásos dolgok jönnek: indítunk egy vételen ciklust, mivel nem tudjuk hogy mikor ér majd véget a játék. Bekérjük a kipróbált számot amit player_number néven tárolunk el. Ez után jön pár ellenőrzés: ha kisebb a játékos száma akkor kiírjuk hogy túl kicsi a szám; ha nagyobb a játékosé akkor kiírjuk hogy túl nagy; amúgy meg nyerés van. Pont ahogy a feladat leírásban volt.

Na akkor futtassunk egy menetet:

Your guess: 50
Your guess is too low
Your guess: 75
Your guess is too high
Your guess: 62
Your guess is too low
Your guess: 68
Your guess is too high
Your guess: 65
Your guess is too low
Your guess: 66
Your guess is too low
Your guess: 67
You guessed my number!

Tök fasza nem?

A programot megszakítani a CTRL+C billentyűkombinációval lehet (szokás szerint).
A kitalálás során fejben a bináris keresés algoritmusát alkalmaztam.

Dátum és óra

Írjunk egy egyszerű kis scriptet ami a számítógépen beállított dátumot és időt mutatja élőben, tehát minden másodpercben frissüljön:

import time

while True:
    print(time.strftime('%Y-%m-%d %H:%M:%S'), end='\r')
    time.sleep(1)

A forráskód futását a sleep() függvénnyel tudjuk megállítani, ami a time modulban található. Itt található többek közt az strftime() függvény is, amivel formázhatjuk az idő megjelenését.

Importáltuk tehát a time modult, és indul is a végtelen ciklus. Itt a print()-en belül meghívjuk a time.strftime() függvényt. Az strftime()-nak adott '%Y-%m-%d %H:%M:%S' paraméter szerint az időt úgy formázzuk, hogy az ehhez hasonlóan jelenjen meg: 2016-09-25 11:56:12.

Most nem mennék bele, hogy melyik hieroglifa mit jelent, de az strftime() leírásában megtalálható minden.

A print()-ben még beállítjuk az end='\r' kwarggal, hogy a sor végét lezáró karakter legyen a carriage return (erről itt lehet olvasni). Így tehát kiíródik a dátum és idő, majd a kurzor visszakerül a sor elejére, és a következő kiírás felülírja a meglevőt. Így az óra egy helyben marad.

Az utolsó sorban a time.sleep(1)-el 1 másodpercig áll a program. Itt tetszőleges törtet is megadhatunk (így kevesebb időt is várakozhatunk).

Megoldás másik modullal

A datetime modul is dátum és idő funkciókat tartalmaz. Ebben a modulban van egy olyan függvény, ami egyből szépen formázva adja vissza az időt. Próbáljuk ki ezt:

import datetime
import time

while True:
    print(datetime.datetime.now(), end='\r')
    time.sleep(1)

Importáltuk a datetime modutl, és a print()-be beírtuk a datetime.datetime.now() hívást. Ha futtatjuk a programot akkor egy kicsit pontosabb időt látunk, mert látszódnak a milliszekundumok, de ez most elhanyagolható. Valami ilyesmi jelenik meg: 2016-09-25 12:05:49.251479

Ez a fenti egy érdekes példa, amit nem is említettem külön: a modulok tartalmazhatnak modulokat, ezzel nincs semmi gond, csupán még egy .-ot kell kifejezésbe tenni.

Az is látszik a példában, hogy 2 km kódot kell írni a pontos időhöz. Írjuk át így a kódot:

import time
from datetime import datetime

while True:
    print(datetime.now(), end='\r')
    time.sleep(1)

A datetime csomag tartalmazza az azonos nevű datetime modult, de ott vannak még például a time, date vagy timedelta modulok is a csomagban.

Még két példa

A fenti példákból úgy tűnhet mintha csak függvények lennének elérhetőek a modulokból, de ez nem így van. Például ha a pi értéke elég pontosan elérhető a math modulból:

import math
print(math.pi)

Látszik, ez csak egy egyszerű tört.

Egy kicsit szeméylesbb példával zárnám ezt a blokkot. Anno azért kezdtem el komolyabban programozni, hogy tudjak írni egy olyan programot, amivel leelőzhetem a barátaimat egy hülye Facebookos játékban. Ehhez az kellett hogy a programom tudjon kommunikálni a távoli szerverrel, szóval hogy elérhessem az internetet. Az urllib segítségével ez megoldható.

Ez a kis script letölt egy weboldalt, ami az IP-címünket tartalmazza:

import urllib.request

response = urllib.request.urlopen('https://httpbin.org/ip')
print(response.read())

A kimeneten valami ehhez hasonló jelenik meg: b'{\n "origin": "10.0.0.1"\n}\n'. Ez amúgy ránézésre majdnem olyan mintha Python kód lenne, de valójából JSON objektumot látunk. A json modullal átalakíthatjuk egy dict-é ezt az eredményt:

import json
import urllib.request

response = urllib.request.urlopen('https://httpbin.org/ip')
my_ip = json.loads(response.read().decode())['origin']

print(my_ip)

Nem minden szerver válaszol ilyen szépen formázva, de a példa kedvéért kerestem egyet.

Honnan tudom hogy mi van a modulban?

Kérdés hogy mégis honnan a picsából kéne tudnom, hogy mi-merre van? Sehonnan. Senki se tudja magától, senki se tudja mit-hogy szoktak a programozók elnevezni. Így válik nyilvánvalóvá, hogy nem is magát a Pythont (szintaxist, kifejezéseket) tart sokáig megtanulni, hanem azt, hogy melyik csomagban és modulban mi van; azt hogy hívják, és hogyan lehet elérni. Általában elmondható, hogy a csomagok ismeretének elsajátítása tart a legtovább a programozói karrier építése során.

A tanulás során érdemes mindig kéznél tartani a Python Standard Library oldalát, illetve olyan fejlesztői környezetet (IDE) beszerezni, ami ki tudja egészíteni a parancsainkat, így nem kell mindenre pontosan emlékezni.

Igazából a Python nyelvtől teljesen független, hogy az ember milyen fejlesztői környezetet használ, ezért nem is foglalkoztam ezzel a kérdéssel eddig, és most se szánnék rá időt. Rövidre fogva, én a JetBrains PyCharm szoftverét használom évek óta.

Hogyan kell modult írni?

Azt hittem már hogy sose érkezünk meg ehhez a ponthoz. Modult szerencsére pofon egyszerű írni: másoljuk össze a kódjainkat egy különálló .py fájlba, és a fájl neve lesz a modul. Ilyen egyszerű.

Korábban a számkitatlálós példában írtam az int(input()) kifejezést, ezzel alakítottam át a játékos által beírt szöveget int-é. Ezt a kis kódocskát átalakítjuk egy függvénnyé, és kirakjuk a typed_inputs.py nevű fájlba.

# typed_inputs.py

def int_input(prompt):
    return int(input(prompt))

Mondjuk azt, hogy a typed_inputs modulban olyan szöveg beolvasó függvényeket gyűjtünk, amik megfelelő típusúvá alakítják a felhasználótól érkező szöveget. Így az int_input()-tól azt várjuk, hogy egy int-el tér vissza.

Ez után egy másik fájlból, amit most hívjunk program.py-nak, importáljuk a modult így:

# program.py

import typed_inputs

number = typed_inputs.int_input('Enter a number: ')

És kész is: az int-es inputunk mostantól független életet élhet.

Hogyan kell csomagot írni?

A csomag sem egy nagy mutatvány, ezért is nem írtam róla sokat idáig. Annyit kell róluk tudni, hogy több modult fognak össze. Roppant könnyű létrehozni egy sajátot: tegyük a moduljainkat egy könyvtárba, és ebben a könyvtárban készítsünk egy __init__.py elnevezésű fájlt is. A könyvtár neve lesz a csomag neve.

Véleményem szerint csak azért nem szükséges csomagot készíteni hogy legyen olyan is. Akkor érdemes csomagot írni ha van több különböző modulunk amik logikailag jól összetartoznak. A modulok nagyon jól elvannak magukban is a hétköznapokban.

Tegyük fel hogy mindenféle típus-specifikus modulokat írogattunk, úgyhogy ezeket belerakjuk a typed nevű csomagba. A következő könyvtárstruktúrát alakítsuk ki:

program.py
typed/
    __init__.py
    inputs.py

“Magyarul”: legyen egy program.py fájlunk és mellette egy typed nevű könyvtár. Ebben a könyvtárban legyen egy __init__.py és egy inputs.py nevű fájl.

Ezért a példáért nem akartam új forráskódot kitalálni, szóval az inputs.py tartalma egyezzen meg a feljebb bemutatott typed_inputs.py tartalmával. Ha ez megvan, akkor a program.py így módosul:

# program.py

from typed import inputs

number = inputs.int_input('Enter a number: ')

A lényeg a 3. sorban látható: a typed csomagból importáljuk az inputs modult. Ez a forma pont úgy néz ki, mint amikor egy modulból egy objektumot importáltunk. Akkor most hogyan tudjuk megkülönböztetni hogy csomagból vagy modulból történt az importálás? Szokás szerint nem tudjuk. De igazából érdekel ez akárkit is? Nem.

Egyébként ha már itt tartunk, még mindig lehetséges a typed csomag inputs moduljának egyetlen objektumát importálni ha ezt írjuk:

from typed.inputs import int_input

Viszont meglepő lehet, de ez nem fog működni:

# program.py

import typed

number = typed.inputs.int_input('Enter a number: ')

Mire való az __init__.py?

A csomagban helyet foglaló __init__.py, mint ahogy egyébként bármi más is, nem dísznek került oda a könyvtárba. Persze állhat üresen is, a csomag akkor is működni fog, de írhatunk ide mindenféle inicializáló kódokat. Ez azt jelenti, hogy amikor a csomagból importálunk valamit, akkor az __init__.py fájl tartalma automatikusan le fog futni!

Írjuk be ezt a typed/__init__.py fájlba:

# __init__.py

print('*typed package is in action*')

Írjuk vissza a program.py-t a működő verzióra és futtassuk így. Valami ilyesmi fog megjelenni a konzolban:

*typed package is in action*
Enter a number:

A *typed package is in action* szöveg varázslatosan megjelent abban a pillanatban, hogy az importálásra sorára került a program futása.

Remélem érezhető hogy ide akármilyen kódot írhatunk, ami segíthet például beállítani a csomag saját lelki világában valamit még mielőtt az őt használó program futhatna. Egy konkrét esetet mutatnék: írjuk be ezt az __init__.py-ba:

# __init__.py

from typed import inputs

Így a program.py-ban ez már lehetséges lesz:

# program.py

import typed

number = typed.inputs.int_input('Enter a number: ')

De ennyi trükközés egyenlőre elég lesz. Majd még visszatérünk erre.

A modul névtere

A névterekről volt már szó. Akkor azt mondtam, hogy minden Python fájl 1-1 névtér. Így, hogy már több fájlunk is van, ez végre értelmet is nyert.

Az egyszerűség kedvéért térjünk vissza erre a struktúrára:

program.py
typed_inputs.py

Ahogy a program.py elején megtörténik az importálás a typed_inputs.py tartalma lefut, a saját izolált névterében. Ebből a névtérből egy nevet (függvényt, konstanst, objektumot) az import utasítással tudtunk elérni. Más módszer viszont nincs az átjárásra.

A modulokat nem úgy kell elképelni, hogy azok összeállnak egyetlen nagy szöveggé amikor elindul a program, hanem mindegyik modul önálló és az importálás hatására kelnek életre. Éppen ezért azok a modulok amik soha se voltak importálva nem is futnak le sose; viszont ha fut az importálás, akkor minden sor végrehajtódik mint bármelyik szabványos Python program esetén.

Egyébként az izoláció nem azt jelenti, hogy csak olvashatóak az importált modulok. Tehát lehetséges a futó modulban található objektumokat módosítani, törölni. Ekkor magában a forráskódban nem történik változás, de a futó program máshogy működhet. De erről majd a második rész szól.

Zárás

Most hogy végre tudunk a modulok és csomagok létezéséről én végre színesebb példákat és ti izgalmasabb programokat írhatok. Alig várom hogy végre valami érdekes szoftvert írjunk. Addig is az objektumorientált programozással folytatjuk (már majdnem kész az 1. rész!)

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

-slp

2016. június 26., vasárnap

ffmpeg encoder relay

A nyár során a hétvégéimet általában a Balatonon töltöm, az északi parton a nyaralónkban. Az internetelérés szezonról-szezonra visszatérő probléma szokott lenni, de tavaly óta UPC mobil előfizető vagyok, így jár a korlátlan mobilinternet elérés, 1 Mbps sávszélességgel.

2016-ban teljesen valid kérdés, hogy ekkora sebesség ugyan mégis mire elég (ha elég egyáltalán), de ez a bejegyzés most nem erről fog szólni. Rövidre zárva a gondolatot, 1 Mbps böngészésre jó, és a 480p-s Youtube-os videók is szépen betöltődnek.

Ilyen sebességkorlát mellett az ember azért meggondolja, hogy minek a letöltésével fogja az elkövetkező perceit vagy óráit eltölteni. Pont úgy mint a régi időkben. Ez egyébként még egy megoldható probléma, viszont a mostani hétvégén pont úgy jött ki a lépés, hogy a Defqon 1 élő rádió közvetítését is akartam hallgatni, amit MP3 formátumban streamelnek, 256 kbps-os bitrátával. 1 Mbps-ből (1000 kbps) konstans 256 kbps-ot elvenni azért elég súlyos; ez a sávszélesség negyede. Nyilvánvalóvá vált a feladat: valahogy meg kell oldanom, hogy alacsonyabb bitrátájú streamet hallgathassak. Mivel ilyen csatornát nem üzemeltettek, ezért nekem kellett felállítanom egy olyan átjárót (relayt), ami élőben hatékonyabb tömörítéssel újrakódolja a rádió adást.

Hardver

Mindenképp kell lennie egy szervernek valahol, ami elég erős az folyamatos élő átkonvertáláshoz, illetve ugye a feltöltési sebesség is fontos. Az eredeti stream 256 kbps; ennél jóval kisebb bitrátájú adást akarok sugározni, legfeljebb 128 kbps-osat, szóval ennyi upload kell.

Két helyről tudtam megoldani a feltöltést: a szüleim házában üzemel egy Odroid X2, illetve az albérletemben egy Raspberry Pi 2 látja el a szerver feladatát. Most ez utóbbit választottam a feladatra, mivel úgy 50ms-al kisebb volt a pingem felé. A feltöltés mindkét helyen 10 Mbps, ezzel tehát nincs gond.

Szoftver

Konvertálásokra kérdés nélkül az ffmpeg-et használom, (vagy nagyon ritkán a gstreamer-t). Az ffmpeg több mint egy svájci bicska, amit ez nem tud kezelni, azt valószínűleg más se (mivel hogy sok média lejátszó egyszerűen az ffmpeg-et használja backendként). Az ffmpeg csomag tartalmazza az ffserver programot is, ami majd kiszolgálja az átkonvertált stream-et. Egy fontos kérdés maradt: mivel konvertáljunk?

Encoder

Az MP3 a legnépszerűbb veszteséges audio formátum, de rajta kívül egy csomó modernebb és hatékonyabb tömörítő eljárás létezik még, amik tehát alacsonyabb bitráta mellett is hozzák az MP3 minőségét.

Egyszer szívesen írnék egy nagyobb összehasonlítást a különböző hangkodekekről, mert mindig is imádtam a témát.

Az ffmpeg wikijén találtam egy sorrendet, hogy melyik formátum mennyire hatékony a többihez képest. A libfdk_aac-ot már többször használtam is, és nagyon jó tapasztalataim voltak vele. A libvorbis-al is volt már tapasztalatom, de a libopus új volt nekem, úgyhogy végeztem egy gyors összehasonlítást köztük. Ehhez egy WAV-ban rögzített zenét konvertáltam át a két encoder segítségével, különböző bitrátákkal kombinálva.

A gyors teszt végeredménye alapján a libopus encodert választottam, 64 kbps-os bitrátával.

Forrás minősége

64 kbps-on még a fenti kodekek is érezhetően gyengébb minőséget produkálnak, de az azonos tulajdonságú MP3-hoz képest összehasonlíthatatlanul több marad meg a zenéből, tehát érdemes a konvertálás. A forrás, ami élő közvetítés a fesztiválról, valószínűleg nem a DJ keverőjének outputja, vagy ha mégis akkor elég erős kompresszor van rajta, de ami fontosabb hogy hallható a közönség hangja is. Magyarul tehát a forrás kicsit torz, tele fütyüléssel és ordibálással. A konvertálás során elveszett részletek tehát majd hogy még javítanak is a hang minőségén.

libfdk_aac

Ez az encoder nem része az ffmpeg csomagjának, mivel a licence nem kompatibilis az ffmpeg-ével. Ez rossz pont, de szerencsére mindig saját magam fordítom az ffmpeg-et forrásból (e leírás szerint), és mindig belefordítom ezt a kodeket is, ezért is tudtam elvégezni az összehasonlítást.

libopus

Az Opus egy teljesen szabad formátum, aminek amúgy igen széles a támogatottsága, így akár a telefonomon is tudom hallgatni, még a böngészőből is.

Setup

Na jó, megérkeztünk végre az izgalmas részhez. Az ffserver beállításait egy config fájlba kell írni, ami nagyjából így néz ki:

HTTPPort 8080
HTTPBindAddress 0.0.0.0

MaxHTTPConnections 2
MaxClients 2
MaxBandwidth 1024

<Feed bestofdq1.ffm>
    ACL allow 127.0.0.1
    FileMaxSize 512K
    Launch ffmpeg -i http://audio.true.nl/BESTOFDQ1
</Feed>

<Stream bestofdq1.ogg>
    Feed bestofdq1.ffm

    Format ogg
    AudioCodec libopus
    AudioBitRate 64
    AudioChannels 2
    AudioSampleRate 48000
    AVOptionAudio flags +global_header

    NoVideo
</Stream>

<Stream status.html>
    Format status
</Stream>

Az első két sor írja le hogy a szerver milyen porton és milyen címről fogad kapcsol. A 8080-as portot tehát ki kell nyitni ha router mögött vagyunk, a 0.0.0.0 pedig azt jelenti, hogy bárki kapcsolódhat a szerverre. Az ezt követő három sor a kapcsolatok számát és a maximális kimenő sávszélességet jelöli. A MaxBandwidth mértékegysége kbps.

Ez után a <Feed> jelöli azt a végpontot, ahonnan az ffserver adatot vár majd. Az ffmpeg majd a konvertált adatot ide fogja küldeni. Az ACL sor azt jelöli, hogy milyen címről fogadhat kapcsolatot az ffserver. A példban ez a localhost. A FileMaxSize a szerver buffer mérete. Én nem akartam sokáig bufferelni, ezért ez kellően kicsi érték lett. A Launch mellé kerül az a parancs, amit az ffserver elkezd futtatni amikor a feed életre kel. Itt nagyon egyszerűen csak az ffmpeg-et és az input elérési útvonalát kell megadni. A feed címét az ffserver automatikusan a parancs végére helyezi majd.

A <Stream> szekcióba kerül a stream neve, alá pedig a kódolás tulajdonságai. Tételesen:

  • Feed bestofdq1.ffm: A stream ebből a forrásból fog táplálkozni
  • Format ogg: ogg konténer formátumba kerül beágyazásra az opus stream
  • AudioCodec libopus: Kodeknek a libopus-t használjuk
  • AudioBitRate 64: A bitráta 64 kbps
  • AudioChannels 2: A hang kétcsatornás
  • AudioSampleRate 48000: A mintavételezési frekvencia 48 kHz
  • AVOptionAudio flags +global_header: Header információk bekerülnek a streambe
  • NoVideo: Nincs video stream

Egy <Feed>-hez több <Stream> is tartozhat, azaz ugyan azt a forrást több különböző módszerrel is átalakíthatjuk, és mind egy-egy stream lesz majd.

A fájl végén, a <Stream status.html> egy HTML kiterjesztésű stream-et definiál. Ezen belül a Format status egy olyan formátum, ami a szerver állapotáról szóló weboldalt állít elő.

Futtatás

Most már nincs sok hátra. Mondjuk hogy a fenti fájlt config.conf-nak nevezzük el, akkor így tudjuk elindítani a szervert:

ffserver -f config.conf

Ha minden rendbe van, akkor igazából a verzió információkon kívül semmit sem mutat az ffserver. Nyissuk meg 127.0.0.1:8080/status.html oldalt a böngészőben. Itt látszódni fognak a streamek, ki lehet másolni a címet, és mehet is a foobar-ba, vagy szólhat a böngészőből és kész is.

Zárás

Az eredeti stream mellett 744 kbps maradna böngészésre, így viszont 936 marad, ami 25.8%-al több. Teljesen megérte.

-slp

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

2016. április 11., hétfő

08 - Hibakezelés

Hibák kezelése

Eddig tökéletes, hibátlan szoftvereket fejlesztettünk, mert ezek egyszerű kis kódok voltak, amik általában nem is függtek olyan adattól amit a felhasználótól kaphattunk. Ezért nem is nagyon volt minek elromlania.

A valódi életben viszont a valami mindig el fog baszódni. Jobb esetben a program futtatása előtt, rosszabb esetben (és általában) futás közben.

Háromféle hiba keletkezhet a programban: szintaktikai, futásidejű és szemantikai (logikai).

Szintaktikai hiba az, amikor a forráskódunk hibás: nem megfelelő a behúzás, elmarad a zárójel, vessző, és ehhez hasonló dolgok, amik megakadályozzák a Pythont hogy elolvassa a forráskódot. Az ilyen hibák nem engedik hogy elinduljon a program, viszont elég jól behatárolhatóak, mert a Python meg tudja mondani, hogy melyik volt az a sor és karakter, ahol valami szokatlan/nem várt parancs állt.

A futásidejű hiba az, amikor a programunk helyesen fut, de aztán egyszer csak valami nem várt esettel találkozik a program. Rengeteg ilyen hiba keletkezhet akkor, ha a felhasználótól várunk adatra. Például felszólítjuk az usert, hogy gépeljen be egy dátumot, de ő nem olyan formátumban teszi ezt meg amire az algoritmust írtunk. Ekkor rövid időn belül hibát jelez a Python, mert mondjuk a begépelt adatban nem annyi számjegy van mint amire számítottunk; kötőjel helyett szóköz van és ezért nem tudjuk a dátumot komponensekre szétbontani. Remélem érezhető: igen nehéz minden esetre felkészülni, de a lényeg, hogy a nem várt adat előbb-utóbb mindenképp megakasztja az algoritmust, amit a Python jó vastag hibaüzenettel ad majd a tudtunkra.

A szemantikai hiba a legtrükkösebb. Ilyenkor a forráskód helyes, viszont a program “nem azt csinálja amit akarunk” (természetesen a program mindig azt és csak is azt csinálja amit a forráskódban leírtunk). Ezeket a hibákat csak akkor tudjuk kijavítani, ha átolvassuk és újra értelmezzük az algoritmusunkat, esetleg teljesen újat írunk ugyan annak a problémának a megoldására. A lényeg, hogy szemantikai hiba miatt nem kapunk majd hibaüzenetet, ezért nagyon nehezen detektálhatóak az ilyen hibák, e miatt igen kártékonyak tudnak lenni (mert csak akkor vesszük észre hogy valami rosszul működik, mire az látható kárt okozott).

A hibákat a hétköznapokban bug-nak szokták hívni.

Szintaktikai hibák

A szintaktikai hibák száma annál kevesebb minél többet programoz az ember, ezért ezekkel csak egy példa erejéig foglalkozunk. Vegyük az alábbi hibás kódot:

print(Hello world)

A probléma hogy a Hello World nem string, mivel nincs idézőjelek közé téve. Ha futtatjuk a programot, akkor valami ilyesmit kapunk a konzolba:

  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 1
    print(Hello world)
                    ^
SyntaxError: invalid syntax

Pythonban ezt a szöveget traceback-nek nevezik. A következő fejezetben részletesen át is nézzük a részeit. Egyelőre viszont elég az utolsó sort olvasni, mert többnyire itt van a lényeg. Ez az esetünkben a SyntaxError: invalid syntax szöveg. Igen csekély angol tudással is látható, hogy a hibát a helytelen szintaxis okozza. Soronként felfelé haladva látszódik az a részlete a forráskódnak, ahol a hiba érzékelhetővé vált, majd legfelül az is, hogy melyik fájl melyik sorában történt a hiba. Így már csak oda kell menni és ki kell javítani.

Szintaktikai hiba keletkezése

A fenti tracebackben a ^ jellel jelölték, hogy a forráskódban hol keletkezett szintaktikai hiba. Ez a jel a world szó végére mutat. Ez nem véletlenül van így: a Hello szónál nem beszélünk még hibáról, hisz a Hello idézőjelek nélkül akár értelmezhető lehetne változóként is. Viszont a függvény argumentumai között vesszőnek kell állnia, ami hiányzik. A forráskód értelmezése eljut függvény hívásának végéig, így biztossá válik, hogy már nem is lesz vessző az argumentumok között, így tehát a world szó az, amire nem számít a Python, és létrejön a SyntaxError.

A mesének az a lényege, hogy a Python mindig azt a pontot mutatja meg, ahol már biztos hogy hibás a szintaxis. Előfordulhat, hogy ez sorokkal később válik csak nyilvánvalóvá:

print(int('10')

Itt lemaradt a print() bezáró zárójele. A következő tracebacket kapjuk:

  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 2

                   ^
SyntaxError: unexpected EOF while parsing

Bizony, ez a nagy büdös semmire mutat. Az unexpected EOF while parsing üzenet azt jelenti, hogy korábban lett vége a fájlnak mint amire a Python számított (Az EOF az End Of File kifejezés rövidítése).

Ez azért történhetett, mert a Python a print() lezáró zárójelét keresete, de közben véget ért a fájl, így az már sose lesz meg.

Szemantikai hibák

A szemantikai (logikai) hibák úgy kerülhetőek el a legkönnyebben, ha nem írunk rossz kódot. Na persze, ezt aztán baromi könnyű mondani, de megcsinálni azért már nem annyira.

Gondos tervezéssel és megfelelő teszteléssel kellően jól megbizonyosodhatunk róla hogy az algoritmusunk jól működik. A forráskód tesztelésére is kínál eszközöket a Python, de egyelőre nem foglalkozunk ezeknek a használatával, mert arról megint egy jó vastag bejegyzésre való szöveget össze lehetne hordani.

Egyelőre tehát igyekezzünk jó algoritmusokat írni, és keressük a hibákat végig a fejlesztés során. Azaz készüljön el a program egy kisebb része, próbáljuk ki hogy megfelelően működik-e, majd folytassuk az építkezést. Így még a program alapjainál kiderülhet ha valami rosszul működik.

Ha beütött a baj, és látszólag minden megfelelőnek tűnik, akkor jöhetnek az úgynevezett debugger programok. Pythonban ilyenkor a program futása minden soron megáll, és megvizsgálhatjuk a memória tartalmát, láthatjuk hogy a program egy elágazásban milyen irányba fog haladni, és egyéb érdekes dolgok.

Szemantikai hibák keletkezése

Vegyünk egy tipikus logikai hibát, amitől a programunk végtelen ciklusba fog kerülni:

i = 0
while i != 10:
    print(i)

A probléma az, hogy elfelejtettük növeli az i változó értékét, így sosem fog a program kilépni a ciklusból.

A fenti példa még elég triviális, a következő viszont már nem biztos:

i = 0
while i != 10:
    print(i)
    i += 0.1

Ebben a kis kódban 0.1-esével növeljük az i változót, amíg az el nem éri a 10-et. Valamiért azonban mégis átugorja a Python a feltételt és elkezd számolni mint a barom felfelé.

Ha sikerül elkapni az első pár számot ami a konzolba kerül, akkor talán beugrik valami:

0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999

A float típusnál említettem, hogy a lebegőpontos számok pontossága nem végtelen. Pontosan e miatt történt a baj is: összeadódnak a törtek hibái, és ezért az i változóban levő tört sose lesz majd pontosan 10.

Futásidejű hibák

Igazából az egész bejegyzésnek a fő témája a futásidejű hibák kezelése.

A programozás során kétféleképp győződhetünk meg róla, hogy nem okoz-e hibát az algoritmusunk: először megnézzük hogy szabad-e elvégezni a műveletet és utána végrehajtjuk, vagy először végrehajtjuk és utólag kezeljük a hibát ha keletkezett.

Az előbbi módszert LBYL-nek (Look before you leap (Először gondolkodj, aztán cselekedj)), az utóbbit EAFP-nek (Easier to ask for forgiveness than permission (Könnyebb bocsánatot kérni mint engedélyt)) nevezik.

A Python nyelvben mindkét filozófia megvalósítható, de a nyelv készítői az utóbbi megoldást javasolják.

Személy szerint eléggé keverten használom a két módszert.

Futásidejű hiba keletkezése

Vegyük a korábban felvázolt problémát: kérjük be a felhasználótól a mai dátumot a következő formátumban: ÉÉÉÉ-HH-NN (év, hónap, nap, számokkal, kötőjellel elválasztva), és írjuk vissza a konzolba a magyar helyesírásnak megfelelően.

months = {
    '01': 'január',
    '02': 'február',
    '04': 'március',
    '05': 'április',
    '06': 'június',
    '07': 'július',
    '08': 'augusztus',
    '09': 'szeptember',
    '10': 'október',
    '11': 'november',
    '12': 'december'
}

today = input('A mai dátum: ')

parts = today.split('-')
year, month, day = parts
month_text = months[month]

print('{0}. {1} {2}.'.format(year, month_text, day))

Ahhoz hogy a számjegyekkel felírt hónapot magyar szavakra átalakítsuk felsoroltam az összes hónapot a months dict-ben. Arra számítok hogy ha az aktuális hónap egy számjegyű akkor a bal oldalára nullát írnak majd. A kulcsok azért str-ek és nem int-ek, mert így megspóroltam a begépelt hónap int-é alakítását.

Akkor futtassuk a programot és íruk be pl hogy 2016-04-10:

A mai dátum: 2016-04-10
2016. március 10.

Ez bizony elég szépen ment. Akkor most gépeljük be a dátumot de kötőjelek helyett pontokkal, így: 2016.04.10.

A mai dátum: 2016.04.10.
Traceback (most recent call last):
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 18, in <module>
    year, month, day = parts
ValueError: not enough values to unpack (expected 3, got 1)

Meg is érkeztünk, ez bizony elég szépen meghalt. Kezdjük a tűzoltást a hibaüzenet értelmezésével.

A traceback értelmezése

Szerintem a Python nagyon jó és informatív üzenetet ad hiba esetén. Végigkövethető benne a hiba teljes útvonala egész addig amíg az össze nem omlasztotta a programot.

A traceback két részből áll: a legutolsó sora mutatja a hiba típusát, és a hozzá tartozó rövid hibaüzenetet. A fenti példában tehát ez az:

ValueError: not enough values to unpack (expected 3, got 1)

A hiba típusa a kettőspontig tart, ami ebben az esetben a ValueError. Ez nem csak egy egyszerű szó, hanem egy konkrét Python osztály megnevezése, de egyelőre még hívjuk csak típusnak. Több ilyen típus is található beépítve a Pythonban.

A kettőspont után következik maga a hibaüzenet. Ez általában valamilyen angol mondat, nálunk most ez: not enough values to unpack (expected 3, got 1).

Az üzenet azt jelenti, hogy nincs elég elem amit unpack-elni lehetne (3 kéne de csak 1 van). Ezt a problémát mindjárt orvosoljuk, de előbb vegyük a traceback többi részét.

A hibaüzenet feletti sorok ezek voltak:

Traceback (most recent call last):
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 18, in <module>
    year, month, day = parts

Itt látható hogy melyik fájlban ("/home/slapec/.PyCharm2016.1/config/scratches/blog.py"), melyik sorban (18), melyik modulban (<module>) történt a hiba, alatta pedig az a konkrét sora a forráskódnak, ahol a hiba létrejött.

A <module> ebben az esetben azt jelenti, hogy közvetlenül a folyó forráskódban volta a hiba.

Ez egy elég rövid kis traceback volt, mivel a hiba nem valamelyik függvény (vagy más egyéb struktúra) mélyén keletkezett. Írjuk át kicsit a kódot, hogy vastagabb hibaüzenetet is lássunk:

def get_month(month):
    months = {
        '01': 'január',
        '02': 'február',
        '04': 'március',
        '05': 'április',
        '06': 'június',
        '07': 'július',
        '08': 'augusztus',
        '09': 'szeptember',
        '10': 'október',
        '11': 'november',
        '12': 'december'
    }

    return months[month]

def pretty_date(today):
    parts = today.split('-')
    year, month, day = parts
    month_text = get_month(month)

    return '{0}. {1} {2}.'.format(year, month_text, day)


today = input('A mai dátum: ')
print(pretty_date(today))

Annyi változtatás történt, hogy a hónap átalakítása, és a magyar formátumra alakítás egy-egy függvénybe került (get_month() és pretty_date()). Vegyük észre, hogy a pretty_date() hívja majd meg a get_month()-ot.

Most írjuk be a dátumot például így: 2014-4-10.

A mai dátum: 2016-4-10
Traceback (most recent call last):
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 27, in <module>
    print(pretty_date(today))
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 21, in pretty_date
    month_text = get_month(month)
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 16, in get_month
    return months[month]
KeyError: '4'

Ez rögtön érdekesebben néz ki. A hiba típusa most a KeyError. Ilyet akkor kapunk ha nem létező kulcsot próbálunk elérni egy dict-ben. A hibaüzenet egyszerűen annyi hogy '4'. Így össze is áll a kép: a '4' kulcs valahonnan hiányzik. Mindjárt az is meglesz hogy honnan.

Láthatóan 3 sor tartalmaz fájlnevet, de ezek mind ugyan arra a fájlra mutatnak. Általában elmondható, hogy ezeket a sorokat lentről-felfelé érdemes olvasni. Ha így haladunk, akkor legelőször azt a sort látjuk, ahol ténylegesen létrejött a hiba. Ahogy haladunk felfelé, úgy követhetjük hogy a hiba hogyan gyűrűdzik végig a programon.

  • A program a 16. sorban létrejött a hiba a return months[month] utasításnál. Ez a get_month modulban (függvényben) van. Így már tiszta is, hogy a months dict-ből hiányzott a kulcs.
  • Ide úgy jutottunk, hogy a 21. sorban a month_text = get_month(mont)-nél meghívtuk a hibázó függvényt. Ez a pretty_date modulban (függvényben) történt.
  • A 21. sorba pedig a 27. sorból ugrottunk. Ez a <module> modulban van, tehát minden függvényen kívül.

Jól látható, hogy maga a hiba igen mélyen történt, az mégis egészen addig halad felfelé a programban, amíg a program ki nem lép.

A hiba kezelése az a művelet lesz, amikor ezt a hibát az útja során valahol elkapjuk, hogy ne omlassza össze a programot.

A hiba keletkezését hétköznapian úgy mondjuk hogy az algoritmus hibát dob. Angolul, a Pythonos elnevezéseknek megfelelően ezt úgy mondják, hogy raise.
Az egyértelmű hogy a raise nem azt jelenti hogy dobni. Más programozási nyelvekben a raise helyett a throw kifejezést használják és ennek a szónak a magyar változata épült be a beszédbe.
Hasonlóan, a hibát elkapni fogjuk. Ennek a Pythonos elnevezése az except lesz, ami szintén köszönő viszonyban sincs az elkapni kifejezéssel. Ugyan úgy, az eltérő programozási nyelvekben használt catch kifejezés vált elfogadottá a beszédben.

A hiba kezelése

Azon a ponton ahol létrejött a hiba véget ér a kód futása, és egy hiba objektum (exception) indul felfelé a programban. Ilyenkor különleges állapotba kerül a Python: ha nem kapjuk el a hibát, akkor a hibakezelő algoritmusok kivételével semmilyen kód sem fut tovább, csak az objektum halad felfelé. Ha sehol se kaptuk el a hibát, akkor végül mindenképp kilép a program (így látszódik majd minden sor a traceback-ben amin végighaladt a hiba).

Az algoritmusunkat (illetve annak tetszőleges részét) úgynevezett try blokkba rakva jelezhetjük a Pythonnak, hogy a blokkban előfordulhat hiba. Ha a blokkban hiba történik, akkor a futás automatikusan a hibakezelő ágakra kerül. Itt típusonként különböző módon foglalkozhatunk a hibával. Ha nem volt olyan ág, ahol a hibát lekezeltük volna, akkor a hiba halad tovább felfelé.

A korábbi traceback-ben három sor is látható volt, így jól megbecsülhető, hogy három helyen kaphattuk volna el a hibát. Az egyszerűség kedvéért most a program végén tesszük ezt meg. Lássuk az új struktúrát!

def get_month(month):
    months = {
        '01': 'január',
        '02': 'február',
        '04': 'március',
        '05': 'április',
        '06': 'június',
        '07': 'július',
        '08': 'augusztus',
        '09': 'szeptember',
        '10': 'október',
        '11': 'november',
        '12': 'december'
    }

    return months[month]

def pretty_date(today):
    parts = today.split('-')
    year, month, day = parts
    month_text = get_month(month)

    return '{0}. {1} {2}.'.format(year, month_text, day)


today = input('A mai dátum: ')

try:
    print(pretty_date(today))
except KeyError:
    print('Hibás hónap!')

Semmi sem változott, az utolsó 4 sor kivételével:

  1. A try-al kezdtük az új blokkot.
  2. Hiba esetén az except sorra ugrik a program. Az utasítás mellé a hiba típusa kerül, aminek bekövetkeztében a program futásának itt kell folytatódnia. Látható, hogy a KeyError nem string! Másik fontos dolog, hogy ide nem lehet feltételeket tenni, mint az if esetében.
  3. Az except ágba kerül az a kód, ami KeyError hiba esetén lefut majd.

Futtassuk így a programot, és írjuk be hogy: 2016-4-10. Így már a Hibás hónap! felirat jelenik meg a konzolban. Ez egyből sokkal barátságosabb.

Most próbáljuk meg a legelső hibás példa szerint begépelni a dátumot, azaz pontokkal elválasztva. Ekkor még mindig hibaüzenet kapunk, mivel ez nem KeyError típusú hiba volt.

A try-hoz tetszőleges számú except adható, így kezelhetjük a ValueError-t is:

try:
    print(pretty_date(today))
except KeyError:
    print('Hibás hónap!')
except ValueError:
    print('Hibás formátum!')

Innen már egyszerű: ha a try-ban KeyError történt akkor az az alatti ágban; ha ValueError keletkezett akkor meg annak az ágába folytatódik a program.

Lehetséges egy ágban lekezelni több különböző típusú hibát is a típusok felsorolásával:

try:
    print(pretty_date(today))
except (KeyError, ValueError):
    print('Hibás dátum!')

Bár úgy nézhet ki a zárójel miatt mintha az except egy függvény lenne, de az továbbra is utasítás mard. Az extra szóköz nyomatékosítja ezt.

A fenti kóddal általánosságban elmondhatjuk, hogy ha a pretty_date() akár KeyError akár ValueError hibát dob, akkor hibás volt a dátum amit begépelt a felhasználó.

Az else ág

Írjuk át a programot így:

try:
   text = pretty_date(today)
except (KeyError, ValueError):
    print('Hibás dátum!')

print('A dátum:', text)

A szándék az volt, hogy a try-ban csak a dátum konvertálását végezzük el. Ha ez sikeres volt, akkor a kiírás is biztos menni fog, így annak felesleges try-ban maradnia.

Ha helyes dátumot írunk be, akkor minden a számításaink szerint zajlik majd. De ha hibásan:

Hibás dátum!
Traceback (most recent call last):
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 33, in <module>
    print('A dátum:', text)
NameError: name 'text' is not defined

Akkor csak egy jóféle NameError exceptiont kapunk csak. Ez azért történt, mert a text változót a try ágban deklaráltuk. A hiba miatt viszont a deklaráció nem történt meg, így a try-on kívül nem létező változót próbáltunk meg kiírni.

Ezt többféleképp is elkerülhetjük, de talán a legjobb megoldás az, ha az else ágát használjuk:

try:
   text = pretty_date(today)
except (KeyError, ValueError):
    print('Hibás dátum!')
else:
    print('A dátum:', text)

Az else igazából az except-el van kapcsolatban: KeyError vagy ValueError esetén fusson az except ág, különben fusson az else ág. Tehát ebben az ágban van az a kód, amivel folytatni kell a programot ha nem volt hiba.

A finally ág

Az utolsó ág ami a try-hoz tartozik a finally. Az ebben álló kód mindenképp lefut amikor a program futása elhagyja a try blokkot, akár volt hiba, akár nem.

Ide általában cleanup kódot szoktak tenni, például bezárják a nyitott fájlokat, vagy kiírnak valami üzenetet mielőtt elhalálozik a program.

Mivel még nem tudjuk hogy hogyan kell fájlba írni, ezért innen most hiányzik a példa.

A nagyon nem várt hiba

A fenti algoritmust végülis egész jól körbebástyáztuk. A komplex programokban könnyen előfordul, hogy bár egy csomó esetre felkészültünk, valamiért mégis olyan hibát kapunk, amire aztán egyáltalán nem számítottunk.

A beépített hiba típusok hierarchikus elrendezésűek, azaz a hierarchia alján álló típusokra minden igaz ami a felettük álló típusokra (öröklik azok tulajdonságait). Ennek a hierarchiának a tetején az Exception típus áll. Azaz ha ezt a típusú hibát kapjuk el, akkor tényleg minden hibát le tudunk kezelni:

try:
    print(pretty_date(today))
except Exception:
    print('Hibás dátum!')

Opcionálisan így rövidíthető a fenti kód:

try:
    print(pretty_date(today))
except:
    print('Hibás dátum!')

Egyes esetekben elvárt, hogy a program semmilyen körülmények között se álljon le, viszont az ilyen hibakezeléssel azt kockáztatjuk, hogy az amúgy igen súlyos fennakadásokról sose kapunk majd tájékoztatást.

Saját hiba készítése

Nem csak a Pythontól várhatunk hibákat, hanem saját magunk is eldobhatunk exception-öket amiket valahol máshol elkaphatunk. Ez többek között függvényekben jöhet nagyon jól: így hibával jelezhetjük, ha nem megfelelő értékeket kaptunk.

Írjunk egy nagyon egyszerű kis függvényt, ami kizárólag pozitív számokat adhat össze:

def add(a, b):
    if a >= 0 and b >= 0:
        return a + b
    else:
        raise ValueError('A paramétereknek 0-nak vagy annál nagyobbnak kell lenniük')

Az 5. sorban látható az újdonság, a raise. Ezzel az utasítással egy exception objektumot dobhatunk el, magyarul így tudunk hibát létrehozni. Itt ugyan azt a ValueError-t használtam, mint amit korábban a hibás formátumú dátumnál elkaptunk.

A ValueError-t majdnem úgy kell használni mint egy függvényt, csak ez igazából egy osztály. Régóta kerülgetjük már az osztály fogalmát, pár rész és végre tényleg megdumáljuk hogy mik is azok valójában.

Ha megfelelő, tehát 0 vagy annál nagyobb argumentummal hívjuk meg a függvényt akkor minden rendben lesz. Ha valamelyik argumentum viszont negatív, például így:

add(-5, 10)

Akkor ezt a csinos hibaüzenetet kapjuk:

Traceback (most recent call last):
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 8, in <module>
    add(-5, 10)
  File "/home/slapec/.PyCharm2016.1/config/scratches/blog.py", line 5, in add
    raise ValueError('A paramétereknek 0-nak vagy annál nagyobbnak kell lenniük')
ValueError: A paramétereknek 0-nak vagy annál nagyobbnak kell lenniük

Őszintén megmondom, igen csinos ez a kis üzenet. Innentől már csak rajtunk áll, hogy hol-milyen hibákat akarunk eldobálni.

Természetesen nem csak a ValueError használható; itt van a korábban látott KeyError is, és még egy csomó más típust tartalmaz beépítve a Python, de a legjobb hír az, hogy később majd mi is fogunk teljesen saját hiba típusokat létrehozni.

Az üres utasítás

Levezetésnek következzen egy új utasítás, amiről idáig nem volt szó: a pass.

Pythonban nem hagyhatunk üresen blokkokat, de van amikor mégis erre lenne szükség: például hiba esetén egyszerűen ne csináljunk semmit. Ekkor használható a pass utasítás. Ez az utasítás a szó szoros értelmében nem csinál semmit, csak járattatja a processzort. Tölteléknek használható.

Vegyük elő újra a dátum beolvasós példát. Írjuk át úgy, hogy ha hiba történik, akkor szépen csöndben lépjünk ki minden felhajtás nélkül:

def get_month(month):
    months = {
        '01': 'január',
        '02': 'február',
        '04': 'március',
        '05': 'április',
        '06': 'június',
        '07': 'július',
        '08': 'augusztus',
        '09': 'szeptember',
        '10': 'október',
        '11': 'november',
        '12': 'december'
    }

    return months[month]

def pretty_date(today):
    parts = today.split('-')
    year, month, day = parts
    month_text = get_month(month)

    return '{0}. {1} {2}.'.format(year, month_text, day)


today = input('A mai dátum: ')

try:
    print(pretty_date(today))
except (KeyError, ValueError):
    pass

A legutolsó sorban van a lényeg. A pass akárhol használható egyébként (függvényben, ciklusban, stb) de csak akkor van értelme használni, ha önmagában áll, máskülönben akármilyen más utasítás behelyettesíthető a helyére.

Függvényben akkor szokták használni, ha a függvény neve már megvan, de az algoritmus még nincs. Ilyenkor hogy a függvény hívható legyen egy pass utasítást tesznek a törzsébe és később írják meg a kódot a helyére.

Utolsó példa

Írjuk át úgy a fenti kódot, hogy a program próbáljon meg újra dátumot beolvasni ha hiba történt.

while True:
    today = input('A mai dátum: ')

    try:
        print(pretty_date(today))
        break
    except (KeyError, ValueError):
        print('Hibás dátum!')

A végtelenségig várjuk a begépelt szöveget és majd a 6. sorban ugrunk ki a ciklusból. Az 5. sorban megpróbáljuk átalakítani a dátum átalakítását. Ha ezen a ponton hiba történik akkor a futás az except ágra ugrik. Itt nem állítjuk meg a ciklust, így az újra indul majd.

Ha viszont nem volt hiba, akkor a dátum kiírása után a program 6. sorral folytatódik, ahol a break utasítás kiléptet a ciklusból. Ilyen egyszerű.

Zárás

Azt hittem hogy ez a bejegyzés nem lesz bazi hosszú, és hogy még a modulokra is lesz idő. Ehhez képest már majdnem 20.000 karakternél járok, úgyhogy jobb is itt befejezni ezt a témát.

A következő részben tényleg jönnek a modulok, és remélhetőleg végre fájlba is fogunk írni.

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

-slp