Üdvözlünk a Gránit Guru Python/Data oldali dokumentációján!
Áttekintés
A wiki ezen része a Python-os és datás munkafolyamatainkkal kapcsolatos információkat, tippeket, leírásokat tartalmaz.
Python vs. Data
A „Python“ és a „Data“ fogalmakat felcserélhető módon használjuk a projekten, amikor feladatokról vagy fejlesztőkről beszélünk. Ez fogalmilag nem helyes, hiszen nem minden Python feladathoz szükséges Data Science, és nem minden Data Science feladat Python-ban valósul meg. Ugyanakkor jelenleg projekten a Python fejlesztők mind Data Scientist-ek, így nem félrevezető az elnevezések esetleges felcserélése.
-
Rendszer összeállítása
Útmutató a lokális futtatáshoz.
-
Fejlesztői eszközök
Az általunk használt Python fejlesztői eszközök kódminőség ellenőrzésére
-
Kódbázis
A fejlesztés során használt git repók.
-
Új vagy a csapatban?
Itt megtalálsz minden elirányítást!
Mi az a Python-service?
A Gránit Guru elsődlegesen egy olyan termék, ami természetes nyelven kommunikál felhasználókkal. Ez számos olyan megoldást kíván, melyek NLP alapúak. Emellett szintén központi probléma a strukturálatlan adat feldolgozása, ami rengeteg kísérleti megoldást és próbálkozást kíván. Ilyen jellegű feladatokban a Python jobban teljesít mind eszköztár, mind rugalmasság terén.
A Python service koncepcionálisan tehát a Guru azon szubrutinjainak gyűjtőhelye, melyek nem strukturált adatok kezelésével, feldolgozásával, kiértékelésével foglalkoznak. Ez persze nem egy pontos definíció, és nem is ilyen éles a határ a Backend és a Python service funkciói között. Ugyanakkor arra megfelelő, hogy egy távoli képet adjon arra, hogy miért szükséges a rendszer.
Technikai oldalról a Python service egy FastAPI alapú ASGI webapp, melyet az Uvicorn webszerver szolgál ki.
Fejlesztői onboarding
Amennyiben új vagy a projekten, üdv! Itt találsz elirányítást a Python és data oldali információk tömkelegében. Mindenképp olvasd el az általános onboarding szócikket, amennyiben nem onnan jössz. Emellett data szemszögből kiemelten fontos a Virtuális Asszisztens alapok és a RAG koncepcionális ismerete.
Fejlesztőként a legfontosabb lépés a lokális környezet összeállítása és a fejlesztői eszközök ismerete. Emellett datásként sokat fogsz foglalkozni a pipeline egyes elemeinek koncepcionális kidolgozásával vagy módosításával, így a Koncepciók és Modulok szócikkeit legalább átfutni hasznos.
JIRA jegyek
Mint ahogy azt az általános onboarding-nál is megemlítettük, datásként neked első sorban a „Data:“ előtagú JIRA jegyekkel kell foglalkoznod. Előfordul, hogy ezeknek van egy full-stack oldali megfelelője is, hiszen pl. új funkcionalitás kidolgozásánál azt be is kell kötni.
Együtt dolgozás a backend-del
Előfordul, hogy egy Python-os módosításnak van backend-es vonzata is, nevezetesen amikor új funkcionalitás kerül hozzáadásra, vagy egy már létezőnek megváltozik a be- és/vagy kimenete (akár formailag, akár koncepcionálisan). Ilyenkor ügyelj arra, hogy ezt kommunikáld megfelelően a backend-oldali feladat fejlesztőjével, illetve midnenképp módosítsd az api repóban a pythonservice.yml fájlt API változás esetén, hiszen a Backend és a Python service is ebből generálja a megfelelő adatleírókat.
Logolás
Fejlesztés közben ne feledkezz el a logolásról, hiszen innen sokszor reprodukálás nélkül is látszik, hogy hogy viselkedik a rendszer, és milyen váratlan viselkedések történtek (nem mindig akarjuk manuálisan újra megfuttatni). Logoláshoz a beépített logging modult használjuk:
import logging
logger = logging.getLogger("my_favourite_logger")
logger.info("Ez történik épp a programban")
logger.warning("Ez nem feltétlen úgy történt, ahogy elvárnánk")
logger.error("Itt valami hiba történt")
Próbálj lényegre törően és jól követhetően logolni. Feleslegesen ne tűzdeld tele a logot, de ügyelj arra, hogy minden fontosabb információt kilogolj és ahol szükséges és érdemes a mért időket is.
Logolás exception esetén
Amennyiben a try-except minta segítségével kapunk egy hibát, és ezt logolni akarjuk, célszerű a logger.exception("Valami hiba") használata. Ez automatikusan logolja a stack trace-t (azaz hogy hol, milyen hívás közben történt pontosan az adott hiba), így nem kell manuálisan formázgatni. A következő minta mindenképp kerülendő:
Hibakezelés, tesztelés
Ha elkészültél a funkciókkal, WFR-be rakás előtt gondold át a lehetséges edge case-eket, és teszteld le alaposan az elkészült feature-t. Mivel a python service-ben elsődlegesen API endpoint-okat készítünk, ezeket könnyen tesztelheted manuálisan is, akár egy endpoint tesztelő alkalmazásból (pl. Postman), vagy akár a FastAPI swagger UI-ából (lokális indítás esetén localhost:8000/docs).
Fontos, hogy a lehető legtöbb hibát kezeld le, és ha meg lehet, akkor oldd meg Python oldalon, vagy térj vissza megfelelő hibakóddal és üzenettel. Ehhez az alábbi két dolgot érdemes észben tartani.
Először is ne kapj el tetszőleges hibát! Ez persze hatásvadász sarkítás, van, amikor pont hogy mindent el akarunk kapni, mivel adott függvénynek mindig hiba nélkül kell futnia (erre rengeteg példa van az Testing Framework kiértékelés moduljában). Ugyanakkor alapvetően ha egy hibát kezelni akarunk, érdemes kizárólag azt a hibát elkapni.
Példa
try:
x = method_that_usually_fails_with_value_error()
except:
x = []
logging.error("Method failed with ValueError")
Ebben a kódban feltételezzük, hogy az elkapott hiba az egy ValueError, és ennek megfelelően kezeljük. Ugynakkor ez csak feltételezés, ezzel akár egy TimeoutError hibát is elkaphattunk, amit lehet, teljesen máshogyan akarunk kezelni, vagy akár nem is akarjuk kezelni, csak egy magasabb szinten. Erre egyébként a Ruff figyelmeztet
Másodszor, készíts saját hibaosztályokat! Nyilván ez se írja le az esetek 100%-át. Ugyanakkor célszerű úgy megírni egy funkcionalitást, hogy egy féle hiba központilag kezelhető legyen, egyenesen a FastAPI beépített hibakezelőjén keresztül. Ezzel először is kikényszeríthető, hogy végiggondoljuk, hogy mi is az a hiba, amit mi elkapunk, és lehet, újabb helyeken is megtaláljuk. Másodszor a hibakezelés így elég magas szinten módosítható, tehát ha pl. egy modult kiemelünk, akkor nem egy framework szerint fogjuk a hibákat dobni, és a saját hibákat bárhogy kezelhetjük, kontextustúl függően.
Példa
Mutatunk egy példát 3 különböző megoldással. Tekintsük a következő feladatot: van egy osztályunk, ami fenntart egy listát. Ezt kezdetben üres, de bővíthetjük. Írjunk egy függvényt, ami kiszámolja a lista értékeinek átlagát:
class AveragingModule:
def __init__(self) -> None:
self.state: list[int] = []
def extend_state(self, value: int) -> None:
self.state.append(value)
def calculate_average(self) -> int: ...
Alkalmazzunk egyszerűen RuntimeError hibát a probléma észlelésekor.
def calculate_average(self) -> int:
if len(self.state) == 0:
raise RuntimeError("The state is empty")
return sum(self.state) / len(self.state)
Ekkor ugyan a függvény sikeresen hibát ad egy jó hibaüzenettel, ezt a hibát már nem tudjuk ilyen egyszerűen elkapni. Ugyanis ekkor vagy általános RuntimeError-t kapunk el a try-except struktúrában (amit bármi dobhat), vagy szűrünk magára a hibaüzenetre (nem túl elegáns, és változhat is). Továbbá amennyiben később nem kezeljük, a webappunk egyszerűen 500-as hibakóddal fog visszatérni egy ilyen esetben.
Használjuk a FastAPI beépített HTTPException hibáját.
from fastapi import HTTPException, status
def calculate_average(self) -> int:
if len(self.state) == 0:
raise HTTPException(
status_code = status.HTTP_403_FORBIDDEN,
detail = "The state is empty",
)
return sum(self.state) / len(self.state)
Itt részben ugyanaz a probléma áll fenn, mint a beépített hiba esetén: nem tudjuk később szépen kezelni a problémát. Amivel javít ez a megoldás a beépített eseten, hogy a webapp ekkor már helyes hibakóddal fog visszatérni, így a request feladója is hatékonyabban tudja kezelni a problémát.
Ugyanakkor más szempontból romlott a helyzet: ezzel a megoldással osztálynak függősége a fastapi csomag. Következik, hogy ha át akarjuk emelni máshova a kódot, akkor kénytelenek vagyunk módosítani azt (amennyiben ez nem egy FastAPI webapp).
Hozzunk létre egy specifikus hibát arra az esetre, amikor a state változó üres.
class EmptyAveragingStateError(RuntimeException): ...
def calculate_average(self) -> int:
if len(self.state) == 0:
raise EmptyAveragingStateError
return sum(self.state) / len(self.state)
Ekkor az első megoldás alapvető problémáját orvosoltuk: így könnyen el lehet kapni később a hibát. Továbbá elvégezhetjük az app FastAPI objektummal a következő módosítást:
from fastapi import status, HTTPException
from fastapi.exception_handlers import http_exception_handler
async def empty_averaging_state_error_handler(request: Request, exc: EmptyAveragingStateError):
http_exc = HTTPException(
status_code = status.HTTP_403_FORBIDDEN,
detail = "The state is empty",
)
return await http_exception_handler(request, http_exc)
...
app.add_exception_handler(EmptyAveragingStateError, empty_averaging_state_error_handler)
Ezzel a másik hibát is orvosoltuk: a webapp úgy fogja kezelni az új hibát, mint ha egy sima 403-as hibakódú HTTP hiba lenne.
A megoldás hátránya, hogy több boilerplate kódot igényel. Emellett ha túl specifikus hibákat gyártunk, akkor gyorsan el tudják lepni a kódbázist az egyetlen problémára specifikus hibaüzenetek. Azonban ha sikerül megtalálnunk a megfelelő „hiba-általánosítási szintet“, akkor egy tisztább, könnyebben kezelhető kódbázist kapunk.
Formázás
Mint ahogy azt a Kódminőség oldalon is láthattad, számos opcionális és kötelező kódminőség-ellenőrző eszközt alkalmazunk a projekten. Ügyelj, hogy ezeket WFR-be helyezés előtt lefuttasd, ezzel későbbi hibákat elkerülve.
Megjegyzések a kódban
Előfordulhat, hogy a kódban valami nem nyilvánvaló, hogy miért úgy lett megoldva ahogy, akár techinkai, akár koncepcionális szemszögből. A kódban tehát szabad ilyen módon dokumentálni, viszont ügyelj arra, hogy ez ne váltsa ki a megfelelően struktúrált kód írását; legtöbbször épp ellenkezőleg, a kód struktúrálásának indoklásaként célszerű alkalmazni. A következő komment flag-ek vannak a pyproject.toml fájlban felsorolva, ezek leírják a use-case-ek egy jelentős részét:
# TODO: Ezt majd a jövőben módosítani kell
# NOTE: Ezt így meg így oldottam meg, azért mert...
# FIXME: Ez egy potenciális hiba
# WARNING: Tudatosan nem kezelt probléma, próbáld ne szándékosan előidézni
Több soros komment hozzáadására a következő stílust szoktuk alkalmazni
### NOTE:
# Itt egy nagyon hosszú leírás, hogy miért nem lehet
# megoldani azt a problémát, amit akarunk.
#
# Ez a naygon súlyos probléma már korábban is előjött másoknál,
# itt vannak a referenciák:
# - www.stackoverflow.com/rendkivul-sulyos-problemat-nem-tudok-megoldani
A kommentek nyelve váltakozóan angol és magyar, nincs standard-izálva.