Ачааллж байна...
Python-ийн тухай англи хэл дээрх баримт бичиг, Stack Overflow-ийн хариулт, алдааны мессеж уншихад хоёр үг байнга давтагддаг: iterable ба iterator. Course 2-т та энэ хоёр үгийг анх сонсож, ерөнхий утгыг нь мэдсэн. Гэхдээ өдөр тутмын ярианд хүмүүс энэ хоёрыг ихэвчлэн нэг зүйл мэт хольж хэрэглэдэг тул ялгаа нь бүдэг үлдэх нь их. Энэ хичээлийн зорилго бол энэ хоёр үгийг эцэслэн салгаж, тус бүр нь яг юуг хэлж байгааг тодорхой болгох явдал юм. Энэ ялгаа сонирхолтой нарийн ширийн зүйл биш, харин хачин санагдах бодит алдаануудын шалтгаан болдог тул бүрэн ойлгох нь чухал.
Шууд хэлэхэд: iterable гэдэг нь "давтаж гүйж болох зүйл", өөрөөр хэлбэл for loop-д өгч болох аливаа объект юм. List, string, dict, range, file, tuple бүгд iterable. Харин iterator гэдэг нь тэр давталтыг бодитоор гүйцэтгэдэг, "одоо хаана явж байна" гэдгээ санаж, дараагийн элементийг нэг нэгээр нь өгдөг тусгай объект юм. Iterable бол агуулга, iterator бол тэр агуулгыг нэг нэгээр нь гаргаж өгөх ажилтан гэж хэлж болно. Энэ хичээлийн гол баримт бол list нь iterable боловч iterator биш гэдэг. Бид үүнийг Python-оор өөрөөр нь батлуулна.
Энэ хичээлд бид дараагийн хичээлийн гол хэрэгсэл болох iter() ба next() функцийг маш бага, зөвхөн ялгааг харуулахын тулд ашиглана. Тэдгээрийг бүрэн, гүнзгий судлах нь дараагийн хичээлийн ажил. Энд бидний анхаарал зөвхөн нэг асуулт дээр: энэ хоёр үг яагаад өөр өөр зүйлийг хэлдэг вэ?
Ангид шинэ дэвтрүүдээр дүүрэн хайрцаг ирлээ гэж төсөөлөөд үзээрэй. Хайрцаг бол дэвтрүүдийг агуулдаг зүйл, гэхдээ хайрцаг өөрөө дэвтэр тараадаггүй. Багш нэг сурагчийг "хайрцагнаас дэвтрүүдийг нэг нэгээр нь тараа" гэж томилно. Энэ сурагч бол түгээгч юм. Тэр хэдэн дэвтэр өгснөө, хайрцагны аль хэсэгт хүрснээ санаж явдаг. Хүн бүр "дараагийнхыг өгөөч" гэхэд тэр нэгийг гаргаж өгнө. Хайрцаг хоосорвол "дууслаа" гэж хэлнэ.
Python-ий хэлээр бол хайрцаг нь iterable, түгээгч сурагч нь iterator. Энэ зүйрлэлийн хамгийн чухал хэсэг нь энэ: нэг хайрцагнаас хэдэн ч түгээгч томилж болно. Маргааш өөр ангид дэвтэр тараах хэрэгтэй бол хайрцаг дээр шинэ сурагч томилоход тэр эхнээс нь эхэлнэ. Харин нэг удаа ажлаа дуусгасан түгээгч дахин тараахыг хүсэхэд "надад үлдсэн юм алга" гэж л хэлнэ, учир нь түгээгч эхэнд нь эргэж очдоггүй.
Яг ийм учраас нэг list дээр хоёр удаа for loop гүйлгэхэд хоёулаа бүрэн ажилладаг ч зарим объект дээр хоёр дахь loop юу ч хэвлэхгүй. Эхнийх нь хайрцаг, хоёр дахь нь аль хэдийн ажлаа дуусгасан түгээгч. Энэ хичээлийн туршид энэ зураглалыг санаж яваарай.
Эхлээд list-ээс шууд "дараагийн элементийг өгөөч" гэж хүсэж үзье. Python-д "дараагийнхыг өгөөч" гэдэг хүсэлт нь next() функц юм. Хэрэв list iterator байсан бол энэ ажиллах ёстой.
python
numbers = [10, 20, 30]
print(next(numbers))Ажиллуулахад дараах алдаа гарна:
TypeError: 'list' object is not an iteratorPython шууд хэлж байна: “list объект бол iterator биш.” Хайрцагнаас дэвтэр гуйсан шиг л болсон, хайрцаг өөрөө тараадаггүй. Гэтэл бид энэ list-ийг for loop-д олон мянган удаа ашигладаг. Энэ хоёр хэрхэн зэрэг үнэн байж чадах вэ? Хариулт нь: list нь түгээгч биш ч, түгээгч томилж чаддаг. Түгээгч томилох хүсэлт бол iter() функц юм.
python
numbers = [10, 20, 30]
distributor = iter(numbers)
print(distributor)
print(next(distributor))
print(next(distributor))Гаралт нь иймэрхүү байна:
<list_iterator object at 0x7f3a2c1b9e50>
10
20Эхний мөрөнд iter(numbers) нь шинэ объект буцааж, түүний төрөл нь list_iterator гэдгийг Python өөрөө хэлж байна. Хаягийн тоо (0x7f3a...) таны компьютер дээр өөр гарах нь хэвийн. Энэ list_iterator бол бидний түгээгч бөгөөд next()-д хариулж чаддаг. Нэг дуудахад 10-ыг, дахин дуудахад 20-ыг өгсөн нь түүнийг хаана явж байгаагаа санаж байгааг харуулж байна. List өөрөө огт өөрчлөгдөөгүй, түүнд "хаана явж байна" гэсэн мэдээлэл байдаггүй. Байрлалыг зөвхөн iterator санадаг.
Одоо Python-оос эдгээр объектууд ямар чадвартай болохыг шууд асууя. Iterable байх чадвар нь __iter__ гэдэг тусгай method-оор, iterator байх чадвар нь __next__ гэдэг method-оор илэрхийлэгддэг. hasattr() функц нь объект тухайн нэртэй attribute-тай эсэхийг True эсвэл False-оор хариулдаг.
python
numbers = [10, 20, 30]
distributor = iter(numbers)
print(hasattr(numbers, "__iter__"))
print(hasattr(numbers, "__next__"))
print(hasattr(distributor, "__iter__"))
print(hasattr(distributor, "__next__"))True
False
True
TrueЭнэ дөрвөн мөр бол хичээлийн гол санааны нотолгоо юм. List-д __iter__ байна, тиймээс энэ нь iterable, түгээгч томилж чадна. Харин __next__ байхгүй, тиймээс өөрөө тарааж чадахгүй. Iterator-т хоёулаа бий. Энэ нь сонирхолтой баримтыг илтгэж байна: iterator бүр мөн iterable байдаг, гэхдээ iterable бүр iterator биш. Iterator-ийн __iter__ нь өөрийгөө буцаадаг. Түгээгчээс "түгээгч томил" гэвэл "би өөрөө л байна" гэж хариулдаг гэсэн үг. Бид үүнийг дараа нь өөрсдийн class-д бичихдээ дахин харна.
Одоо for loop-ийн нууцыг хагас нээж болно. for loop эхлэхдээ давтах гэж буй объектод iter() дуудаж, түгээгч авдаг. Дараа нь түгээгчээс next()-ээр нэг нэгээр нь элемент авч, түгээгч "дууслаа" гэтэл давтана. Үүнийг нэг жишээн дээр алхам алхмаар мөрдөж үзье.
python
names = ["Бат", "Сараа"]
for name in names:
print(name)
for name in names:
print(name)Гаралт нь:
Бат
Сараа
Бат
СарааСанах ойд юу болж байгааг алхам бүрээр бичвэл:
Алхам 1. names гэдэг list үүснэ. Санах ойд: хоёр элементтэй list. Ямар ч iterator хараахан байхгүй.
Алхам 2. Эхний for loop эхлэхэд Python далдуур iter(names) дуудна. Санах ойд: list хэвээрээ, мөн шинэ list_iterator үүссэн, түүний байрлал 0 дээр байна. Энэ iterator-т бид нэр өгөөгүй, түүнийг зөвхөн for loop өөрөө барьж байна.
Алхам 3. for loop next() дуудаж "Бат"-ыг авна, name хувьсагчид онооно, хэвлэнэ. Iterator-ийн байрлал 1 болно. List өөрчлөгдөөгүй.
Алхам 4. next() дахин дуудагдаж "Сараа"-г авна, хэвлэнэ. Iterator-ийн байрлал 2 болно, энэ нь list-ийн төгсгөл.
Алхам 5. next() дахин дуудагдахад iterator "дууслаа" гэсэн дохио өгнө. for loop энэ дохиог хүлээн аваад чимээгүй зогсоно. Энэ iterator-ыг хэн ч барихаа больсон тул Python түүнийг хаяна. Санах ойд дахин зөвхөн list үлдлээ.
Алхам 6. Хоёр дахь for loop эхлэхэд Python дахин iter(names) дуудна. Энэ бол шинэ list_iterator, байрлал нь 0. Тиймээс "Бат", "Сараа" дахин бүрэн хэвлэгдэнэ.
Энэ trace-ийн хамгийн чухал мөр бол алхам 6. List хоёр дахь удаагаа ажилласан шалтгаан нь list эхэндээ "буцаж очсон" биш, харин шинэ түгээгч томилогдсон явдал юм. Хайрцаг хэзээ ч хоосордоггүй, хоосордог нь түгээгч.
Жишээ 1: zip — аль хэдийн түгээгч
Одоо эсрэг талын тохиолдлыг харъя. Python-ий зарим функц iterable биш, шууд iterator буцаадаг. zip() бол тэдний нэг. Энэ нь хоёр жагсаалтыг хос хосоор нь нийлүүлдэг.
python
pairs = zip(["Бат", "Сараа"], [90, 85])
for pair in pairs:
print(pair)
print("--- дахин ---")
for pair in pairs:
print(pair)('Бат', 90)
('Сараа', 85)
--- дахин ---Хоёр дахь loop юу ч хэвлэсэнгүй, мөн ямар ч алдаа гарсангүй. Үүнийг trace-ээр харъя. zip() дуудагдахад санах ойд zip объект үүснэ. Энэ бол түгээгч өөрөө бөгөөд байрлал нь эхэнд байна. Эхний for loop iter(pairs) дуудахад zip iterator учир өөрийгөө буцаана, шинэ түгээгч үүсэхгүй. Loop хоёр хосыг авч хэвлээд түгээгчийг төгсгөл хүртэл хоосолно. Хоёр дахь for loop дахин iter(pairs) дуудахад тэр ижил, аль хэдийн хоосорсон zip объект өөрийгөө буцаана. Эхний next() шууд "дууслаа" гэж хариулах тул loop нэг ч удаа ажиллалгүй дуусна.
Бид үүнийг шууд шалгаж болно:
python
pairs = zip(["Бат"], [90])
print(iter(pairs) is pairs)
numbers = [1, 2]
print(iter(numbers) is numbers)True
Falseis нь хоёр нэр яг нэг объект руу заадаг эсэхийг шалгадаг. zip-ийн хувьд iter() өөрийгөө буцаасан тул True гарав. List-ийн хувьд iter() шинэ объект үүсгэсэн тул False гарав. Энэ жижиг шалгалт аливаа объект iterator мөн эсэхийг хэлэх найдвартай арга юм. zip-ийг хоёр удаа ашиглах шаардлагатай бол үр дүнг нь list(zip(...))-ээр list болгож хадгалах эсвэл zip-ийг дахин дуудах хэрэгтэй.
Жишээ 2: map ба enumerate — нууцаар түгээгчид
zip ганцаараа биш. map(), filter(), enumerate(), reversed() бүгд iterator буцаадаг. Үүнийг Python-оос шууд асууя.
python
scores = [70, 85, 92]
doubled = map(lambda x: x * 2, scores)
indexed = enumerate(scores)
print(hasattr(doubled, "__next__"))
print(hasattr(indexed, "__next__"))
print(doubled)True
True
<map object at 0x7f3a2c1c4a90>Эхний хоёр мөр эдгээр нь iterator гэдгийг баталж байна. Гурав дахь мөр нь олон хүнийг гайхшруулдаг зүйл юм. map-ийг хэвлэхэд бид [140, 170, 184] гэсэн утгуудыг хүлээж байсан ч Python зөвхөн “map объект” гэж хэлэв. Шалтгаан нь түгээгч хэдэн дэвтэр үлдсэнээ урьдчилан мэддэггүй, асуухад л нэг нэгээр нь бэлтгэдэгт оршино. Утгуудыг харахын тулд түгээгчийг хоослох хэрэгтэй, жишээлбэл list(doubled) гэж.
Энэ "асуухад нь л бэлтгэдэг" чанар бол iterator-ийн том давуу тал бөгөөд дараагийн бүлэгт generator-ийг судлахдаа бид түүнд дахин эргэж ирнэ. Нэг сая элементийг map-аар боловсруулахад нэг сая үр дүн санах ойд нэг дор үүсэхгүй, нэг нэгээр гарч ирээд алга болно. Энэ бол сул тал биш, харин санаатай хийсэн загвар юм.
Жишээ 3: file — нэг удаа уншигддаг түгээгч
Өдөр тутмын ажилд хамгийн их тааралддаг iterator бол нээлттэй файл юм. Таны төслийн хавтаст names.txt гэсэн файл байгаа гэж үзье, дотор нь хоёр мөр байна: "Бат", "Сараа".
python
with open("names.txt", encoding="utf-8") as f:
print(iter(f) is f)
for line in f:
print(line.strip())
print("--- дахин ---")
for line in f:
print(line.strip())True
Бат
Сараа
--- дахин ---Эхний мөр файл объект өөрөө iterator гэдгийг баталж байна. Файлыг нэг удаа гүйхэд унших байрлал файлын төгсгөлд хүрнэ, хоёр дахь loop тэр байрлалаас эхлэх тул юу ч олохгүй. Энэ бол zip-тэй яг ижил зүйл. Файлын хувьд энэ загвар маш утга учиртай. Хэдэн гигабайт log файлыг уншихад бүх мөрийг санах ойд ачаалахгүйгээр мөр мөрөөр урсгаж уншдаг нь яг энэ iterator чанараас үүдэлтэй. Хэрэв файлыг дахин эхнээс нь унших хэрэгтэй бол дахин нээх эсвэл f.seek(0) ашиглан байрлалыг эхэнд нь буцааж болно.
Жишээ 4: range — iterator мэт харагддаг iterable
Сүүлийн жишээ олон хүнийг төөрөгдүүлдэг. range() нь map, zip шиг "асуухад нь л тоо үүсгэдэг", санах ойд бүх тоог хадгалдаггүй. Тиймээс олон хүн түүнийг iterator гэж боддог. Шалгаж үзье.
python
r = range(3)
print(hasattr(r, "__next__"))
for n in r:
print(n)
for n in r:
print(n)False
0
1
2
0
1
2range нь iterator биш, iterable юм. Хоёр loop хоёулаа бүрэн ажилласан нь үүнийг баталж байна. range бол хайрцаг шиг ажилладаг: loop бүр түүнээс шинэ түгээгч авдаг. Энэ жишээний сургамж бол "асуухад нь л утга үүсгэдэг" байх нь iterator байхтай ижил зүйл биш юм. Iterable ч мөн хүссэнээрээ хэмнэлттэй байж чадна. Ялгааг тодорхойлдог зүйл нь ганцхан: объект өөрөө байрлалаа санаж, next()-д хариулдаг уу, эсвэл loop бүрд шинэ түгээгч томилдог уу.
Ялгааг нэгтгэвэл
Iterable бол for loop-д өгч болох аливаа зүйл. Энэ нь __iter__ method-той бөгөөд iter() дуудахад түгээгч буцаана. Ихэнх iterable, тухайлбал list, tuple, string, dict, set, range, дахин дахин давтагдаж болдог, учир нь loop бүр шинэ түгээгч авдаг. Iterator бол байрлалаа санадаг, next()-д хариулдаг түгээгч. Энэ нь __iter__ ба __next__ хоёулаа бүхий бөгөөд iter() дуудахад өөрийгөө буцаана. Iterator нэг удаагийн хэрэглээтэй: хоосорсон бол хоосон хэвээр үлдэнэ. zip, map, filter, enumerate, reversed, нээлттэй файл бүгд iterator юм.
Сонирхвол Python-ий interactive горимд string, dict, set зэрэг хэдэн өөр объект дээр hasattr(x, “next”) болон iter(x) is x шалгалтыг хийж үзэж болно. Таны таамаг хэр олон удаа зөв байх бол? Өөр нэг санаа: enumerate(["а", "б"])-ийг нэг хувьсагчид хадгалаад, list()-ээр хоёр удаа хувиргаж үзээрэй. Хоёр дахь үр дүн юу болох вэ, яагаад гэдгийг энэ хичээлийн зүйрлэлээр тайлбарлаж чадах уу? Эдгээр нь зөвхөн санал бөгөөд дараагийн хичээл үүнийг хийсэн гэж үзэхгүй.
TypeError: ‘list’ object is not an iterator. Энэ мессеж “list объект бол iterator биш” гэсэн утгатай. List, dict, string, range зэрэг iterable-аас шууд next() дуудахад гарна. Шийдэл нь эхлээд iter() дуудаж түгээгч авах: next(iter(numbers)). Ихэнх тохиолдолд next()-ийг гараар дуудах шаардлага огт байдаггүй тул for loop хэрэглэх нь илүү энгийн шийдэл байдаг.
TypeError: ‘int’ object is not iterable. Энэ мессеж “int объект бол iterable биш” гэсэн утгатай. Энэ бол дээрхээс ч илүү суурь алдаа: объект түгээгч томилж ч чадахгүй. Хамгийн түгээмэл шалтгаан нь for i in 5: гэж бичих бөгөөд зөв нь for i in range(5): юм. Мөн функц list биш тоо буцааж байгааг анзаараагүй үед ч гардаг. Хоёр мессежийн ялгааг анхаараарай: “not an iterator” бол “iter() дуудаад үзээрэй”, “not iterable” бол "энэ объектыг ерөөсөө давтах боломжгүй" гэсэн үг.
Хоёр дахь loop чимээгүй хоосон гарах. Энэ бол алдааны мессежгүй алдаа тул хамгийн хэцүү нь. zip, map, filter эсвэл файлыг хувьсагчид хадгалаад хоёр удаа давтахад хоёр дахь удаа юу ч гарахгүй, Python юу ч хэлэхгүй. Програм "ажиллаж" байгаа мэт харагдах ч үр дүн нь буруу. Шийдэл нь хоёр удаа хэрэглэх өгөгдлийг list()-ээр хадгалах эсвэл iterator-ыг дахин үүсгэх юм.
<map object at 0x...> хэвлэгдэх. Энэ нь алдаа биш, харин iterator-ийг утга мэт хэвлэх гэж оролдсоны үр дүн. Утгуудыг харахын тулд list(...) ашигла. Гэхдээ list() iterator-ыг хоосолдог тул дараа нь тэр iterator-ыг дахин хэрэглэх боломжгүй гэдгийг санаарай.
Iterable бол давтаж болох зүйл, iterator бол тэр давталтыг гүйцэтгэдэг, байрлалаа санадаг түгээгч. List бол iterable боловч iterator биш гэдгийг бид next(numbers)-ийн TypeError болон hasattr шалгалтаар баталлаа. for loop бүр далдуур iter() дуудаж шинэ түгээгч авдаг тул list-ийг хэдэн ч удаа давтаж болдог. Харин zip, map, enumerate, файл зэрэг iterator-ууд нэг удаагийн хэрэглээтэй, хоосорвол чимээгүй хоосон үлддэг. Аливаа объект iterator мөн эсэхийг iter(x) is x гэж шалгаж болно.
Энэ хичээлд бид iter() ба next()-ийг зөвхөн ялгааг харуулахын тулд ашигласан. Дараагийн хичээлд бид тэдгээрийг гол дүр болгож, for loop-ийг бүхэлд нь гараараа удирдана. Trace-ийн алхам 5-д дурдсан "дууслаа" гэсэн дохио нь үнэндээ StopIteration гэдэг exception юм. Бид түүнийг нүдээрээ харж, Монгол хэлээр уншиж, for loop түүнийг хэрхэн чимээгүй барьдгийг бүрэн ойлгох болно.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.