Kihagyás

Projektek, IDE

Szerkesztés alatt!

Az oldal további része szerkesztés alatt áll, a tartalma minden további értesítés nélkül többször, gyakran, jelentősen megváltozhat!

Projekt struktúra

Egy-egy projekt (bármely programozási nyelven) több forrásfájlból áll, illetve ezek mellett előfordulhatnak mindenféle egyéb erőforrások is. Az könnyen látható, hogy a "mindent egy könyvtárba" elv egy bizonyos projektméret felett nem olyan nagyon hatékony szervező elv. Egy projekt fájljait így érdemes alkönyvtárakba szervezni. Hogy hogyan? Nos, ennek több módja van, de egyik sem feltétlenül jobb vagy rosszabb, mint a másik.

Általánosan ökölszabály, hogy a projekt gyökerében legyen egy (általában README.md) fájl, amely a projekt fontosabb jellemzőit, meta-adatait tartalmazza. Ezen belül a klasszikus felosztás az src, test, bin hármas szokott lenni, ahol

  • az src tartalmazza a(z adott programozási nyelvű) forrás- és (mindenféle egyéb szükséges adatokat tartalmazó) erőforrás-fájlokat,
  • a test tartalmazza a projekt tesztjeit, és
  • a bin tartalmazza a kész futtatható (vagy futtatáshoz szükséges) fájlokat.

Java nyelven például ilyenkor az src-ben vannak a .java fájlok, a test-ben a szintén .java fájlokban megírt tesztek, és a bin-ben pedig a .class és .jar fájlok. C/C++ nyelven az src és a test tartalmazza a .c vagy .cpp kiterjesztésű C/C++ nyelven írt forrás és tesztfájlokat, a bin pedig a futtatható programokat (Windows alatt .exe kiterjesztéssel) és az elkészült libeket (.a, .so, .dll). Python nyelven a forrásnyelvű .py fájlokat közvetlenül futtathatónak tekinthetjük (bár ez különféle Python implementációkban lehet, hogy nem teljesen helyénvaló nézet, de ebbe most nem mennénk bele részleteset -- sőt, inkább semennyire), így a bin könyvtárra általában nincs szükség. Emiatt az is előfordul (sőt, Pythonban még ajánlott is), hogy az src könyvtárat elhagyjuk, és a különböző Python package-ek könyvtárai lesznek az első szinten az alkönyvtáraink (plusz egy teszt könyvtár).

De ezek mind csak ajánlások, és akár projektről projektre változhat, hogy hogyan szervezzük a forrásainkat. A lényeg az, hogy értelmesen szervezzük. Pár szervezési elv:

  • Az összetartozó fájlok egy alkönyvtárba (annak további alkönyvtáraiba) kerüljenek.
  • A jól elkülöníthető, önálló elemeket tegyük külön alkönyvtárakba.
  • A teszteket különítsük el a nem teszt jellegű ("production") forrásoktól.
  • Legyen egy README fájl, ami bemutatja a projektet.

Python programozási egységek

Az (bármilyen programozási nyelven megírt program esetén) nagyon ritka, amikor a programunk semmilyen előre megírt, az adott programozási nyelv részét amúgy nem képező elemet nem használ fel. Ezeket az elemeket általában library-kből vesszük. Különböző programozási nyelveken ezeknek a library-knek különböző formái lehetnek. Vannak olyan előre megírt library-k, amiket "csak" felhasználunk, és olyanok is lehetnek, amiket mi magunk írunk.

Python-ban kétféle szinten tudunk újrahasznosítani korábban megírt elemeket: modul és package szinten.

Modul

Python-ban minden egyes forrásfájl önálló modulnak tekinthető, ami más forrásfájlokból importálható. Ha van egy module.py modulunk, annak elemeit az import module utasítás lefutása után a module. előtaggal (scope-pal) kiegészítve használhatjuk.

main.py

1
2
3
4
import module

print(f"{module.g(42, 7)}")
print(f"{module.f(4)}")

A modul első importja során a Python interpreter lefuttatja a modulunk teljes kódját. Ha a modulunk programként íródott meg, akkor tulajdonképpen ez annak a programnak a lefuttatását jelenti. Ezért szokás a Python programokat egy main függvényben megírni, de legalább az if __name__ == "__main__": feltétellel levédeni:

module.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
"""
Ide jönnek a modul elemei.
"""
print("Ez a module.py első (feltétel nélküli) utasítása.")
def f(x):
    return x ** x

def g(a, b):
    return max(a, b) % min(a, b) == 0

def main():
    """
    Ide jön a "főprogram" kódja. 
    """
    print(f"Főprogram. A __name__ változó értéke '{__name__}'.")
    print(f"3^3 = {f(3)}")

if __name__ == "__main__":
    main()

if __name__ == "module":
    print(f"Használat modulként. A __name__ változó értéke '{__name__}'.")

Python-ban a __name__ egy speciális, előre adott értékű változó, melynek értéke fixen "__main__", ha az adott fájlt programként futtattuk, és a modul neve (a példában "module"), ha a fájlt modulként használjuk fel. Ugyanazon modul bármely további importja esetén már nem történik meg a modul kódjának újrafuttatása.

Lehetőség van az import során a modulnak külön nevet adni az import module as alias alakú utasítással; ezután az importált modul elemeire az alias. előtaggal tudunk hivatkozni.

main_as.py

1
2
3
4
import module as m

print(f"{m.g(42, 7)}")
print(f"{m.f(4)}")

Lehetséges továbbá a modulból csak egyes elem(ek)et importálni a from module import item alakú utasításokkal, melyek után az item (de a modulból csak ez) külön scope megadása nélkül használható.

main_from.py

1
2
3
4
5
6
7
from module import g

print(f"{g(42, 7)}")

from module import f

print(f"{f(4)}")
Feladat: programok futtatása

Futtasd le a module.py, main.py, main_as.py és main_from.py programokat. Melyik forrásfájloknak mely részei milyen sorrendben és hányszor futnak le az egyes esetekben?

Csomag

Előfordulhatnak olyan összetett feladatok, melyeknek egyetlen modulban való leírása nem lehetséges (de legalábbis nem észszerű, nem szép). Egy csomag (package) tulajdonképpen összetartozó modulok becsomagolása. Ez fizikailag egy mappát, logikailag egy közös, magasabb szintű scope-ot jelent.

physix/constants.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
"""
Fizikai konstansok
"""
print(f"'{__name__}' modul inicializálása")

e         = 1.602_176_634e-19      # C
G         = 6.674_30e-11           # m^3 kg^-1 s^-2
h         = 6.626_070_15e-34       # J Hz^-1
c         = 2.997_924_58e8         # m s^-1
epsilon_0 = 8.854_187_818_8e-12    # F m^-1
mu_0      = 1.256_637_061_27e-6    # N A^-2
m_e       = 9.109_383_713_9e-31    # kg
alpha     = e**2/(2*epsilon_0*h*c) # = 0.007_297_352_564_3
K_J       = 2*e/h                  # = 4.835_978_484_848_485e14 Hz⋅V^-1
R_inf     = alpha**2*m_e*c/(2*h)   # = 1.097_373_156_815_7e7 m^-1
R_K       = h/e**2                 # = 2.581_280_745_454_545e4 Ω

physix/functions.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
"""
Fizikai képletek
"""
print(f"'{__name__}' modul inicializálása")

def acceleration_of_mass_with_force(m, F):
    return F / m

def force_needed_to_accelerate_mass(a, m):
    return m * a

def mass_a_force_can_accelerate_as(F, a):
    return F / a

A csomagok inicializálása a csomagban lévő __init__.py futtatásával történik. Ennek a feladata, hogy a csomagot használatra előkészítse, a szükséges modulokat (alcsomagokat) importálja.

physix/__init__.py

1
2
3
4
5
6
7
"""
Fizikai csomag inicializálása
"""
print(f"'{__name__}' package inicializálása")

from . import constants
from . import functions

Egy package-et importálni és használni ugyanúgy lehet, mint egy modult, de arra figyelni kell, hogy az almoduljai (vagy alcsomagjai, mert olyat is lehet) egy-egy al-scope-ban vannak!

main_physix.py

1
2
3
4
5
6
import physix

print("physix csomag importálva a főprogramból.")

print(f"c = {physix.constants.c} m/s")
print(f"{physix.functions.acceleration_of_mass_with_force(10, 5)} m/s²")
Feladat: program futtatása

Futtasd le a main_physix.py programokat. Melyik forrásfájloknak mely részei milyen sorrendben és hányszor futnak le az egyes esetekben?

Kérdés: mi történik, ha a physix csomagnak megpróbálod csak az egyik modulját importálni?

Indíts egy interaktív Python interpretert, és importáld a physix csomag constants modulját. Mi történik? Mi fut le, és mi nem fut?


IDE - Integrated Development Environment

Az Integrált Fejlesztői Környezetek nagyon leegyszerűsítve olyan szövegszerkesztők, amelyek a forrásfájlok szimpla szerkesztésén túl mindenféle egyéb funkciókkal is segítik programok, programrendszerek fejlesztését. Néhány jellemző funkció:

  • Syntax highlight (ezt manapság már a legszimplább szövegszerkesztők is tudják)
  • Syntax checking
  • Intelligens kiegészítések
  • Projektkezelés (beleértve akár a fordítási/futtatási környezetek kezelését, verziókezelést)
  • Fordítás felparaméterezése és indítása
  • Futtatás felparaméterezése és a program indítása
  • Debugging funkciók
Az IDE NEM fordítóprogram, és NEM futtatókörnyezet

Nagyon fontos megérteni, hogy a programok fordítását és futtatását NEM a fejlesztői környezetek végzik. Az egyes környezetek tudják, hogy hogyan, mely fordítóprogramok és futtatókörnyezetek segítségével kell adott nyelvű programokat lefordítani, futtatni. Fel tudják például paraméterezni a Java vagy C/C++ fordítóprogramokat, el tudják indítani a lefordított programokat akár natívan, akár valamely interpreter (JVM vagy Python interpreter) segítségével. De ha egy IDE mellett nincs feltelepítve mondjuk egy Python is a gépre (ami sok esetben, főként programnyelvekre dedikált IDE-k estében automatikusan megtörténik), akkor az IDE maximum a syntax checkinget, esetleg intelligens kiegészítéseket fogja tudni, de a Python programunkat önállóan futtatni már nem.

IDE projektek

A legtöbb IDE projektekben gondolkodik, ami azt jelenti, hogy a forrásfájljaink mellett mindenféle meta-információt is eltárol (általában jól elrejtett alkönyvtárakban, fájlokban). Ilyen információ lehet például, hogy a programunk milyen külső library-ket használ, azokat honnan és hogyan lehet beszerezni, hogyan lehet a programot fordítani és lefuttatni, a futtatáskor milyen paramétereket kell megadni a programunknak, van-e verziókövetve a projektünk, stb. Ezeket a paramétereket akár ő maga frissítheti, ahogyan a kódot írjuk (például észreveheti, hogy egy új csomagot importáltunk a kódban), vagy mi magunk is módosíthatjuk.

Feladat: IDE használat

Indítsd el a kedvenc (és elérhető) IDE-det, majd készíts benne egy új üres projektet! Adj a projekthez egy forrásfájlt, ami valami nagyon egyszerű feladatot végez! Futtasd a programot az IDE segítségével!

Feladat: IDE fájlok

Egy terminálban navigálj el a projektkönyvtárba. Milyen fájlokat, alkönyvtárakat hozott neked létre az IDE?

IDE fájlok/könyvtárak

Ha egy projektet verziókövetünk, például Git segítségével, akkor az IDE által a saját maga számára generált fájlokat nem szokás, sőt, nem szabad verziókövetni! Minden fejlesztőnek más és más lehet ugyanis a lokális beállítása, mások lehetnek a preferenciái, a környezete, stb. Az ilyen fájlokat/alkönyvtárakat érdemes a .gitignore segítségével a Git számára "láthatatlanná" tenni.

Egy IDE mind felett (?)

Miért nem ragaszkodunk egy IDE-hez, és miért nem várjuk el meg egységesen annak az IDE-nek a használatát? Egyrészt, mert nincs olyan, hogy legjobb IDE. Sőt, még csak jobb és rosszabb sincs, más van. Ugyanaz az IDE bizonyos esetekben, bizonyos feladatokra alkalmasabb lehet, mint másra, vagy mint egy másik IDE. És van akinek az egyik jön be, van akinek a másik. Meg olyan is van, hogy a cég megmondja, hogy melyikkel kell dolgozni, és akkor hiába specializálódott valaki egy másikra.

Ezzel kapcsolatban ajánlott olvasmány Isaac Asimov-tól az A hivatás (Profession).


Debugging

A debugging az a tevékenység, amikor már tudjuk, hogy a programunk rosszul működik, és próbáljuk megérteni, hogy miért, és próbáljuk benne kijavítani a hibát. A debugging-nak többféle formája lehet.

Printing / Loggolás

A "printing debug" debuggolás mindig működő legegyszerűbb de legprimitívebb formája. Arról van szó, hogy debuggolás közben a programunkat teletűzdeljük mindenféle kiíratásokkal (hol járunk, mik az egyes változók értékei, stb.), és ezek alapján próbáljuk meg megtalálni a hiba okát. Ez a program többszöri futtatását (és ha kell, előtte a fordítását) teszi szükségessé. És természetesen a hiba kijavítása után az összes ilyen kiíratást újból el kell távolítani a programból.

Ennél egy fokkal jobb, ha valamilyen loggert használunk, Python-ban például a logging csomagot. Ezzel többszintű kiíratás megvalósítását tesszük lehetővé, azaz szabályozhatjuk, hogy a program egy futása során nagyon részletes, debugging szintű logokat produkáljon, vagy megelégszünk, ha pusztán a kritikus esetekről informál bennünket. Így ugyanaz a program alkalmas lesz hibakeresésre, de éles működésre is.

A logging használata: logger.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import logging
logger = logging.getLogger(__name__)

def main():
    logging.basicConfig(filename="logger.log", level=logging.INFO)
    logger.debug('Részletes információ a program aktuális állapotáról')
    logger.info('Normál működési információ')
    logger.warning('Figyelmeztetés, valami helyrehozható, javított probléma')
    logger.setLevel(logging.CRITICAL)
    logger.error('Hiba, az adatfeldolgozás hibás, hiányos lehet')
    logger.critical('Kritikus, nem javítható programhiba')

if __name__ == '__main__':
    main()
Teljesítményvesztés veszélye

Bár ha a loggolás szintjét magasra állítjuk, akkor nem lesznek kiírva bizonyos üzenetek; a kiíratandó szöveg összeállítása és egy döntés a kiíratásról/ki nem íratásról akkor is része lesz a program futásának. A túl sok loggoló utasítás tehát akkor is lassítja a programot, ha a loggolási szint aktuális megválasztása miatt ténylegesen nem történik kiíratás.

IDE

A legtöbb ide nyújt valamilyen debuggolási lehetőséget. Ezek általában magukba foglalják a következőket:

  • breakpoints: A forráskódban kijelölhetünk sorokat, függvényeket, utasításokat, és ha az elindított program futása ezen pontok valamelyikéhez ér, akkor a végrehajtást felfüggesztjük, és a debugger segítségével lehetőségünk van ellenőrizni a program állapotát, vagy lépésenként vizsgálni, mi történik.
  • print: A program tetszőleges változójának az értékét kiírathatjuk.
  • watch: Figyelhetjük a program egyes változóit, és ha a program futását felfüggesztjük, ezen változók értékét automatikusan megjeleníti a debugger.
  • continue, resume: A program futása tovább folytatódik, általában a következő breakpoint-ig.
  • restart: Újraindíthatjuk a programot.
  • next, step, step into, step over, step out, return: A program következő utasításának, utasításainak végrehajtása különböző módokon: következő utasítás, függvényhívás esetén belépve a hívott függvénybe vagy átugorva (egyben végrehajtva) azt, futtatás az aktuális függvényből való kilépésig.
Feladat: Debugging IDE-ben

Egészítsd ki a programodat több utasítással és legalább egy függvényhívással! Tegyél egy breakpoint-ot a programod elejére, majd indítsd el! A program felfüggesztése után ellenőrizd le a változók értékeit, majd léptesd a programot és figyeld a változókat, hogyan változnak! A program többszöri újraindítása során nézd meg a step into, step over és step out jellegű léptetések hatását!

*db

Többféle programnyelvhez többféle konzolos debugger program létezik. C/C++-ra az egyik legismertebb a gdb, Java-ra ott van a jdb, Python-ra pedig az egyik megoldás a pdb. Ezen programok jellemzően konzolosak, és parancsok segítségével tudjuk vezérelni a debuggolás, illetve a debuggolt program végrehajtásának folyamatát.

Python-ban a pdb a programban elhelyezett breakpoint() hívások segítségével is indítható. A breakpoint() elérésével a program futása fel lesz függesztve, és a pdb veszi át a vezérlést. A debuggernek a következő parancsokat adhatjuk:

  • p x: Az x kifejezés értékének kiíratása. (print)
  • w: A stack trace, azaz a hívott függvények sorozatának kiíratása.
  • b line, b function: Egy breakpoint az adott sorra vagy függvényre. (breakpoint)
  • s: Következő utasítás végrehajtása, függvénybe belépéssel, ha kell. (step in)
  • n: Következő utasítás végrehajtása, függvényhívások "átugrásával". (step over)
  • r: Következő utasítások végrehajtása a függvényből való kilépésig. (step out)
  • c: Végrehajtás folytatása. (continue)
  • l: Forráskód listázása.
  • a: Függvény argumentumainak kiíratása.
  • display x: Az x kifejezés automatikus kiértékelése és kiíratása minden lépésben. (watch)
  • q: kilépés