Ачааллж байна...
Энэ бүлгийн өмнөх хоёр хичээл алдааг төлөвлөх ба дамжуулах-д зориулагдсан: өөрийн шатлал, raise from, тэмдэглэл. Энэ хичээл алдааны дараах ажилд зориулагдсан: цэвэрлэгээ. Програм нөөц ашиглах бүрт, файл, түр файл, сүлжээний холболт, түгжээ, ажил амжилттай болсон ч, алдаагаар тасарсан ч тэр нөөцийг зөв байдалд үлдээх ёстой. Хагас бичигдсэн файл, хаагдаагүй холболт, суллагдаагүй түгжээ бол програмын хамгийн хэцүү олддог алдаануудын нэг, учир нь тэдгээр шинж тэмдгээ ихэвчлэн огт өөр газар, огт өөр цагт харуулдаг.
Python-д цэвэрлэгээг илэрхийлэх гурван хэрэгсэл бий бөгөөд та гурвууланг нь аль хэдийн мэднэ. finally: try блок хэрхэн дууссанаас үл хамааран заавал ажилладаг блок. Хичээл 6.1-т бид with-ийг “finally-ийн товчлол” гэж задалсан. else: try блок алдаагүй дууссан үед л ажилладаг блок. Энэ бол try-ийн хамгийн бага хэрэглэгддэг хэсэг бөгөөд олон хөгжүүлэгч түүнийг хэзээ ч хэрэглэдэггүй. Context manager: Бүлэг 6-ын хэрэгсэл, цэвэрлэгээний бодлогыг дахин ашиглагдах объектод багцалдаг.
Энэ хичээлд бид нэг бодит асуудлыг сонгож, түүнийг гурван аргаар шийднэ. Асуудал нь: тайлангийн файлыг хагас бичигдсэн хэвээр хэзээ ч үлдээхгүй байх. Шийдэл бүрийг ажиллуулж, амжилттай ба алдаатай замуудыг trace-ээр мөрдөж, try-ийн дөрвөн хэсэг, try, except, else, finally, ямар дарааллаар ажилладгийг бүрэн ойлгоно. Энэ дараалал нэг Windows-ийн тусгай алдааг тайлбарлах бөгөөд энэ алдаатай та бодит ажилд заавал таарна. Эцэст нь хэзээ аль хэрэгслийг сонгох шударга зөвлөмж гаргана.
Мэдээллийн самбарт шинэ зар наах ажлыг төсөөлөөд үзээрэй. Хамгийн гэнэн арга: хуучин зарыг хуулаад, шинийг шууд самбар дээр гараар бичих. Хэрэв бичих явцад үзэг дуусвал, эсвэл хэн нэгэн таныг дуудаж аваачвал самбар дээр хагас бичигдсэн зар үлдэнэ: хуучин мэдээлэл алга, шинэ нь дутуу. Уншиж буй хүмүүс буруу мэдээлэл авна.
Ухаалаг арга: шинэ зарыг эхлээд ширээн дээрх тусдаа цаасан дээр бичих. Бичиж дуусахад л самбар дээрх хуучин зарыг хурдан солих. Хэрэв бичих явцад юм болвол ширээн дээрх хагас цаасыг хаяж, самбар дээр хуучин, бүрэн зар хэвээр үлдэнэ. Аль ч мөчид самбар дээр бүрэн зар байна: хуучин эсвэл шинэ, хэзээ ч хагас биш.
Энэ ажилд гурван үүрэг бий. Нэгдүгээрт, ажил юу ч болсон үзгээ тагла: finally, заавал хийгдэх цэвэрлэгээ. Хоёрдугаарт, бичиж дууссан бол л самбарт наа: else, зөвхөн амжилтын үед. Гуравдугаарт, бүтэлгүйтвэл ширээн дээрх цаасыг хая: except, зөвхөн алдааны үед. Гурав нь өөр нөхцөлд ажилладаг бөгөөд тэдгээрийг холих нь, жишээлбэл бүтэлгүйтсэн ч самбарт наах, бичиж дууссан ч хаях, зар алдахад хүргэнэ.
Програмчлалд энэ аргыг atomic write буюу "хуваагдашгүй бичилт" гэж нэрлэдэг: файлыг эхлээд түр нэрээр бүрэн бичиж, дараа нь нэг үйлдлээр жинхэнэ нэр рүү солих. Python-ий os.replace() функц нэг хавтас доторх файлыг ингэж солих бөгөөд үйлдлийн систем түүнийг хагас хийгдэхгүй гэж баталгаажуулдаг.
Эхлээд асуудлыг харъя. Борлуулалтын мөрүүдийг тайлангийн файлд бичдэг функц, мөр бүрийг форматлах тусдаа функцтэй:
python
def format_row(row):
product, amount = row
if not isinstance(amount, int):
raise ValueError(f"'{product}' мөрийн дүн бүхэл тоо биш: {amount!r}")
return f"{product}: {amount:,}₮\n"
def export_naive(rows, path):
with open(path, "w", encoding="utf-8") as f:
for row in rows:
f.write(format_row(row))
good = [("Ном", 25000), ("Дэвтэр", 1500)]
bad = [("Үзэг", 800), ("Цүнх", "45к"), ("Ном", 25000)]
export_naive(good, "report.txt")
print(open("report.txt", encoding="utf-8").read())
try:
export_naive(bad, "report.txt")
except ValueError as error:
print("Алдаа:", error)
print(open("report.txt", encoding="utf-8").read())Ном: 25,000₮
Дэвтэр: 1,500₮
Алдаа: 'Цүнх' мөрийн дүн бүхэл тоо биш: '45к'
Үзэг: 800₮Эхний экспорт амжилттай. Хоёр дахь экспорт хоёр дахь мөр дээр алдаа гаргасан. Эцсийн файлыг хараарай: өмнөх бүрэн тайлан алга, оронд нь ганц "Үзэг" мөртэй хагас тайлан. open(path, "w") файлыг нээх мөчид түүний агуулгыг устгадаг. with блок файлыг зөв хаасан, Бүлэг 6-ын баталгаа ажилласан, гэвч хаалт нь асуудлыг шийдсэнгүй: файл хаагдсан ч хагас. Хэрэв өөр програм, жишээлбэл нягтлангийн скрипт, энэ файлыг уншдаг бол буруу дүнтэй тайлан авна. Энэ бол самбар дээрх хагас зар.
Шийдлийн шаардлага тодорхой: экспорт бүтэлгүйтвэл өмнөх тайлан бүрэн хэвээр үлдэх ёстой. Бүтэлгүйтсэн оролдлогын ул мөр, түр файл, үлдэх ёсгүй.
Эхний шийдэл try-ийн бүх хэсгийг тодорхой бичнэ:
python
import os
def export_report(rows, path):
temp_path = path + ".tmp"
try:
f = open(temp_path, "w", encoding="utf-8")
try:
for row in rows:
f.write(format_row(row))
finally:
f.close()
except Exception:
if os.path.exists(temp_path):
os.remove(temp_path)
raise
else:
os.replace(temp_path, path)
print(f"Тайлан хадгалагдлаа: {path}")Хоёр давхар try байна. Дотоод try/finally ганц ажилтай: юу ч болсон файлыг хаах. Гадна try/except/else түр файлын хувь заяаг шийднэ: алдаа гарвал түр файлыг устгаж, алдааг Хичээл 11.2-ын ганц raise-ээр өөрчлөлтгүй дээш дамжуулна. Алдаа гараагүй бол түр файлыг жинхэнэ нэр рүү солино. os.path.exists() файл байгаа эсэхийг шалгана: хэрэв open өөрөө бүтэлгүйтсэн бол түр файл үүсээгүй байж болно.
python
export_report(good, "report.txt")
try:
export_report(bad, "report.txt")
except ValueError as error:
print("Алдаа:", error)
print(open("report.txt", encoding="utf-8").read())
print("Түр файл үлдсэн үү:", os.path.exists("report.txt.tmp"))Тайлан хадгалагдлаа: report.txt
Алдаа: 'Цүнх' мөрийн дүн бүхэл тоо биш: '45к'
Ном: 25,000₮
Дэвтэр: 1,500₮
Түр файл үлдсэн үү: FalseБүтэлгүйтсэн экспортын дараа өмнөх бүрэн тайлан хэвээр, түр файл цэвэрлэгдсэн. Самбар дээр хуучин бүрэн зар.
Trace: дөрвөн хэсэг ямар дарааллаар ажилладаг вэ
Хоёр замыг алхам алхмаар мөрдье. Энэ trace хичээлийн хамгийн чухал хэсэг, учир нь try-ийн хэсгүүдийн дараалал ихэнх хүний таамаглахаас өөр.
Амжилттай зам (export_report(good, ...)):
Алхам 1 (гадна try эхлэх). Түр файл нээгдэнэ. Файлын систем: report.txt (хуучин), report.txt.tmp (хоосон, нээлттэй).
Алхам 2 (дотоод try). Хоёр мөр бичигдэнэ. Алдаа гараагүй.
Алхам 3 (дотоод finally). Файл хаагдана. Файлын систем: report.txt (хуучин), report.txt.tmp (бүрэн, хаалттай).
Алхам 4 (гадна except-ийг алгасах). Алдаа гараагүй тул except алгасагдана.
Алхам 5 (гадна else). Гадна try блок, дотоод try/finally бүхэлдээ, алдаагүй дууссан тул else ажиллана: os.replace түр файлыг report.txt руу солино. Файлын систем: report.txt (шинэ, бүрэн). Түр файл алга, учир нь replace түүнийг шилжүүлсэн.
Алдаатай зам (export_report(bad, ...)):
Алхам 1 (гадна try эхлэх). Түр файл нээгдэнэ.
Алхам 2 (дотоод try). "Үзэг" бичигдэнэ. "Цүнх" мөр дээр format_row ValueError шиднэ. Дотоод try-ийн үлдсэн хэсэг алгасагдана.
Алхам 3 (дотоод finally). Алдаа дээш явахын өмнө finally ажиллаж файлыг хаана. Файлын систем: report.txt (хуучин, бүрэн), report.txt.tmp (хагас, хаалттай). Дотоод try-д except байхгүй тул алдаа үргэлжлэн гадна try руу явна.
Алхам 4 (гадна except). except Exception ValueError-ийг барина. Түр файл байгаа тул устгагдана. Файлын систем: report.txt (хуучин, бүрэн). raise алдааг дээш дамжуулна.
Алхам 5 (else алгасагдах). Алдаа гарсан тул else ажиллахгүй. report.txt огт хөндөгдөөгүй.
Энэ trace-ийн хамгийн чухал баримт: except ба else нь finally-ээс өмнө ажилладаг, ижил try хэлбэрт. Хэрэв бид бүх зүйлийг нэг try-д бичиж, файл хаахыг тэр try-ийн finally-д тавьсан бол except эсвэл else ажиллах үед файл хараахан нээлттэй байх байсан. Windows-д нээлттэй файлыг устгах эсвэл солих боломжгүй: Түгээмэл алдаанууд хэсэгт энэ алдааны бодит мессежийг харна. Тиймээс хоёр давхар try хэрэгтэй болсон: файл хаах дотоод finally түр файлын шийдвэрээс өмнө ажиллах ёстой.
else яагаад хэрэгтэй вэ
else-ийг орхиж, os.replace-ийг гадна try блокийн төгсгөлд тавьж болох мэт санагдана. Үр дүн амжилттай замд ижил. Гэвч алдаатай тохиолдлыг бодъё: түр файл бүрэн бичигдсэн, гэвч os.replace бүтэлгүйтэв. Жишээлбэл report.txt-ийг Excel-д нээсэн байсан тул Windows түүнийг солихыг зөвшөөрсөнгүй. Хэрэв replace try дотор байсан бол except түүнийг барьж, бүрэн бичигдсэн түр файлыг устгана. Хэрэглэгч Excel-ээ хаагаад дахин оролдсон ч ажил бүхэлдээ алга, экспортыг эхнээс нь ажиллуулах хэрэгтэй.
else энэ асуудлыг шийддэг: try блок зөвхөн бүтэлгүйтэх гэж хүлээж буй кодыг агуулах ёстой, тэр бүтэлгүйтлийн шийдэл except-д. Амжилтын дараах алхмууд else-д, өөрийн алдаагаа өөрөө гаргана, except-ийн цэвэрлэгээнд хүрэлгүйгээр. Бүлэг 9.4-т “Observer хариу үйлдэлд, шийдвэрт биш” гэж хүрээг ялгасан шиг, else try-ийн хүрээг нарийн байлгадаг. Нарийн try бол Хичээл 11.1-ийн "зөвхөн юу хийхээ мэддэг алдааг бар" зарчмын нөгөө тал: зөвхөн юу хийхээ мэддэг кодыг хамгаал.
Арга 1-ийн дотоод try/finally нь файл хаах ганц үүрэгтэй. Бүлэг 6-аас бид мэднэ: файл объект context manager бөгөөд with яг энэ ажлыг хийдэг. Дотоод давхаргыг with-ээр солье:
python
def export_report(rows, path):
temp_path = path + ".tmp"
try:
with open(temp_path, "w", encoding="utf-8") as f:
for row in rows:
f.write(format_row(row))
except Exception:
if os.path.exists(temp_path):
os.remove(temp_path)
raise
else:
os.replace(temp_path, path)
print(f"Тайлан хадгалагдлаа: {path}")Арван гурван мөрөөс арав болсон, нэг давхар try алга. Trace ижил: with блокийн __exit__ нь Арга 1-ийн дотоод finally-ийн байрыг эзэлж, файлыг гадна except эсвэл else-ээс өмнө хаана, учир нь with блок гадна try блокийн дотор бүхэлдээ дуусдаг. Энэ хувилбар ихэнх Python хөгжүүлэгчийн бичих хэлбэр: файл, түгжээ гэх мэт context manager-тай нөөцийн хаалтыг with-д даатгаж, бизнесийн шийдвэрийг try/except/else-д үлдээх.
Арга 2 сайн, гэвч нэг асуудал үлдсэн. Хэрэв програм арван газарт файл бичдэг бол түр файлын бүх логик, түр нэр, устгах, солих, арван газарт хуулагдана. Хэрэв нэг газар os.path.exists шалгалтыг мартвал, эсвэл түр файлын нэрийг өөрөөр бичвэл алдаа зөвхөн тэр газарт гарна. Хичээл 6.3-ын хэрэгслээр энэ бодлогыг нэг удаа, дахин ашиглагдах context manager болгоё:
python
import os
from contextlib import contextmanager
@contextmanager
def atomic_write(path, encoding="utf-8"):
temp_path = path + ".tmp"
try:
with open(temp_path, "w", encoding=encoding) as f:
yield f
except BaseException:
if os.path.exists(temp_path):
os.remove(temp_path)
raise
else:
os.replace(temp_path, path)Хэрэглээ:
python
def export_report(rows, path):
with atomic_write(path) as f:
for row in rows:
f.write(format_row(row))
print(f"Тайлан хадгалагдлаа: {path}")export_report одоо зөвхөн өөрийн ажлаа, тайлан бичихийг агуулж байна. "Түр файл", "устгах", "солих" гэсэн үгс түүнд огт байхгүй. Бодлого atomic_write-д амьдарна. Хичээл 6.3-ын trace-ийг санаарай: with блок дотор алдаа гарвал тэр yield мөрөн дээр шидэгдэнэ. Тэгэхээр atomic_write-ийн бүтэц Арга 2-той яг ижил ажиллана: yield дотоод with дотор байгаа тул файл эхлээд хаагдаж, дараа нь except эсвэл else.
Энд BaseException барьсныг анзаараарай. Хичээл 11.1-т бид түүнийг барихаас сэрэмжлүүлсэн, учир нь тэр Ctrl+C-г ч барина. Гэвч энд тэр зөв: бид алдааг залгихгүй, цэвэрлээд ганц raise-ээр заавал дахин шиддэг. Хэрэв хэрэглэгч урт экспортын дунд Ctrl+C дарвал түр файл устгагдаж, дараа нь KeyboardInterrupt үргэлжлэн програмыг зогсооно. except Exception-тэй бол Ctrl+C-ийн дараа хагас түр файл үлдэх байсан. Дүрмийг нарийсгая: BaseException-ийг зөвхөн цэвэрлэгээнд, заавал дахин шидэх нөхцөлтэй барина.
Одоо атомын бичилт програмын хаана ч нэг мөр:
python
with atomic_write("config.json") as f:
json.dump(settings, f, ensure_ascii=False, indent=2)Тохиргооны файлыг хадгалах явцад програм унавал хуучин тохиргоо бүрэн хэвээр үлдэнэ. Хагас JSON-той тохиргооны файл програмыг дараагийн эхлэлд Хичээл 9.3-ын JSONDecodeError-оор унагах байсан.
Аль аргыг сонгох вэ
Гурван аргыг шударгаар харьцуулъя. Тус бүр өөрийн газартай.
finally нь context manager байхгүй, нэг удаагийн цэвэрлэгээнд хамгийн шууд. Жишээлбэл функцийн эхэнд ямар нэг тохиргоог өөрчлөөд, төгсгөлд буцаах, тэр загвар өөр хаана ч давтагдахгүй бол. Мөн try-ийн дөрвөн хэсгийн дараалал хэрхэн ажилладгийг ойлгоход Арга 1 хамгийн тодорхой, тиймээс энэ хичээл түүнээс эхэлсэн.
else бие даасан хэрэгсэл биш, бусад хоёртой хамт хэрэглэгдэнэ: "амжилтын дараах алхам except-ийн хамгаалалтаас гадна байх ёстой" үед. Бодит кодонд else ховор, гэвч тэр хэрэгтэй үед, Арга 1-ийн “replace бүтэлгүйтэхэд бүрэн файл устах” тохиолдол шиг, түүнгүйгээр зөв бичих хэцүү.
Context manager нь Python-ий хамгийн их илүүд үздэг хэлбэр: нөөц аль хэдийн context manager-тэй бол, файл, түгжээ, түр хавтас, with. Ижил цэвэрлэгээний бодлого хоёроос олон газарт давтагдах бол өөрийн context manager, Арга 3. Бодлого нэг газарт, тестлэгддэг, нэр нь зорилгоо хэлдэг: atomic_write гэсэн нэр гурван догол мөр тайлбараас илүү тодорхой.
Хүсвэл atomic_write-д pytest тест бичиж үзээрэй: tmp_path fixture-ийг ашиглан амжилттай бичилт жинхэнэ файлыг үүсгэж байгааг, with блок дотор санаатайгаар ValueError шидэхэд анхны файл өөрчлөгдөхгүй, түр файл үлдэхгүй байгааг шалгах. Хичээл 6.4 ба 9.4-ийн тестүүд загвар болно. Сонирхвол Хичээл 3.4-ийн log боловсруулагчийн analyze.py-д тайланг atomic_write-ээр файлд бичдэг сонголт нэмж болно. Мөн Арга 1-ийн дотоод try/finally-г арилгаж, f.close()-ийг гадна finally-д шилжүүлээд Windows дээр алдаатай замыг ажиллуулах нь Түгээмэл алдааны эхний мессежийг өөрийн нүдээр харах шууд арга. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
PermissionError: [WinError 32] The process cannot access the file because it is being used by another process: ‘report.txt.tmp’. "Файлыг өөр процесс хэрэглэж байгаа тул хандах боломжгүй" гэсэн утгатай. Windows-ийн тусгай алдаа: нээлттэй файлыг устгах эсвэл солих гэж оролдоход гарна. Энэ хичээлийн контекстэд ихэвчлэн try-ийн дарааллын алдаанаас: файлыг хаах код finally-д, түүнийг устгах эсвэл солих код ижил try-ийн except эсвэл else-д байх үед. Trace-ийн гол баримтыг санаарай: except ба else нь finally-ээс өмнө ажилладаг. Шийдэл: файлыг дотоод try/finally эсвэл with-ээр, шийдвэрээс өмнө хаах. macOS ба Linux нээлттэй файлыг устгахыг ихэвчлэн зөвшөөрдөг тул ийм алдаа тэнд нуугдаж, зөвхөн Windows-д илэрч болно. Мөн файлыг Excel эсвэл өөр програмд нээсэн үед ч ижил алдаа гарна.
Бүрэн бичигдсэн ажил алга болох. Мессежгүй, else-ийн хэсэгт тайлбарласан алдаа. Амжилтын дараах алхмыг try дотор байрлуулахад тэр алхмын алдааг except барьж, бүтэлгүйтэл гэж үзэн цэвэрлэнэ. Шинж тэмдэг нь "заримдаа экспорт ямар ч ул мөргүй алга болдог". Шийдэл: try-д зөвхөн бүтэлгүйтэх гэж хүлээж буй кодыг үлдээж, дараах алхмуудыг else-д.
Алдаа чимээгүй алга болох: finally доторх return. Мессежгүй, ноцтой алдаа:
python
def risky():
try:
raise ValueError("чухал алдаа")
finally:
return "амжилттай"
print(risky())амжилттайfinally дотор return бичихэд тэр шидэгдсэн алдааг залгина: ValueError хаана ч харагдахгүй, функц амжилттай мэт утга буцаана. break ба continue ч ижил үр дагавартай. Python 3.14-өөс хойш ийм кодонд “SyntaxWarning: ‘return’ in a ‘finally’ block” буюу “finally блок доторх return” гэсэн анхааруулга гарна. Хуучин хувилбаруудад анхааруулгагүй. Дүрэм: finally зөвхөн цэвэрлэгээнд, хэзээ ч return, break, continue биш.
SyntaxError: expected ‘except’ or ‘finally’ block. “except эсвэл finally блок хүлээсэн” гэсэн утгатай. except-гүйгээр try: ... else: ... бичихэд гарна. else-ийн утга нь “except ажиллаагүй бол” тул except-гүй else утгагүй. Хэрэв except хэрэггүй бол else-ийн кодыг ердөө try-ийн дараа, try-ийн гадна бичнэ.
FileNotFoundError: [Errno 2] No such file or directory: ‘report.txt.tmp’. "Ийм файл байхгүй" гэсэн утгатай. except блок дотор түр файлыг os.path.exists шалгалтгүйгээр устгах үед гарна: хэрэв open өөрөө бүтэлгүйтсэн бол, жишээлбэл хавтас байхгүй, түр файл огт үүсээгүй. Энэ алдаа цэвэрлэгээний дотор гарч, Хичээл 11.2-ын “During handling of the above exception” гинжийг үүсгэж, анхны алдааг нууна. Шийдэл: exists шалгалт, эсвэл Хичээл 6.3-ын with ignore(FileNotFoundError):, эсвэл стандарт сангийн contextlib.suppress(FileNotFoundError). Цэвэрлэгээний код өөрөө алдаа гаргах ёсгүй.
Цэвэрлэгээ нь ажил амжилттай ч, бүтэлгүй ч нөөцийг зөв байдалд үлдээдэг. try-ийн дөрвөн хэсэг өөр нөхцөлд ажилладаг: try хамгаалагдсан код, except зөвхөн алдааны үед, else зөвхөн алдаагүй үед, finally үргэлж. Хамгийн чухал дараалал: except ба else нь ижил try-ийн finally-ээс өмнө ажилладаг, тиймээс файл хаах шиг заавал эхлээд хийгдэх цэвэрлэгээг дотоод try/finally эсвэл with-д байрлуулна. else try-ийг нарийн байлгаж, амжилтын дараах алхмын алдааг бүтэлгүйтлийн цэвэрлэгээнээс тусгаарладаг. Atomic write нь файлыг түр нэрээр бүрэн бичиж, os.replace-ээр нэг үйлдлээр солих загвар бөгөөд бүтэлгүйтвэл анхны файл бүрэн хэвээр үлдэнэ. Нэг удаагийн цэвэрлэгээнд finally, context manager-тай нөөцөд with, давтагдах бодлогод өөрийн context manager. Цэвэрлэгээнд зориулж BaseException-ийг барих нь зөв, гэхдээ заавал дахин шиднэ. finally доторх return алдааг залгидаг тул хэзээ ч бүү бич. Цэвэрлэгээний код өөрөө алдаа гаргахгүй байх ёстой.
Ингээд Бүлэг 11 дуусч, алдаанууд таны програмын төлөвлөгдсөн, шалгагдсан хэсэг боллоо. Дараагийн бүлэг Түвшин 4-ийн гурав дахь сэдэв рүү шилжинэ: хурд. Энэ сургалтын эхнээс бид "хурдны тухай мэдэгдэл хэмжилтгүйгээр гарахгүй" гэсэн дүрмийг баримталж, timeit ба perf_counter-ийг хэд хэдэн удаа хэрэглэсэн. Дараагийн хичээлд тэр дүрэм хууль болно: хэмжихгүйгээр бүү оновчил. Бид хоёр хэмжилт хийх бөгөөд хоёулаа таны зөн совинг гайхшруулах болно: "энэ хурдан байх ёстой" гэсэн мэдрэмж хэр олон удаа буруу байдгийг тоогоор харна.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.