Ачааллж байна...
Өмнөх хичээлд бид with-ийг хэрэглэгчийн талаас бүрэн ойлгосон: __enter__ дуудагдаж, түүний буцаах утга as-ийн нэрэнд очдог, блок ажилладаг, дараа нь юу ч болсон __exit__ дуудагддаг. Тэнд бид Spy гэдэг жижиг class-аар протоколыг тагнасан ч түүнийг хэрхэн бичихийг тайлбарлаагүй. Энэ хичээлд бид class-аар context manager бичихийг жинхэнээр сурна. Бүлэг 1-д iterator протоколын хэрэглэгч талыг ойлгосны дараа Countdown class бичсэнтэй яг ижил алхам.
Бидний бүтээх хэрэгсэл бол Timer: with блок доторх кодын ажиллах хугацааг хэмждэг context manager. Хичээл 5.1-ийн timed decorator бүхэл функцийг хэмждэг байсан. Гэтэл заримдаа функцийн зөвхөн нэг хэсэг, эсвэл функц огт биш, скриптийн хэдэн мөр л хэмжих шаардлага гардаг. Timer яг үүнд зориулагдсан. Энэ хэрэгсэл энэ сургалтын хэмжилтийн зарчимд шууд үйлчилнэ, мөн Түвшин 2-ын төгсгөлийн бүтээлд бидний хэрэгслийн сангийн нэг хэсэг болно.
Хичээлийн хоёр дахь хагас өмнөх хичээлд хойш тавьсан асуултуудад хариулна. __exit__-ийн гурван argument яг юу агуулдаг вэ? __exit__-ийн буцаах утга юуг шийддэг вэ? Context manager блок доторх алдааг "залгиж", програмыг үргэлжлүүлж болно. Энэ боломж хэзээ хэрэгтэй, хэзээ аюултай болохыг шударгаар харуулна. Энэ хичээлийн хамгийн чухал анхааруулга нь санамсаргүй алдаа залгихтай холбоотой, учир нь тэр нь Python-ий хамгийн хэцүү олддог алдаануудын нэгийг үүсгэдэг.
Серверийн өрөөний харуул руугаа буцъя. Өмнөх хичээлд түүний гол үүрэг нь гарах хүн бүрийг бүртгэх байсан. Одоо түүний ажлын нэг нарийн хэсгийг харъя. Хүн хэвийн гарахад харуул ердөө "гарсан" гэж тэмдэглэнэ. Харин галын дохиогоор гүйж гарсан хүн гарахад харуулд нэмэлт мэдээлэл ирнэ: ямар төрлийн дохио, хаана, ямар нөхцөлд. Харуул энэ мэдээллийг дэвтэртээ бичнэ.
Дараа нь харуул шийдвэр гаргах ёстой. Ихэнх тохиолдолд тэр дохиог дээш дамжуулна: "өрөөнд гал гарлаа, барилгын удирдлага мэдэх ёстой". Гэвч заримдаа харуул дохиог өөрөө шийдэж чадна. Жишээлбэл, өрөөнд зориулсан тусгай дохио "агааржуулагчийн шүүлтүүр солих хугацаа болсон" гэж дуугарвал харуул түүнийг тэмдэглээд, хэнд ч дамжуулалгүйгээр "энд шийдэгдсэн" гэж хаана. Энэ эрх мэдэл ашигтай ч аюултай. Хэрэв харуул бүх дохиог "шийдэгдсэн" гэж хаадаг болбол жинхэнэ гал хэзээ ч удирдлагад хүрэхгүй.
__exit__ бол тэр харуулын гарах үеийн ажил. Түүний гурван argument нь дохионы мэдээлэл. Түүний буцаах утга нь шийдвэр: None эсвэл False бол "дээш дамжуул", True бол "энд шийдэгдсэн". Зүйрлэлийн анхааруулга бүрэн хүчинтэй.
Хамгийн энгийн хувилбараас эхэлье.
python
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.elapsed = time.perf_counter() - self.start
print(f"Хугацаа: {self.elapsed:.4f} секунд")
with Timer():
total = sum(x * x for x in range(1_000_000))
print(total)Миний компьютер дээрх нэг ажиллуулалт:
Хугацаа: 0.0583 секунд
333332833333500000Хоёр method-ыг уншъя. __enter__ эхлэх цагийг self.start-д хадгалж, self-ийг буцаана. Яагаад self-ийг буцаадгийг удахгүй харна. __exit__ дуусах цагаас эхлэх цагийг хасаж, self.elapsed-д хадгалаад хэвлэнэ. Гурван argument-ыг одоохондоо хэрэглээгүй ч заавал бичих ёстой, учир нь Python __exit__-ийг үргэлж гурван утгатай дууддаг. Нэрс нь уламжлалт: exc_type, exc_value, traceback. Энгийн нэрээр бичиж болох ч эдгээр нэрийг Python баримт бичиг ба бусдын код ашигладаг тул ингэж бичих нь зүйтэй. Блок дотор Хичээл 2.2-ын generator expression-оор сая квадратын нийлбэрийг бодсон. Таны хугацаа өөр байх нь хэвийн.
Энэ class-д __init__ огт байхгүйг анзаараарай. Timer-д үүсэх үед тохируулах зүйл байхгүй, бүх ажил with эхлэх ба дуусах үед хийгдэнэ. Хэмжилтийн хамгийн чухал зарчим энд биелэгдэж байна: эхлэх цагийг объект үүсэх үед биш, блок эхлэх мөчид, __enter__ дотор авдаг. Эс тэгвээс Timer үүсгэх ба with эхлэх хоёрын хоорондох хугацаа хэмжилтэд орох байсан.
Сайжруулалт: шошго ба as
Одоо хоёр хэрэгтэй боломж нэмье. Нэгдүгээрт, олон хэмжилтийг ялгахын тулд шошго. Хоёрдугаарт, хэмжсэн хугацааг блокийн дараа кодоор ашиглах боломж.
python
import time
class Timer:
def __init__(self, label="Блок"):
self.label = label
self.elapsed = None
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.elapsed = time.perf_counter() - self.start
print(f"{self.label}: {self.elapsed:.4f} секунд")
with Timer("Жагсаалт бүтээх") as t:
squares = [x * x for x in range(1_000_000)]
print(f"Нэг элементэд: {t.elapsed / len(squares) * 1e9:.1f} наносекунд")Жагсаалт бүтээх: 0.0421 секунд
Нэг элементэд: 42.1 наносекунд__init__ шошгыг хадгалж, elapsed-ийг None-оор эхлүүлнэ. None нь "хараахан хэмжигдээгүй" гэсэн тодорхой утга. Одоо as t хэрэгтэй боллоо: __enter__ self-ийг буцаадаг тул t нь Timer объект өөрөө. Блок дууссаны дараа t.elapsed хэмжсэн хугацааг агуулна. Өмнөх хичээлийн “as-ийн утга нь __enter__-ийн буцаасан зүйл” гэсэн дүрэм энд шийдвэрлэх: Timer өөрийгөө буцаадаг учир хэрэглэгч хэмжилтийн үр дүнд хүрэх боломжтой. 1e9 нь тэрбум буюу секундээс наносекунд руу хөрвүүлэх үржүүлэгч.
Trace: төлөв self дээр хэрхэн хуримтлагддаг вэ
Алхам 1 (Timer("Жагсаалт бүтээх")). Объект үүсч, __init__ ажиллана. Объектод: label = "Жагсаалт бүтээх", elapsed = None.
Алхам 2 (__enter__). with manager-ийн __enter__-ийг дуудна. Объектод: label, elapsed = None, start = 1824.5631 (perf_counter-ийн утга, ямар нэг дурын эхлэлээс тоологдсон секунд; зөвхөн ялгавар нь утгатай).
Алхам 3 (as t). __enter__ self-ийг буцаасан тул t нэр ижил объектыг заана.
Алхам 4 (блок). Жагсаалт бүтээгдэнэ. Объект өөрчлөгдөхгүй.
Алхам 5 (__exit__). Блок хэвийн дууссан тул гурван None argument-тай дуудагдана. Объектод: label, start = 1824.5631, elapsed = 0.0421. Хэвлэлт хийгдэнэ. __exit__ юу ч буцаагаагүй буюу None.
Алхам 6 (блокийн дараа). t нэр хэвээр объектыг заасаар, t.elapsed = 0.0421.
Энэ trace-ийг Бүлэг 4-ийн closure-уудтай харьцуулбал: closure төлөвийг үүргэвчин доторх хайрцагт хадгалдаг байсан бол class-аар бичсэн context manager төлөвийг self дээрх attribute-д хадгалдаг. __enter__ ба __exit__ хоёр тусдаа method тул тэдгээр мэдээллээ хуваалцах газар хэрэгтэй, тэр нь self. Дараагийн хичээлд ижил Timer-ийг generator-оор бичихэд энэ төлөв огт өөр газар, generator-ийн хөлдсөн frame-д амьдрах болно.
Жишээ 1: бодит хэмжилт ба давтан ашиглалт
Timer-ийг Хичээл 3.4-ийн log боловсруулагч дээр хэрэглэж, тайлангийн хэсэг бүрийн хугацааг тусад нь хэмжье. Энэ нь функцийг өөрчлөхгүйгээр, аль хэсэг удаан байгааг олох хамгийн хурдан арга.
python
from log_tools import build_records, count_levels, first_errors, slowest
with Timer("count_levels"):
count_levels(build_records(["app.log"]))
with Timer("slowest"):
slowest(build_records(["app.log"]))
with Timer("first_errors"):
first_errors(build_records(["app.log"]))Миний компьютер дээрх нэг ажиллуулалт:
count_levels: 1.6104 секунд
slowest: 1.8732 секунд
first_errors: 0.0001 секундХичээл 3.4-т бид тайлангийн нийт хугацааг л хэмжсэн. Одоо хэсэг бүрийн хувь нэмэр тодорхой: эхний хоёр бүтэн файлыг уншдаг тул тус бүр 1.6-1.9 секунд, first_errors эрт зогсдог тул бараг 0. slowest-ийн нэмэлт хугацаа heapq-ийн харьцуулалтаас ирдэг. Ийм нарийн хэмжилт Бүлэг 12-т удаан скриптийг хурдасгахад бидний гол хэрэгсэл болно. Тоонууд таны компьютер дээр өөр байна.
Нэг Timer объектыг дараалан хэд хэдэн удаа ашиглаж ч болно, учир нь __enter__ бүрт start шинэчлэгддэг:
python
t = Timer("Давталт")
for size in [10_000, 100_000, 1_000_000]:
with t:
sum(range(size))Давталт: 0.0001 секунд
Давталт: 0.0012 секунд
Давталт: 0.0118 секундХэмжээ арав дахин өсөх бүрт хугацаа арав дахин өсч байна. Нэг шударга хязгаарлалт: ижил Timer объектыг үүрлэж, нэг with дотор дахин with t гэж хэрэглэвэл дотоод __enter__ start-ийг дарж бичих тул гадна хэмжилт буруу болно. Үүрлэсэн хэмжилтэд тус бүрт шинэ Timer үүсгэнэ.
Жишээ 2: __exit__-ийн гурван argument
Одоо блок дотор алдаа гарахад юу болохыг харъя. __exit__-д гурван argument-ыг хэвлэдэг түр мөр нэмж, тэгээр хуваая:
python
class Timer:
def __init__(self, label="Блок"):
self.label = label
self.elapsed = None
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.elapsed = time.perf_counter() - self.start
print(" exc_type: ", exc_type)
print(" exc_value:", repr(exc_value))
print(" traceback:", traceback)
print(f"{self.label}: {self.elapsed:.4f} секунд")
try:
with Timer("Хуваах"):
result = 10 / 0
except ZeroDivisionError:
print("Гадна талд алдаа барьсан") exc_type: <class 'ZeroDivisionError'>
exc_value: ZeroDivisionError('division by zero')
traceback: <traceback object at 0x7f6d3a2b1c40>
Хуваах: 0.0000 секунд
Гадна талд алдаа барьсанГурван argument-ыг уншъя. exc_type бол алдааны класс: ZeroDivisionError. Энэ нь except ZeroDivisionError: бичихэд хэрэглэдэг тэр нэр. exc_value бол алдааны объект өөрөө: except ZeroDivisionError as error: гэж бичихэд error-д ирдэг тэр зүйл, “division by zero” гэсэн мессежтэй. traceback бол алдааны гарсан газрын бүрэн мэдээлэл агуулсан тусгай объект. Python алдааны traceback хэвлэхдээ яг энэ объектыг ашигладаг. Блок хэвийн дуусахад гурвуулаа None байдгийг өмнөх хичээлээс мэднэ. Тиймээс if exc_type is None: бол хамгийн түгээмэл шалгалт: "блок амжилттай дууссан уу?"
Хоёр чухал ажиглалт. Нэгдүгээрт, хэмжилт хэвлэгдсэн: алдаа гарсан ч Timer ажлаа хийсэн. Хоёрдугаарт, __exit__ юу ч буцаагаагүй тул алдаа гадагш гарч, гадна талын except түүнийг барьсан. Timer алдааг зүгээр л ажигласан, шийдвэрт оролцоогүй.
Энэ мэдээллээр Timer-ийг илүү ухаалаг болгоё: амжилттай ба амжилтгүй хэмжилтийг ялгаж хэвлэх.
python
def __exit__(self, exc_type, exc_value, traceback):
self.elapsed = time.perf_counter() - self.start
if exc_type is None:
print(f"{self.label}: {self.elapsed:.4f} секунд")
else:
print(
f"{self.label}: {self.elapsed:.4f} секунд "
f"(алдаатай дууслаа: {exc_type.__name__})"
)Одоо хэмжилтийн гаралт алдаатай гүйлтийг тод ялгана. Алдаагаар зогссон блокийн хугацаа ихэвчлэн бодит ажлын хугацаа биш тул түүнийг хэвийн хэмжилттэй андуурах нь буруу дүгнэлтэд хүргэнэ. Энэ бол Timer-ийн эцсийн хувилбар бөгөөд Түвшин 2-ын бүтээлд бид түүнийг хэрэглэнэ.
Жишээ 3: алдааг залгих — __exit__ True буцаах
Одоо харуулын эрх мэдэл. __exit__ үнэн гэж тооцогдох утга, тухайлбал True буцаавал Python алдааг залгина: алдаа гадагш гарахгүй, програм with блокийн дараагийн мөрөөс хэвийн үргэлжилнэ. Энэ эрх мэдлийг маш нарийн, зөвхөн тодорхой төрлийн алдаанд хэрэглэх ёстой. Сайн жишээ бол "файл байгаа бол устга, байхгүй бол юу ч болохгүй" гэсэн ажил:
python
import os
class IgnoreMissing:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None and issubclass(exc_type, FileNotFoundError):
print(f" Алгаслаа: {exc_value}")
return True
return False
with IgnoreMissing():
os.remove("old_report.txt")
print("Програм үргэлжилж байна") Алгаслаа: [Errno 2] No such file or directory: 'old_report.txt'
Програм үргэлжилж байнаos.remove() файлыг устгадаг бөгөөд файл байхгүй бол FileNotFoundError шиддэг. __exit__ алдааны төрлийг шалгаж байна. issubclass() нь нэг класс нөгөө классын төрөл мөн эсэхийг хэлдэг. FileNotFoundError эсвэл түүнээс удамшсан алдаа бол мэдээ хэвлээд True буцааж, алдааг залгина. Бусад бүх тохиолдолд False буцааж, алдааг дээш дамжуулна. False ба None ижил үр дүнтэй боловч False бичих нь "би санаатайгаар дамжуулж байна" гэдгийг уншигчид тодорхой хэлнэ.
Хэрэв файл байгаа бол os.remove амжилттай устгаж, __exit__ гурван None-тай дуудагдаж, False буцааж, юу ч болохгүй. Хэрэв өөр алдаа гарвал, жишээлбэл файлыг устгах эрхгүй бол PermissionError, тэр алдаа залгигдахгүй, гадагш гарна. Энэ нь яг хүссэн зан чанар: "байхгүй" бол хүлээн зөвшөөрөгдөх, "эрхгүй" бол жинхэнэ асуудал.
Шударгаар хэлэхэд, стандарт сангийн contextlib модульд энэ ажлыг хийдэг бэлэн хэрэгсэл бий: contextlib.suppress(FileNotFoundError). Бодит кодонд түүнийг хэрэглэх нь зүйтэй. Бид IgnoreMissing-ийг бичсэн нь suppress-ийн ард юу байгааг ойлгохын тулд: тэр яг ийм __exit__, ердөө төрлийн жагсаалттай.
Жишээ 4: түр өөрчлөлтийг заавал буцаах
Context manager-ийн бас нэг түгээмэл хэрэглээ бол ямар нэг тохиргоог түр өөрчилж, блок дуусахад анхны утгыг нь заавал сэргээх. Тестэд, эсвэл програмын нэг хэсэгт өөр тохиргоо хэрэгтэй үед энэ нь чухал.
python
class TemporarySetting:
def __init__(self, settings, key, value):
self.settings = settings
self.key = key
self.value = value
def __enter__(self):
self.old_value = self.settings[self.key]
self.settings[self.key] = self.value
return self.settings
def __exit__(self, exc_type, exc_value, traceback):
self.settings[self.key] = self.old_value
return False
config = {"page_size": 20, "lang": "mn"}
try:
with TemporarySetting(config, "page_size", 500):
print("Блок дотор:", config["page_size"])
raise RuntimeError("экспорт тасарлаа")
except RuntimeError as error:
print("Алдаа:", error)
print("Блокийн дараа:", config["page_size"])Блок дотор: 500
Алдаа: экспорт тасарлаа
Блокийн дараа: 20__enter__ хуучин утгыг хадгалж, шинийг тавина. __exit__ хуучнаа буцааж тавина. Блок алдаагаар тасарсан ч __exit__ ажиллаж, page_size 20 болж сэргэсэн. Хэрэв энэ with биш энгийн хоёр мөр оноолт байсан бол алдааны дараа тохиргоо 500 хэвээр үлдэж, програмын бусад хэсэг ч 500 мөрийн хуудас харуулах байсан. Ийм "түр өөрчлөлт буцаагдаагүй" алдаанууд олоход маш хэцүү, учир нь шинж тэмдэг нь алдаа гарсан газраас огт өөр газар илэрдэг. Python-ий стандарт санд ижил санаатай бэлэн context manager-ууд бий: тестийн unittest.mock.patch, мөн Python 3.11-ээс хойш одоогийн хавтсыг түр солих contextlib.chdir.
Хүсвэл Timer-т threshold гэсэн тохиргоо нэмж, хугацаа тэр хязгаараас хэтэрсэн үед л хэвлэдэг болгож болно. Удаан хэсгүүдийг шүүхэд хэрэгтэй. Сонирхвол IgnoreMissing-ийг ерөнхийлж, Ignore(FileNotFoundError, KeyError) гэж олон төрөл хүлээн авдаг болгоод, __init__-д *exception_types ашиглаж, issubclass-д tuple өгч болно. issubclass нь tuple хүлээн авдаг. Мөн TemporarySetting-ийн __exit__-д return False-ийг return True болгоод Жишээ 4-ийг ажиллуулж, юу өөрчлөгдөхийг харах нь дараагийн хэсгийн анхааруулгыг өөрийн нүдээр батлах туршилт. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
Алдаа чимээгүй алга болох. Мессежгүй, энэ хичээлийн хамгийн аюултай алдаа. __exit__ санамсаргүй үнэн утга буцаахад гарна. Хамгийн түгээмэл хувилбар нь Timer-т "хугацааг буцаавал хэрэгтэй байх" гэж бодоод return self.elapsed гэж бичих. 0.0421 гэсэн тоо үнэн гэж тооцогддог тул Python блок доторх бүх алдааг залгина. Блок дотор NameError, TypeError, ZeroDivisionError гарсан ч програм юу ч болоогүй мэт үргэлжилж, буруу эсвэл дутуу үр дүн гаргана. Шийдэл нь __exit__-ийн буцаах утгыг үргэлж ухамсартайгаар сонгох: алдааг залгих санаагүй бол юу ч буцаахгүй, эсвэл return False гэж тод бичих. True-г зөвхөн тодорхой, хүлээн зөвшөөрөгдөх алдааны төрөлд, Жишээ 3-ын адил төрлийг шалгасны дараа буцаана.
AttributeError: ‘NoneType’ object has no attribute ‘elapsed’. “NoneType объектод ‘elapsed’ attribute байхгүй” гэсэн утгатай. __enter__-д return self мартсан үед гарна. Method return-гүй дуусвал None буцаадаг тул with Timer() as t: дахь t нь None болно. Блокийн дараа t.elapsed хандахад энэ алдаа гарна. Timer хэвлэлтээ хийсэн тул хэмжилт ажилласан мэт харагдах нь будлиан нэмнэ. Шийдэл: as-тай хэрэглэгдэх context manager-ийн __enter__ хэрэгтэй утгаа заавал буцаана.
TypeError: Timer.exit() takes 1 positional argument but 4 were given. “Timer.exit() нэг байрлалын argument авдаг боловч 4 ирлээ” гэсэн утгатай. __exit__-ийг def __exit__(self): гэж гурван argument-гүйгээр бичсэн үед гарна. Python түүнийг үргэлж self-ээс гадна гурван утгатай дууддаг, нийт дөрөв. Гурван argument-ыг хэрэглэхгүй ч заавал бичих ёстой.
AttributeError: ‘Timer’ object has no attribute ‘elapsed’. “Timer объектод ‘elapsed’ attribute байхгүй” гэсэн утгатай. __init__-гүй анхны Timer хувилбарт блок дотроос t.elapsed хандахад гарна. elapsed зөвхөн __exit__ дотор үүсдэг бөгөөд блок дуусахаас өмнө __exit__ дуудагдаагүй. Манай эцсийн хувилбар __init__ дотор elapsed = None гэж эхлүүлдэг тул алдааны оронд None буцаана. Энэ нь attribute-ыг үргэлж __init__ дотор эхлүүлэх сайн дадлын нэг шалтгаан.
TypeError: ‘Timer’ object does not support the context manager protocol. “Timer объект context manager протоколыг дэмждэггүй” гэсэн утгатай. Шинэ Python хувилбарууд ард нь аль method дутуу байгааг бичдэг, жишээлбэл “(missed exit method)”. Method-ын нэрийг буруу бичсэн үед гарна: __exit, _exit_, __enter. Хичээл 1.4-ийн __iter__ алдаатай ижил: Python яг тодорхой нэрийг хайдаг, ойролцоог танихгүй.
Class-аар context manager бичихэд хоёр method хэрэгтэй. __enter__ блок эхлэхэд ажиллаж, as-ийн нэрэнд очих утгыг буцаадаг. Ихэнхдээ self, ингэснээр хэрэглэгч блокийн дараа үр дүнд хүрнэ. __exit__ блок хэрхэн дууссанаас үл хамааран ажиллаж, гурван argument хүлээн авдаг: алдааны класс, алдааны объект, traceback, хэвийн дуусвал гурвуулаа None. Төлөв __enter__ ба __exit__-ийн хооронд self дээр хадгалагддаг. __exit__-ийн буцаах утга алдааны хувь заяаг шийддэг: None эсвэл False бол алдаа гадагш гарна, үнэн утга бол алдаа залгигдаж, програм with-ийн дараагаас үргэлжилнэ. Алдааг зөвхөн тодорхой, хүлээн зөвшөөрөгдөх төрөлд, issubclass-аар шалгасны дараа залгина. contextlib.suppress энэ ажлын бэлэн хувилбар. Санамсаргүй үнэн утга буцаах нь бүх алдааг чимээгүй нуудаг аюултай алдаа. Түр өөрчлөлтийг заавал сэргээх нь context manager-ийн бас нэг түгээмэл хэрэглээ.
Энэ хичээлийн Timer хоёр method, __init__, гурван attribute-тай. Ажил нь энгийн боловч бүтэц нь бага зэрэг урт. Хичээл 1.4-ын Countdown class-ийг санаарай: тэнд ч энгийн тоолол хорин мөр шаардаж, дараа нь Бүлэг 2-ын generator түүнийг дөрвөн мөрөнд багтаасан. Дараагийн хичээлд яг ижил зүйл давтагдана. contextlib.contextmanager decorator generator-ийг context manager болгон хувиргадаг бөгөөд Timer гурван мөр болно. yield нь with блокийн "хаалга" болж, Бүлэг 2-ын түр зогсоох машин, Бүлэг 5-ын decorator хоёр нэгдэнэ.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.