Asosiy qismga o'tish

Xatolar: istisno dizayni

8-bob 20 kod bloki 3162 so'z

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?

matn1 qator
.venv/bin/python3 -m hisobot yoq_fayl.csv
Chiqish (qaytish kodi 1, 189 ms)
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

python26 qator
# 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!")
Chiqish
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

python31 qator
"""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

python53 qator
"""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
python12 qator
.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}')
"
Chiqish (qaytish kodi 0, 170 ms)
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

matn2 qator
ism,guruh,ball
Aziz,A,85

shu faylni yaratingbosh.csv

matn1 qator
ism,guruh,baho,sana
python10 qator
.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}')
"
Chiqish (qaytish kodi 0, 468 ms)
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:

matn1 qator
.venv/bin/python3 -m pytest tests/test_oqish.py -q
Chiqish (qaytish kodi 1, 3405 ms)
...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

python31 qator
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")
matn1 qator
.venv/bin/python3 -m pytest tests/test_oqish.py -q
Chiqish (qaytish kodi 0, 2453 ms)
....                                                                     [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

python20 qator
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__)
Chiqish
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:

python7 qator
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"},
]))
turmisolnima qilish
Davom etib bo'lmaydifayl yo'q, ustun yetishmaydiDarhol `raise` — ish ma'nosiz
Qisman muvaffaqiyat1000 qatordan 3 tasi buzuqTo'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

python7 qator
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"},
]))
yomonnega
try ichida 20 qator kodQaysi biri xato berdi — noma'lum; keraksiz xatolar ham ushlanadi
except ExceptionDasturlash xatolari (NameError, TypeError) ham yutiladi
except (A, B, C, D, E)Ehtimol umumiy ota-sinf bor — o'shani ishlating
try/except ichida yana try/exceptOdatda 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

python25 qator
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)
Chiqish
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:

python20 qator
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")
Chiqish
   [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

python30 qator
"""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())
matn6 qator
.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: $? ---"
Chiqish (qaytish kodi 0, 516 ms)
--- 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

bash47 qator
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
matn2 qator
.venv/bin/python3 -m pytest -q
python3 -m mypy
Chiqish (qaytish kodi 0, 5626 ms)
.............................................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

  1. oqi ga papka yo'lini bering (fayl emas). Qanday xato chiqadi va u FaylXato mi? Tuzating.
  2. QatorXato sinfi yozilgan, lekin ishlatilmagan. tozala ni uni ishlatadigan qilib o'zgartiring. Testlar hali ham o'tadimi?
  3. vaqtni_olch kontekst menejerini kengaytiring: yield orqali o'tgan vaqtni qaytarsin.
  4. UTF-8 bo'lmagan CSV yasang (latin-1 da yozing) va oqi ga bering. Xabar tushunarlimi?

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar