Xatolar: istisno dizayni
Xato — bu ham API
Funksiyangiz nima qaytarishi kabi, qanday yiqilishi ham uning shartnomasi. Chaqiruvchi shunga tayanadi.
3-bobdagi savolga qaytamiz: mavjud bo'lmagan faylni bersak nima bo'ladi?
.venv/bin/python3 -m hisobot yoq_fayl.csv
Traceback (most recent call last):
File "/usr/lib/python3.10/runpy.py", line 196, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/usr/lib/python3.10/runpy.py", line 86, in _run_code
exec(code, run_globals)
File "/mnt/c/Users/Admin/Desktop/Course Material/Python_Muhandisligi/ish/src/hisobot/__main__.py", line 24, in <module>
sys.exit(main())
File "/mnt/c/Users/Admin/Desktop/Course Material/Python_Muhandisligi/ish/src/hisobot/__main__.py", line 13, in main
n = tozala(oqi(argv[0]))
File "/mnt/c/Users/Admin/Desktop/Course Material/Python_Muhandisligi/ish/src/hisobot/oqish.py", line 11, in oqi
with open(yol, encoding="utf-8") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'yoq_fayl.csv'
FileNotFoundError va uzun Python izi. Bu dasturchi uchun yaxshi, foydalanuvchi uchun yomon.
Bu bobda ikkalasini ham to'g'ri qilamiz.
Uchta yomon naqsh
# 1. Yalang'och except — hamma narsani yutadi def yomon_1(x): try: return int(x) except: # Ctrl+C va SystemExit ni ham ushlaydi! return 0 # 2. Xatoni jim yutish def yomon_2(yol): try: with open(yol) as f: return f.read() except Exception: return None # nima bo'ldi? hech kim bilmaydi # 3. Xato o'rniga sehrli qiymat def yomon_3(x): try: return int(x) except ValueError: return -1 # -1 haqiqiy natijami yoki xatomi? print("yomon_1('abc') =", yomon_1("abc"), " <- xato 0 ga aylandi") print("yomon_2('yoq') =", yomon_2("yoq"), " <- fayl yo'qmi yoki bo'shmi?") print("yomon_3('abc') =", yomon_3("abc"), " <- -5 baho ham -1 emas, lekin...") print("yomon_3('-1') =", yomon_3("-1"), " <- bu haqiqiy qiymat, farqi yo'q!")
yomon_1('abc') = 0 <- xato 0 ga aylandi
yomon_2('yoq') = None <- fayl yo'qmi yoki bo'shmi?
yomon_3('abc') = -1 <- -5 baho ham -1 emas, lekin...
yomon_3('-1') = -1 <- bu haqiqiy qiymat, farqi yo'q!
Oxirgi qator eng muhimi: yomon_3 xato va haqiqiy -1 ni ajratib bo'lmaydigan qilib qo'ydi.
O'z istisnolaringiz
Loyihaga o'z istisno oilasini bering. Foydasi: chaqiruvchi "bu kutubxonaning xatosi" ni bitta
except bilan ushlay oladi.
shu faylni yaratingsrc/hisobot/xatolar.py
"""Loyihaning istisno oilasi. Hammasi HisobotXato dan meros oladi — foydalanuvchi bitta except bilan ushlay oladi. """ class HisobotXato(Exception): """Barcha hisobot xatolarining ildizi.""" class FaylXato(HisobotXato): """Fayl o'qib bo'lmadi.""" def __init__(self, yol: str, sabab: str) -> None: self.yol = yol self.sabab = sabab super().__init__(f"faylni o'qib bo'lmadi: {yol} ({sabab})") class QatorXato(HisobotXato): """Bitta qator qoidaga mos emas.""" def __init__(self, qator: int, maydon: str, sabab: str) -> None: self.qator = qator self.maydon = maydon self.sabab = sabab super().__init__(f"{qator}-qator, '{maydon}' maydoni: {sabab}") class SozlamaXato(HisobotXato): """Konfiguratsiya noto'g'ri (11-bob)."""
Har istisno ma'lumot saqlaydi (yol, qator, maydon), faqat matn emas. Bu muhim: chaqiruvchi
xabarni tahlil qilmasdan e.qator ni o'qiy oladi.
Istisnolarni ishlatamiz
shu faylni yaratingsrc/hisobot/oqish.py
"""CSV o'qish va qatorlarni tekshirib modelga aylantirish.""" import csv from pathlib import Path from .modellar import Natija, Talaba from .tekshiruv import bahoni_tekshir, sanani_tekshir from .xatolar import FaylXato, QatorXato KERAKLI_USTUNLAR = {"ism", "guruh", "baho", "sana"} def oqi(yol: str | Path) -> list[dict[str, str]]: """CSV ni o'qiydi. Har qanday muammoda FaylXato qaytaradi.""" p = Path(yol) try: matn = p.read_text(encoding="utf-8") except FileNotFoundError: raise FaylXato(str(p), "fayl topilmadi") from None except PermissionError: raise FaylXato(str(p), "o'qishga ruxsat yo'q") from None except UnicodeDecodeError as e: raise FaylXato(str(p), f"matn UTF-8 emas ({e.reason})") from None qatorlar = list(csv.DictReader(matn.splitlines())) if not qatorlar: raise FaylXato(str(p), "fayl bo'sh yoki faqat sarlavha bor") yetishmayotgan = KERAKLI_USTUNLAR - set(qatorlar[0]) if yetishmayotgan: raise FaylXato(str(p), f"ustunlar yetishmaydi: {sorted(yetishmayotgan)}") return qatorlar def tozala(qatorlar: list[dict[str, str]]) -> Natija: """Xom lug'atlarni Talaba obyektlariga aylantiradi. Xatolar to'planadi, otilmaydi.""" natija = Natija() for i, q in enumerate(qatorlar, start=2): try: baho = bahoni_tekshir(q.get("baho")) except ValueError as e: natija.xatolar.append((i, q.get("ism", "?"), str(e))) continue try: sana = sanani_tekshir(q.get("sana")) except ValueError as e: natija.xatolar.append((i, q.get("ism", "?"), str(e))) continue natija.talabalar.append(Talaba( ism=q.get("ism", "").strip(), guruh=q.get("guruh", "").strip(), baho=baho, sana=sana, )) return natija
.venv/bin/python3 -c " from hisobot.oqish import oqi from hisobot.xatolar import FaylXato, HisobotXato for yol in ['yoq_fayl.csv', 'sinov_malumot.csv']: try: q = oqi(yol) print(f'{yol}: {len(q)} qator o\\'qildi') except FaylXato as e: print(f'{yol}: XATO -> {e}') print(f' ma\\'lumot: yol={e.yol!r}, sabab={e.sabab!r}') "
yoq_fayl.csv: XATO -> faylni o'qib bo'lmadi: yoq_fayl.csv (fayl topilmadi) ma'lumot: yol='yoq_fayl.csv', sabab='fayl topilmadi' sinov_malumot.csv: 8 qator o'qildi
Endi xato tushunarli va mashina o'qiy oladigan.
Boshqa buzuq fayllarni ham sinaymiz:
shu faylni yaratingbuzuq_ustun.csv
ism,guruh,ball Aziz,A,85
shu faylni yaratingbosh.csv
ism,guruh,baho,sana
.venv/bin/python3 -c " from hisobot.oqish import oqi from hisobot.xatolar import FaylXato for yol in ['buzuq_ustun.csv', 'bosh.csv']: try: oqi(yol) except FaylXato as e: print(f'{yol:18} -> {e.sabab}') "
buzuq_ustun.csv -> ustunlar yetishmaydi: ['baho', 'sana'] bosh.csv -> fayl bo'sh yoki faqat sarlavha bor
Xulq o'zgardi — testlar buzildi
oqi endi FileNotFoundError emas, FaylXato otadi. 7-bobda yozilgan test esa eskisini kutadi.
Ishga tushiramiz:
.venv/bin/python3 -m pytest tests/test_oqish.py -q
...F [100%]
=================================== FAILURES ===================================
________________________________ test_fayl_yoq _________________________________
tmp_path = PosixPath('/tmp/pytest-of-abulqosim/pytest-66/test_fayl_yoq0')
def test_fayl_yoq(tmp_path):
with pytest.raises(FileNotFoundError):
> oqi(tmp_path / "yoq.csv")
tests/test_oqish.py:29:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
yol = PosixPath('/tmp/pytest-of-abulqosim/pytest-66/test_fayl_yoq0/yoq.csv')
def oqi(yol: str | Path) -> list[dict[str, str]]:
"""CSV ni o'qiydi. Har qanday muammoda FaylXato qaytaradi."""
p = Path(yol)
try:
matn = p.read_text(encoding="utf-8")
except FileNotFoundError:
> raise FaylXato(str(p), "fayl topilmadi") from None
E hisobot.xatolar.FaylXato: faylni o'qib bo'lmadi: /tmp/pytest-of-abulqosim/pytest-66/test_fayl_yoq0/yoq.csv (fayl topilmadi)
src/hisobot/oqish.py:18: FaylXato
=========================== short test summary info ============================
FAILED tests/test_oqish.py::test_fayl_yoq - hisobot.xatolar.FaylXato: faylni ...
Test yiqildi va bu to'g'ri: u eski shartnomani tekshiryapti. Testlar aynan shuning uchun bor — xulq o'zgarganda ular sizni ogohlantiradi.
Ikki yo'l bor: xulqni orqaga qaytarish yoki testni yangilash. Biz ongli ravishda xulqni o'zgartirdik, demak testni yangilaymiz:
shu faylni yaratingtests/test_oqish.py
import pytest from hisobot.oqish import oqi, tozala from hisobot.xatolar import FaylXato def test_oqi_qatorlar_soni(csv_fayl): qatorlar = oqi(csv_fayl) assert len(qatorlar) == 3 assert qatorlar[0]["ism"] == "Aziz" def test_tozala_xatoni_ajratadi(csv_fayl): n = tozala(oqi(csv_fayl)) assert len(n.talabalar) == 2 assert len(n.xatolar) == 1 assert n.jami == 3 def test_xato_qator_raqami(csv_fayl): n = tozala(oqi(csv_fayl)) qator, ism, sabab = n.xatolar[0] assert qator == 4 # sarlavha 1-qator, ma'lumot 2 dan boshlanadi assert ism == "Xato" assert "son emas" in sabab def test_fayl_yoq(tmp_path): """Endi FaylXato kutamiz — FileNotFoundError emas.""" with pytest.raises(FaylXato, match="topilmadi"): oqi(tmp_path / "yoq.csv")
.venv/bin/python3 -m pytest tests/test_oqish.py -q
.... [100%]
:::tip Test yiqilsa, avval "nega" deb so'rang Test yiqilganda ikki ehtimol bor: (1) kodni buzdingiz, (2) shartnomani ongli o'zgartirdingiz. Birinchisida kodni tuzating, ikkinchisida testni. Lekin hech qachon testni "o'tsin deb" o'zgartirmang — avval qaysi holat ekanini aniqlang. :::
raise ... from: sababni saqlash yoki yashirish
def xom(): return int("abc") print("1) from None — ichki xato YASHIRILADI:") try: try: xom() except ValueError: raise RuntimeError("tashqi xato") from None except RuntimeError as e: print(" __cause__:", e.__cause__, "| __context__:", type(e.__context__).__name__ if e.__context__ else None) print("2) from e — sabab SAQLANADI:") try: try: xom() except ValueError as e: raise RuntimeError("tashqi xato") from e except RuntimeError as e: print(" __cause__:", type(e.__cause__).__name__, "-", e.__cause__)
1) from None — ichki xato YASHIRILADI: __cause__: None | __context__: ValueError 2) from e — sabab SAQLANADI: __cause__: ValueError - invalid literal for int() with base 10: 'abc'
Qoida:
| Yozuv | Qachon |
|---|---|
raise Yangi(...) from e |
Sabab foydali — ichki xatoni ko'rsating |
raise Yangi(...) from None |
Sabab shovqin — foydalanuvchiga keraksiz tafsilot |
raise (yolg'iz) |
Xatoni ushlab, biror ish qilib, o'zgartirmasdan uzatish |
Bizning oqi da from None ishlatildi: foydalanuvchiga UnicodeDecodeError ning ichki tafsiloti
kerak emas, "matn UTF-8 emas" yetarli.
To'plash yoki otish
Ikki xil xato bor va ularni ajratish kerak:
import pandas as pd show(pd.DataFrame([ {"tur": "Davom etib bo'lmaydi", "misol": "fayl yo'q, ustun yetishmaydi", "nima qilish": "Darhol `raise` — ish ma'nosiz"}, {"tur": "Qisman muvaffaqiyat", "misol": "1000 qatordan 3 tasi buzuq", "nima qilish": "To'plang, davom eting, oxirida hisobot bering"}, ]))
| tur | misol | nima qilish |
|---|---|---|
| Davom etib bo'lmaydi | fayl yo'q, ustun yetishmaydi | Darhol `raise` — ish ma'nosiz |
| Qisman muvaffaqiyat | 1000 qatordan 3 tasi buzuq | To'plang, davom eting, oxirida hisobot bering |
Bizning loyihada oqi otadi (faylsiz ish yo'q), tozala esa to'playdi (bitta buzuq qator
butun hisobotni to'xtatmasligi kerak).
Bu tanlov vazifaga bog'liq. Bank o'tkazmalarida bitta buzuq qator butun paketni to'xtatishi kerak; o'quvchilar ro'yxatida esa yo'q.
try blokini qisqa saqlang
import pandas as pd show(pd.DataFrame([ {"yomon": "try ichida 20 qator kod", "nega": "Qaysi biri xato berdi — noma'lum; keraksiz xatolar ham ushlanadi"}, {"yomon": "except Exception", "nega": "Dasturlash xatolari (NameError, TypeError) ham yutiladi"}, {"yomon": "except (A, B, C, D, E)", "nega": "Ehtimol umumiy ota-sinf bor — o'shani ishlating"}, {"yomon": "try/except ichida yana try/except", "nega": "Odatda funksiyaga ajratish kerak"}, ]))
| yomon | nega |
|---|---|
| try ichida 20 qator kod | Qaysi biri xato berdi — noma'lum; keraksiz xatolar ham ushlanadi |
| except Exception | Dasturlash xatolari (NameError, TypeError) ham yutiladi |
| except (A, B, C, D, E) | Ehtimol umumiy ota-sinf bor — o'shani ishlating |
| try/except ichida yana try/except | Odatda funksiyaga ajratish kerak |
Aniq misol — tozala da har tekshiruv alohida try ichida. Shuning uchun xato xabari qaysi
maydon muammo qilganini aniq aytadi.
finally va with
import tempfile, os yol = os.path.join(tempfile.gettempdir(), "sinov_finally.txt") def finally_bilan(): f = open(yol, "w") try: f.write("ma'lumot") raise RuntimeError("xato yuz berdi") finally: f.close() print(" fayl yopildi:", f.closed) def with_bilan(): with open(yol, "w") as f: f.write("ma'lumot") raise RuntimeError("xato yuz berdi") for nom, fn in [("finally", finally_bilan), ("with", with_bilan)]: print(f"{nom}:") try: fn() except RuntimeError as e: print(" xato o'tdi:", e) os.unlink(yol)
finally: fayl yopildi: True xato o'tdi: xato yuz berdi with: xato o'tdi: xato yuz berdi
with — finally ning qisqa shakli. Fayl, ulanish, qulf — hammasi with bilan yopiladi, hatto
xato bo'lsa ham.
O'z kontekst menejeringizni yozish ham oson:
from contextlib import contextmanager import time @contextmanager def vaqtni_olch(nom): t0 = time.perf_counter() try: yield finally: print(f" [{nom}] {(time.perf_counter() - t0) * 1000:.1f} ms") with vaqtni_olch("muvaffaqiyatli ish"): sum(range(200_000)) try: with vaqtni_olch("xato bilan tugagan ish"): sum(range(100_000)) raise ValueError("to'xtadi") except ValueError: print(" xato ushlandi, lekin vaqt baribir o'lchandi")
[muvaffaqiyatli ish] 1.0 ms [xato bilan tugagan ish] 0.4 ms xato ushlandi, lekin vaqt baribir o'lchandi
Foydalanuvchiga ko'rsatish
Endi __main__.py ni tuzatamiz: iz emas, tushunarli xabar.
shu faylni yaratingsrc/hisobot/__main__.py
"""python3 -m hisobot bilan ishga tushadi.""" import sys from .hisob import daraja_taqsimoti, guruh_ortachasi from .oqish import oqi, tozala from .xatolar import HisobotXato def main(argv: list[str] | None = None) -> int: argv = sys.argv[1:] if argv is None else argv if not argv: print("ishlatish: python3 -m hisobot <fayl.csv>", file=sys.stderr) return 2 try: n = tozala(oqi(argv[0])) except HisobotXato as e: print(f"xato: {e}", file=sys.stderr) return 1 print(f"jami: {n.jami}, to'g'ri: {len(n.talabalar)}, xato: {len(n.xatolar)}") for g, o in guruh_ortachasi(n.talabalar).items(): print(f" {g}: {o}") print("darajalar:", daraja_taqsimoti(n.talabalar)) for qator, ism, sabab in n.xatolar: print(f" {qator}-qator ({ism}): {sabab}", file=sys.stderr) return 0 if not n.xatolar else 1 if __name__ == "__main__": sys.exit(main())
.venv/bin/python3 -m hisobot yoq_fayl.csv echo "--- qaytish kodi: $? ---" .venv/bin/python3 -m hisobot buzuq_ustun.csv echo "--- qaytish kodi: $? ---" .venv/bin/python3 -m hisobot sinov_malumot.csv echo "--- qaytish kodi: $? ---"
--- qaytish kodi: 1 ---
--- qaytish kodi: 1 ---
jami: 8, to'g'ri: 6, xato: 2
A: 90.67
B: 83.0
C: 61.0
darajalar: {'yaxshi': 3, "a'lo": 2, 'qoniqarli': 1}
--- qaytish kodi: 1 ---
xato: faylni o'qib bo'lmadi: yoq_fayl.csv (fayl topilmadi)
xato: faylni o'qib bo'lmadi: buzuq_ustun.csv (ustunlar yetishmaydi: ['baho', 'sana'])
5-qator (Elyor): baho son emas: ''
8-qator (Hasan): baho son emas: 'abc'
Uch xil holat, uch xil xabar, va har birida mos qaytish kodi. Uzun Python izi yo'qoldi.
:::gotcha Xato stderr ga chiqsin
print(..., file=sys.stderr) muhim: shunda python3 -m hisobot fayl.csv > natija.txt qilganda
xatolar faylga emas, ekranga chiqadi. Bu Unix an'anasi va boshqa dasturlar shunga tayanadi.
:::
Testlar
shu faylni yaratingtests/test_xatolar.py
import pytest from hisobot.oqish import oqi, tozala from hisobot.xatolar import FaylXato, HisobotXato, QatorXato def test_fayl_yoq(tmp_path): with pytest.raises(FaylXato, match="topilmadi"): oqi(tmp_path / "yoq.csv") def test_fayl_xato_hisobot_xato_ham(): """FaylXato — HisobotXato ning bolasi, demak umumiy except ishlaydi.""" assert issubclass(FaylXato, HisobotXato) assert issubclass(QatorXato, HisobotXato) def test_xato_malumot_saqlaydi(tmp_path): yol = tmp_path / "yoq.csv" try: oqi(yol) except FaylXato as e: assert e.yol == str(yol) assert e.sabab == "fayl topilmadi" else: pytest.fail("FaylXato kutilgandi") def test_ustun_yetishmaydi(tmp_path): yol = tmp_path / "x.csv" yol.write_text("ism,guruh\nA,B\n", encoding="utf-8") with pytest.raises(FaylXato, match="ustunlar yetishmaydi"): oqi(yol) def test_bosh_fayl(tmp_path): yol = tmp_path / "bosh.csv" yol.write_text("ism,guruh,baho,sana\n", encoding="utf-8") with pytest.raises(FaylXato, match="bo'sh"): oqi(yol) def test_tozala_xato_otmaydi(): """Buzuq qator istisno otmaydi, to'planadi.""" n = tozala([{"ism": "A", "guruh": "X", "baho": "abc", "sana": "2026-01-01"}]) assert n.talabalar == [] assert len(n.xatolar) == 1
.venv/bin/python3 -m pytest -q python3 -m mypy
.............................................FF. [100%]
=================================== FAILURES ===================================
_____________________________ test_ataylab_notogri _____________________________
def test_ataylab_notogri():
> assert bahoni_tekshir("85") == 58
E AssertionError: assert 85 == 58
E + where 85 = bahoni_tekshir('85')
tests/test_yiqiladi.py:5: AssertionError
___________________________ test_royxat_solishtirish ___________________________
def test_royxat_solishtirish():
kutilgan = ["Aziz", "Bobur", "Dilnoza"]
olingan = ["Aziz", "Bobur", "Elyor"]
> assert olingan == kutilgan
E AssertionError: assert ['Aziz', 'Bobur', 'Elyor'] == ['Aziz', 'Bobur', 'Dilnoza']
E
E At index 2 diff: 'Elyor' != 'Dilnoza'
E Use -v to get more diff
tests/test_yiqiladi.py:11: AssertionError
=========================== short test summary info ============================
FAILED tests/test_yiqiladi.py::test_ataylab_notogri - AssertionError: assert ...
FAILED tests/test_yiqiladi.py::test_royxat_solishtirish - AssertionError: ass...
Success: no issues found in 7 source files
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
Yalang'och except: |
Ctrl+C ni ham yutadi — hech qachon ishlatmang |
| Sehrli qiymat | -1 yoki None qaytarish xatoni haqiqiy qiymatdan ajratib bo'lmas qiladi |
| O'z istisno oilasi | Bitta ildiz (HisobotXato), bolalarda ma'lumot saqlanadi |
from None / from e |
Sababni yashirish yoki saqlash — ongli tanlov |
| To'plash yoki otish | "Davom etib bo'lmaydi" — oting; "qisman" — to'plang |
try uzunligi |
Qisqa bo'lsin; har tekshiruv alohida |
with |
finally ning qisqa shakli; o'zingiznikini @contextmanager bilan yozing |
stderr |
Xatolar stderr ga, natija stdout ga |
Talaba ishi
oqiga papka yo'lini bering (fayl emas). Qanday xato chiqadi va uFaylXatomi? Tuzating.QatorXatosinfi yozilgan, lekin ishlatilmagan.tozalani uni ishlatadigan qilib o'zgartiring. Testlar hali ham o'tadimi?vaqtni_olchkontekst menejerini kengaytiring:yieldorqali o'tgan vaqtni qaytarsin.- UTF-8 bo'lmagan CSV yasang (
latin-1da yozing) vaoqiga bering. Xabar tushunarlimi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.