Ачааллж байна...
Өмнөх хичээлд бид timeit-ээр жижиг кодын хэсгийг хэмжиж сурсан. timeit нэг асуултад маш сайн хариулдаг: "энэ хэсэг хэр хурдан вэ?" Гэвч бодит програмд эхлээд өөр асуулт гардаг: "програмын хугацаа хаана зарцуулагдаж байна вэ?" Хэдэн зуун мөр, хэдэн арван функцтэй script 30 секунд ажилладаг бол бид функц бүрийг timeit-ээр нэг нэгээр нь хэмжиж чадахгүй. Хэмжсэн ч функцүүд бие биеэ дууддаг тул тус бүрийн хугацааг нэмж нийт хугацааг тайлбарлах боломжгүй.
Энэ асуултын хэрэгсэл бол profiler: програм ажиллах явцад функц бүрийг хэдэн удаа дуудсан, нийт хэр хугацаа зарцуулсныг бүртгэдэг хэрэгсэл. Python-ий стандарт санд cProfile гэдэг profiler бий. Нэр дэх “c” нь түүнийг C хэлээр бичигдсэнийг илтгэнэ: ингэснээр бүртгэлийн ажил програмыг аль болох бага удаашруулна. Нэмэлт суулгах юм байхгүй, Python-той хамт ирдэг.
Энэ хичээлд бид санаатайгаар удаан бичсэн тайлангийн script-ийг cProfile-оор ажиллуулна. Profiler-ийн гаралт бол эхлээд айдас төрүүлэм хүснэгт: зургаан багана, хэдэн зуун мөр, англи товчлолууд. Бид түүнийг багана бүрээр нь Монгол хэлээр уншиж, хоёр өөр эрэмбэлэлтээр хэрхэн харахыг сурна. Дараа нь програмын хугацааны 84 хувийг эзэлдэг ганц халуун цэг-ийг олж, засаад, өмнөх хичээлийн хэмжилтийн загвараар үр дүнг шалгана.
Өмнөх хичээлд бид эмчийн зүйрлэлийг хэрэглэсэн: timeit бол даралтын аппарат, тодорхой нэг зүйлийг хэмжинэ. cProfile бол цусны бүрэн шинжилгээ. Эмч ганц үзүүлэлт биш, хэдэн арван үзүүлэлтийг нэг дор авч, хүснэгт болгон гаргадаг. Ихэнх үзүүлэлт хэвийн хязгаарт байна. Эмчийн ур чадвар нь тэр хүснэгтээс хэвийн бус ганц хоёр мөрийг хурдан олоход оршдог.
Хоёр дахь зүйрлэл илүү нарийн. Байгууллагын төсвийн тайланг төсөөлөөд үзээрэй. Яамны нийт зардал 100 тэрбум төгрөг. Тэр зардал газар, хэлтэс, нэгжүүдэд хуваагдана. Хоёр өөр асуулт асууж болно. Нэгдүгээрт: "энэ газрын нийт зардал хэд вэ, түүний харьяа бүх хэлтсийг оруулаад?" Хоёрдугаарт: "энэ газар өөрөө, захиргааныхаа ажилд, хэд зарцуулсан бэ, харьяа хэлтсүүдийг хасаад?" Эхний асуулт мөнгө хаагуур урсаж байгааг харуулна: яам, газар, хэлтэс гэсэн замаар доош явж хамгийн том урсгалыг дагана. Хоёр дахь асуулт мөнгө хаана бодитоор зарцуулагдаж байгааг харуулна.
cProfile-ийн хүснэгт яг энэ хоёр асуултад хариулах хоёр баганатай: cumtime нь нийт зардал, дуудсан бүх функцийг оруулаад. tottime нь өөрийн зардал, дуудсан функцүүдийг хасаад. Хоёуланг нь ойлгох нь profiler уншихын гол түлхүүр.
slow_report.py файл үүсгэ. Энэ script захиалгуудыг үүсгэж, хэрэглэгч бүрийн нийт худалдан авалтыг тооцож, захиалга бүрийн мөрөнд тухайн хэрэглэгчийн зэрэглэлийг харуулсан тайлан бүтээнэ:
python
import random
def make_orders(n, seed=42):
rng = random.Random(seed)
customers = [f"Хэрэглэгч-{i}" for i in range(500)]
return [
{"id": i, "customer": rng.choice(customers), "amount": rng.randint(1_000, 200_000)}
for i in range(n)
]
def format_amount(amount):
return f"{amount:,}₮"
def customer_totals(orders):
totals = {}
for order in orders:
totals[order["customer"]] = totals.get(order["customer"], 0) + order["amount"]
return totals
def rank_of(customer, totals):
ranking = sorted(totals, key=totals.get, reverse=True)
return ranking.index(customer) + 1
def build_report(orders):
totals = customer_totals(orders)
lines = []
for order in orders:
rank = rank_of(order["customer"], totals)
amount = format_amount(order["amount"])
lines.append(f"{order['id']:>6} {order['customer']:<16} {amount:>12} #{rank}")
return lines
def main():
orders = make_orders(20_000)
lines = build_report(orders)
print(len(lines), "мөр")
print(lines[0])
if __name__ == "__main__":
main()Бүх хэсэг танил. random.Random(seed) нь Хичээл 3.4-ийн адил давтагдах санамсаргүй өгөгдөл үүсгэнэ. customer_totals нь Бүлэг 3-ын dict.get загвар. rank_of нь Хичээл 3.2-ын key= эрэмбэлэлт: хэрэглэгчдийг нийт дүнгээр нь буурахаар эрэмбэлж, тухайн хэрэглэгчийн байрлалыг олно. Эхлээд ердийнхөөр ажиллуулъя:
python slow_report.py20000 мөр
0 Хэрэглэгч-327 102,486₮ #148Зөөврийн компьютер дээр энэ хэдэн секунд ажиллана, 20,000 мөрийн тайланд хэтэрхий удаан. Өмнөх хичээлийн загварын эхний алхам: таамаглал. Таамаглал хийж үзээрэй, үргэлжлүүлэн уншихаас өмнө. Олон хүн make_orders-ийг сэжиглэдэг: санамсаргүй тоо үүсгэх "хүнд" сонсогддог. Зарим нь f-string форматлалтыг, 20,000 удаа хийгддэг учраас. Одоо хэмжье.
python -m cProfile -s tottime slow_report.py-m cProfile нь profiler-ийг програм мэт ажиллуулж, бидний script-ийг түүний дотор ажиллуулна. -s tottime нь хүснэгтийг tottime багана буюу функцийн өөрийн хугацаагаар эрэмбэлэхийг заана. Script-ийн өөрийн гаралтын дараа хүснэгт гарна. Python 3.13 бүхий нэг компьютер дээрх нэг ажиллуулалт, эхний мөрүүд:
20000 мөр
0 Хэрэглэгч-327 102,486₮ #148
341872 function calls in 1.912 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
20000 1.598 0.000 1.598 0.000 {built-in method builtins.sorted}
20000 0.149 0.000 0.149 0.000 {method 'index' of 'list' objects}
1 0.021 0.021 1.803 1.803 slow_report.py:28(build_report)
40000 0.019 0.000 0.034 0.000 random.py:242(_randbelow_with_getrandbits)
20000 0.012 0.000 1.759 0.000 slow_report.py:23(rank_of)
20000 0.011 0.000 0.043 0.000 random.py:341(choice)
20000 0.010 0.000 0.038 0.000 random.py:332(randint)
20000 0.008 0.000 0.008 0.000 slow_report.py:14(format_amount)
...Таны тоонууд, мөрийн дугаарууд, ялангуяа random.py-ийн мөрийн дугаарууд Python-ий хувилбараас хамаарч өөр байна. Харьцаанууд ойролцоо байх ёстой.
Эхний мөр: “341872 function calls in 1.912 seconds”, “1.912 секундэд 341,872 функц дуудагдсан”. Програм ердөө хэдэн арван мөр, гэвч гурван зуун мянга гаруй дуудлага хийсэн. random модулийн дотоод функцүүд, dict-ийн get, list-ийн append бүгд тоологдсон.
“Ordered by: internal time”, "дотоод хугацаагаар эрэмбэлсэн": internal time бол tottime-ийн өөр нэр.
Одоо зургаан багана:
ncalls — “number of calls”, дуудлагын тоо. Функцийг програмын туршид хэдэн удаа дуудсан. sorted 20,000 удаа дуудагдсан: захиалга бүрт нэг удаа. Recursive функцэд энэ багана “3/1” хэлбэртэй байж болно: Хичээл 3.3-ын fib-ийг профайллавал “2692537/1” гэх мэт харагдана. Зүүн тал нь нийт дуудлага, баруун тал нь primitive буюу гаднаас ирсэн, өөрөөсөө биш, дуудлага.
tottime — “total time”, гэвч утга нь өөрийн хугацаа: функцийн биеийн кодыг ажиллуулахад зарцуулсан хугацаа, түүний дуудсан бусад функцүүдийг хасаад. Нэр нь төөрөгдүүлэмхий: “total” гэсэн үгтэй ч нийт биш. Зүйрлэлийн "газар өөрөө, харьяа хэлтсүүдийг хасаад".
percall (эхнийх) — tottime-ийг ncalls-д хуваасан: нэг дуудлагын өөрийн хугацаа.
cumtime — “cumulative time”, хуримтлагдсан хугацаа: функц өөрөө ба түүний дуудсан бүх функцүүд, тэдгээрийн дуудсан функцүүд гэх мэтээр бүгдийг оруулаад. Зүйрлэлийн "газрын нийт төсөв, харьяа бүх хэлтсийг оруулаад".
percall (хоёр дахь) — cumtime-ийг primitive дуудлагын тоонд хуваасан.
filename:lineno(function) — функц хаана тодорхойлогдсон: файл, мөрийн дугаар, функцийн нэр. slow_report.py:23(rank_of) нь “slow_report.py файлын 23-р мөрөнд тодорхойлогдсон rank_of”. Гоё хаалтанд байгаа мөрүүд, {built-in method builtins.sorted} ба {method 'index' of 'list' objects}, нь C хэлээр бичигдсэн функцүүд: Python-ий дотоод функц, тиймээс файл ба мөрийн дугаар байхгүй.
percall-ийн 0.000
percall баганад олон мөр 0.000 гэж харуулж байгааг анзаараарай. Энэ "үнэгүй" гэсэн үг биш. Хүснэгт тоог гурван орны нарийвчлалтай харуулдаг тул миллисекундээс богино бүх зүйл 0.000 болж дугуйрна. sorted-ийн нэг дуудлагын жинхэнэ хугацааг өөрөө тооцоолъё: 1.598 секундийг 20,000-д хуваавал 0.00008 секунд, өөрөөр хэлбэл 80 микросекунд. Хурдан функцүүдийн хувьд percall баганад бүү итгэ, tottime ба ncalls-аас өөрөө тооцоол.
tottime-ээр эрэмбэлсэн хүснэгтийн эхний мөр шууд хариулна: sorted өөрөө 1.598 секунд зарцуулсан, нийт 1.912 секундийн 84 хувь. Хоёр дахь мөр, list-ийн index, 0.149 секунд буюу 8 хувь. Бусад бүх функц, санамсаргүй тоо үүсгэх, форматлах, dict-ийн get, нийлээд хувь хүрэхгүй. Таамаглалуудыг санаарай: make_orders-ийн random функцүүд нийтдээ 0.1 секунд хүрэхгүй. format_amount 0.008 секунд. Хоёр таамаглал хоёулаа буруу.
Гэвч sorted бол стандарт функц, бид түүнийг өөрчлөхгүй. Асуулт нь: хэн түүнийг 20,000 удаа дуудаж байна вэ? Энд cumtime тусална. Хүснэгтийг өөр эрэмбэлэлтээр ажиллуулъя:
python -m cProfile -s cumtime slow_report.py Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 1.912 1.912 {built-in method builtins.exec}
1 0.000 0.000 1.912 1.912 slow_report.py:1(<module>)
1 0.001 0.001 1.911 1.911 slow_report.py:38(main)
1 0.021 0.021 1.803 1.803 slow_report.py:28(build_report)
20000 0.012 0.000 1.759 0.000 slow_report.py:23(rank_of)
20000 1.598 0.000 1.598 0.000 {built-in method builtins.sorted}
20000 0.149 0.000 0.149 0.000 {method 'index' of 'list' objects}
1 0.007 0.007 0.106 0.106 slow_report.py:4(make_orders)
...cumtime эрэмбэлэлт хугацаа хаагуур урсаж байгааг дээрээс доош харуулна, яг төсвийн яам, газар, хэлтэс гэсэн шиг. exec бол script-ийг ажиллуулж буй Python өөрөө. Дараа нь <module> буюу файлын үндсэн түвшин, main, build_report, rank_of, sorted. Зам тодорхой: build_report нь rank_of-ийг 20,000 удаа дуудаж, rank_of бүр sorted-ийг дуудна.
Хоёр эрэмбэлэлтийг хамт хэрэглэх нь profiler уншихын үндсэн арга. tottime хугацаа хаана бодитоор зарцуулагдаж байгааг олно: энд sorted. cumtime тэр газар хүрэх замыг харуулна: энд build_report → rank_of → sorted. rank_of-ийн мөрийг хараарай: tottime ердөө 0.012, өөрөө бараг юу ч хийдэггүй, гэвч cumtime 1.759. Энэ бол "өөрөө хямд, гэхдээ үнэтэй зүйл дууддаг" функцийн тэмдэг, асуудлын эх үүсвэр ихэвчлэн ийм газар.
Одоо rank_of-ийн кодыг дахин уншъя. Функц дуудагдах бүрт бүх 500 хэрэглэгчийг эрэмбэлж зэрэглэл үүсгэдэг. Гэвч totals dict build_report-ийн туршид огт өөрчлөгддөггүй. Тэгэхээр 20,000 удаагийн эрэмбэлэлт бүгд яг ижил үр дүн гаргасан: ижил ажлыг 19,999 удаа илүү хийсэн. Profiler "энэ функц удаан" гэж хэлсэн, кодын тайлбар "энэ функц хэрэггүй давтагддаг" гэж хэлсэн. Profiler хаана харахыг заадаг, яагаад гэдгийг кодыг уншиж ойлгоно.
Зэрэглэлийг нэг удаа тооцоолж, dict-д хадгалъя:
python
def build_ranks(totals):
ranking = sorted(totals, key=totals.get, reverse=True)
return {customer: position for position, customer in enumerate(ranking, start=1)}
def build_report(orders):
totals = customer_totals(orders)
ranks = build_ranks(totals)
lines = []
for order in orders:
rank = ranks[order["customer"]]
amount = format_amount(order["amount"])
lines.append(f"{order['id']:>6} {order['customer']:<16} {amount:>12} #{rank}")
return linesbuild_ranks эрэмбэлэлтийг ганц удаа хийж, хэрэглэгч бүрийг зэрэглэлтэй нь холбосон dict үүсгэнэ. enumerate(ranking, start=1) нь эхний хэрэглэгчид 1-р зэрэглэл өгнө. Давталт доторх ажил одоо dict-ээс ганц хайлт. rank_of функц хэрэггүй болсон тул устгана. Хэмжилтийн загварын гурав дахь алхам: ганцхан зүйл өөрчилсөн. Profiler-ээр дахин ажиллуулна:
python -m cProfile -s tottime slow_report.py20000 мөр
0 Хэрэглэгч-327 102,486₮ #148
281374 function calls in 0.168 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
40000 0.019 0.000 0.034 0.000 random.py:242(_randbelow_with_getrandbits)
1 0.018 0.018 0.035 0.035 slow_report.py:28(build_report)
20000 0.011 0.000 0.043 0.000 random.py:341(choice)
...Эхний мөр нь өөрчлөгдсөнийг гэрчилнэ. Үр дүн ижил: эхний мөр яг “#148” хэвээр. sorted хүснэгтийн оройгоос алга болсон. Одоо халуун цэг тодорхой бус: хугацаа олон жижиг функцэд жигд хуваагдсан. Энэ бол оновчлолоо дуусгах дохио: нэг мөр давамгайлахгүй бол цааш оновчлох нь бага ашигтай.
Хэмжилтийн загварын шударга мэдээллийн алхам: profiler өөрөө програмыг удаашруулдаг, учир нь функцийн дуудлага бүрийг бүртгэх ажил нэмэгддэг. Тиймээс эцсийн хурдны тоог profiler-гүйгээр, өмнөх хичээлийн аргаар хэмжинэ:
python
import time
from slow_report import build_report, make_orders
orders = make_orders(20_000)
start = time.perf_counter()
build_report(orders)
print(f"{time.perf_counter() - start:.3f} с")Энд if __name__ == "__main__": хамгаалалт ажилласан: slow_report-ийг import хийхэд main() автоматаар ажиллахгүй. Засахаас өмнөх хувилбарт энэ кодыг ажиллуулсан тоо 1.21 секунд, засварын дараа 0.031 секунд, нэг компьютер дээр. Ойролцоогоор 40 дахин хурдан. Profiler-тэй хэмжилт 1.91 секунд байсныг анзаараарай: profiler 1.21 секундийг 1.91 болгосон. Профайлын тоонууд харьцангуй утгатай: аль хэсэг их, аль нь бага. Үнэмлэхүй хурдыг үргэлж profiler-гүйгээр хэмжинэ.
Заримдаа бүтэн script биш, програмын тодорхой хэсгийг профайллах хэрэгтэй. cProfile.Profile нь Бүлэг 6-ын context manager:
python
import cProfile
import pstats
from slow_report import build_report, make_orders
orders = make_orders(20_000)
with cProfile.Profile() as profiler:
build_report(orders)
stats = pstats.Stats(profiler)
stats.sort_stats("tottime").print_stats(5)with блок дотор ажилласан код л бүртгэгдэнэ: make_orders хэмжилтэд орохгүй, яг timeit-ийн setup шиг. pstats модуль хүснэгтийг боловсруулна: sort_stats эрэмбэлж, print_stats(5) эхний таван мөрийг л хэвлэнэ. Хэдэн зуун мөрийн хүснэгтэд энэ хязгаар маш хэрэгтэй.
Шударга тэмдэглэл: Python 3.15 стандарт санд шинэ sampling profiler нэмсэн. Энэ төрлийн profiler дуудлага бүрийг бүртгэхийн оронд програмыг тогтмол хугацааны завсараар "харж", аль функц ажиллаж байгааг тоолдог: хамаагүй бага удаашруулдаг тул ажиллаж буй том програмд тохиромжтой. Энэ сургалт Python 3.12-оос хойшхи бүх хувилбар дээр ажиллах ёстой тул бид cProfile-ийг үндсэн хэрэгсэл болгоно. Түүний хүснэгтийн ухагдахуунууд, өөрийн ба хуримтлагдсан хугацаа, бүх profiler-т ижил.
Хүсвэл Хичээл 3.3-ын memo-гүй fib(25)-ийг cProfile-оор ажиллуулж, ncalls баганын "нийт/primitive" хэлбэрийг өөрийн нүдээр харж үзээрэй. Дараа нь lru_cache-тэй хувилбарыг профайллаж хоёр хүснэгтийг харьцуулаарай. Сонирхвол Хичээл 3.4-ийн log боловсруулагчийн analyze.py-г профайллаж, хугацааны ихэнх нь файл унших, мөр задлах, тоолох гурвын алинд зарцуулагдаж байгааг олж болно. Таамаглалаа эхлээд бичээрэй. Мөн slow_report.py-ийн захиалгын тоог 40,000 болгож, засахаас өмнөх ба дараах хувилбаруудын хугацаа хэрхэн өсч байгааг өмнөх хичээлийн хэмжээг өөрчлөх аргаар шалгах нь сонирхолтой. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
cProfile.py: error: option -s: invalid choice: ‘total’. “-s тохиргооны буруу сонголт” гэсэн утгатай, мессежийн үргэлжлэлд зөвшөөрөгдсөн сонголтуудын жагсаалт гарна. Эрэмбэлэлтийн нэрийг буруу бичихэд гарна. Хамгийн их хэрэглэгдэх хоёр нь tottime ба cumtime. ncalls дуудлагын тоогоор эрэмбэлнэ.
AttributeError: module ‘random’ has no attribute ‘Random’ (consider renaming ‘...random.py’ since it has the same name as the standard module named ‘random’ and prevents importing that standard module). “random модульд Random гэдэг атрибут байхгүй, таны random.py файл стандарт модультай ижил нэртэй тул түүнийг нуусан” гэсэн утгатай. Туршилтын файлаа стандарт модулийн нэрээр, random.py, profile.py, time.py гэх мэт, нэрлэхэд гарна: Python таны файлыг стандарт модулийн оронд import хийнэ. Энэ тодорхой зөвлөмж Python 3.13-аас нэмэгдсэн. Хуучин хувилбаруудад мессеж нь зөвхөн эхний хэсэг байдаг тул шалтгааныг олоход хэцүү. Шийдэл: файлаа measure_random.py шиг өөр нэрээр нэрлэ.
Хамгийн их дуудагдсан функцийг оновчлох. Мессежгүй, хүснэгт буруу унших алдаа. format_amount 20,000 удаа дуудагдсан, гэвч нийтдээ 0.008 секунд, програмын хагас хувиас бага. Түүнийг хоёр дахин хурдан болгосон ч програм мэдрэгдэхүйц хурдлахгүй. ncalls өндөр байх нь сэжиг төрүүлж болно, гэвч шийдвэрийг tottime ба cumtime-аар гаргана.
tottime-ийг нийт хугацаа гэж ойлгох. Мессежгүй. Нэр нь “total” гэсэн үгтэй тул rank_of-ийн tottime 0.012-ийг хараад “rank_of хурдан” гэж дүгнэх амархан. Гэвч rank_of-ийн cumtime 1.759: функц өөрөө хурдан, түүний дуудсан зүйл удаан. Эхний хүснэгтэд л харсан бол халуун цэг sorted гэж олдох ч засах газар rank_of байсныг cumtime л харуулсан.
Profiler-тэй хугацааг хурдны тоо болгож мэдээлэх. Мессежгүй. Profiler нь олон жижиг Python функц дууддаг кодыг илүү их, C функцүүдэд их хугацаа зарцуулдаг кодыг бага удаашруулдаг. Тиймээс хүснэгт дэх харьцаа ч бага зэрэг гажсан байж болно. Профайлыг хаана харахыг олоход, эцсийн тоог profiler-гүй perf_counter эсвэл timeit-ээр хэмжинэ.
timeit сэжигтэй хэсгийг хэмждэг бол cProfile програмын хугацаа хаана зарцуулагдаж байгааг олдог. python -m cProfile -s tottime script.py нь функц бүрийн дуудлагын тоо ба хугацааг хүснэгт болгон харуулна. ncalls дуудлагын тоо, recursive функцэд нийт/primitive хэлбэртэй. tottime функцийн өөрийн хугацаа, дуудсан функцүүдийг хасаад. cumtime хуримтлагдсан хугацаа, дуудсан бүгдийг оруулаад. percall-ийн 0.000 "үнэгүй" гэсэн үг биш, гурван оронд дугуйрсан. Гоё хаалттай мөрүүд C хэлээр бичигдсэн дотоод функцүүд. tottime-ээр эрэмбэлж хугацаа бодитоор хаана зарцуулагдаж байгааг олж, cumtime-ээр эрэмбэлж тэр газар хүрэх замыг харна. Өөрөө хямд ч cumtime өндөр функц ихэвчлэн засах газар. Profiler хаана харахыг заадаг, яагаад гэдгийг кодыг уншиж ойлгоно. Profiler програмыг удаашруулдаг тул түүний тоо харьцангуй, эцсийн хурдыг profiler-гүйгээр хэмжинэ. Нэг мөр давамгайлахаа больсон бол оновчлол дуусах дохио. cProfile.Profile нь context manager тул програмын тодорхой хэсгийг л профайллаж болно.
Энэ хичээлийн засвар нэг сонирхолтой зүйлийг нуусан. Бид list-ийн index-ийг dict-ийн хайлтаар сольсон бөгөөд index хүснэгтэд 0.149 секунд эзэлж байсан бол dict-ийн хайлт хүснэгтэд ч харагдахгүй болтлоо хурдан. Яагаад? Дараагийн хичээл энэ асуултаас эхэлнэ: Python-д хамгийн их тохиолддог хурдны дөрвөн засвар. Хайлтын хувьд list-ийн оронд set эсвэл dict, олон string нийлүүлэхэд join, том өгөгдөлд generator, давтагдах тооцоололд cache. Тус бүрийг өмнө ба дараа хэмжиж, хэзээ ашигтай, хэзээ ашиггүйг тоогоор харна.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.