Ачааллж байна...
Course 1-ээс хойш та list comprehension-ийг маш олон удаа бичсэн: дөрвөлжин хаалт доторх нэг мөр давталт, жишээлбэл [x * x for x in numbers]. Энэ бичлэг богино, уншихад ойлгомжтой, Python хөгжүүлэгчдийн хамгийн их хайртай хэрэгслүүдийн нэг. Энэ хичээлд бид түүний "төрсөн дүү"-тэй танилцана: generator expression. Бичлэгийн хувьд ялгаа нь ганцхан: дөрвөлжин хаалтын оронд дугуй хаалт. (x * x for x in numbers). Гэвч энэ жижиг өөрчлөлтийн ард огт өөр зан чанар нуугдаж байдаг.
List comprehension бичигдэх мөчид бүх утгыг тооцоолж, тэдгээрийг шинэ list болгон санах ойд нэг дор байрлуулдаг. Generator expression тэгдэггүй. Тэр өмнөх хичээлийн generator функцтэй яг адил, эхэндээ зогссон машин үүсгэдэг бөгөөд хэн нэгэн асуух хүртэл нэг ч утга тооцоолдоггүй. Өөрөөр хэлбэл generator expression бол generator функцийн нэг мөрт товчлол юм. Энэ "асуухад нь л тооцоолох" чанарыг англиар lazy буюу "залхуу" гэж нэрлэдэг. Програмчлалд энэ үг сөрөг утгагүй, харин "шаардлагагүй ажлыг хийдэггүй" гэсэн ухаалаг зан чанарыг хэлдэг.
Энэ хичээл бас нэг шинэ зүйлээр онцлог. Энэ сургалтын зарчим ёсоор хурд эсвэл санах ойн тухай ямар ч мэдэгдэл хэмжилтгүйгээр гарахгүй. Тиймээс “generator expression санах ой хэмнэдэг” гэсэн алдартай мэдэгдлийг бид бодит тоогоор шалгана. Мөн “generator expression үргэлж хурдан” гэсэн түгээмэл буруу ойлголтыг ч хэмжилтээр шударгаар засна. Хэмжилт заримдаа бидний хүлээлтийг батлах бол заримдаа гайхшруулдаг бөгөөд хоёулаа үнэ цэнэтэй.
Хоёр ресторан төсөөлөөд үзээрэй. Эхнийх нь буфет. Үйлчлүүлэгч ирэхээс өмнө тогооч цэсийн бүх хоолыг бэлдэж, урт ширээн дээр нэг дор тавьдаг. Үйлчлүүлэгч ирэхэд бүх зүйл бэлэн, хүссэнээ шууд авна. Гэхдээ ширээ маш их зай эзэлдэг, хэн ч идээгүй хоол ч бэлдэгдсэн байдаг. Хэрэв үйлчлүүлэгч эхний тавагнаас цадаад явчихвал бусад бүх ажил дэмий болно.
Хоёр дахь нь захиалгын ресторан. Тогооч юу ч урьдчилан бэлддэггүй. Үйлчлүүлэгч нэг таваг захиалахад тогооч яг тэр тавгийг хийж өгнө, дараагийнхыг захиалахад дараагийнхыг хийнэ. Гал тогоонд нэг удаад нэг л таваг байх тул зай бараг эзэлдэггүй. Үйлчлүүлэгч эрт цадвал тогооч эрт амарна. Гэхдээ таваг бүрт тусдаа захиалга өгөх нь бага зэрэг нэмэлт хөдөлгөөн шаарддаг.
List comprehension бол буфет: бүх утга урьдчилан бэлэн, санах ойд нэг дор. Generator expression бол захиалгын ресторан: утга бүр асуухад л бэлдэгдэнэ, санах ойд нэг удаад нэг л утга. Энэ зүйрлэлийн сүүлийн өгүүлбэр чухал: захиалга бүр бага зэрэг нэмэлт ажил шаарддаг. Энэ нь яагаад generator expression үргэлж хурдан биш болохыг тайлбарлах бөгөөд бид үүнийг удахгүй хэмжилтээр харна.
Хоёр хэлбэрийг зэрэгцүүлж бичье.
python
numbers = [1, 2, 3, 4]
squares_list = [x * x for x in numbers]
squares_gen = (x * x for x in numbers)
print(squares_list)
print(squares_gen)[1, 4, 9, 16]
<generator object <genexpr> at 0x7f2d4a1b8c10>List comprehension шууд дөрвөн утгатай list болов. Generator expression харин generator объект болов. Python түүний нэрийг <genexpr> гэж бичсэн нь “generator expression”-ий товчлол юм. Өмнөх хичээлд generator функц <generator object countdown ...> гэж хэвлэгдсэнийг санаарай. Энд функцийн нэр байхгүй тул Python энэ тусгай нэрийг өгсөн. Утгуудыг авахын тулд бид түүнээс асуух хэрэгтэй:
python
print(next(squares_gen))
print(next(squares_gen))
print(list(squares_gen))
print(list(squares_gen))1
4
[9, 16]
[]Энэ гаралт өмнөх хоёр бүлгийн бүх мэдлэгийг баталж байна. next() хоёр удаа дуудагдаж, 1 ба 4 гарав. list() үлдсэн хоёр утгыг цуглуулж машиныг дуусгав. Хоёр дахь list() хоосон list буцаав, учир нь generator expression бол бусад generator-ийн нэгэн адил нэг удаагийн хэрэглээтэй iterator.
Trace: "юу ч тооцоологдоогүй" гэдгийг нүдээр харах
"Нэг ч утга тооцоологдоогүй" гэдэг мэдэгдлийг шалгахын тулд тооцоолол бүрт чагнуур тавья. Квадрат бодох жижиг функц бичээд, түүнийг дуудагдах бүрт хэвлэдэг болгоё.
python
def square(x):
print(f" [{x}-ийн квадратыг бодож байна]")
return x * x
print("list comprehension:")
a = [square(x) for x in range(3)]
print("list бэлэн боллоо")
print("generator expression:")
b = (square(x) for x in range(3))
print("generator бэлэн боллоо")
print(next(b))list comprehension:
[0-ийн квадратыг бодож байна]
[1-ийн квадратыг бодож байна]
[2-ийн квадратыг бодож байна]
list бэлэн боллоо
generator expression:
generator бэлэн боллоо
[0-ийн квадратыг бодож байна]
0Санах ойн түвшинд мөрдье.
Алхам 1 (list comprehension мөр). square() гурван удаа дараалан дуудагдана. “list бэлэн боллоо” хэвлэгдэхээс өмнө бүх тооцоолол дууссан. Санах ойд: a нь гурван бэлэн утгатай list [0, 1, 4].
Алхам 2 (generator expression мөр). square() нэг ч удаа дуудагдсангүй. Python зөвхөн эхэндээ зогссон машин үүсгэж, түүнд range(3)-аас авсан түгээгчийг өгсөн. Санах ойд: b нь generator, дотор нь range-ийн түгээгч 0-р байрлалд, ямар ч квадрат тооцоологдоогүй.
Алхам 3 (next(b)). Машин анх удаа хөдөлнө. range-ийн түгээгчээс 0-ийг авч, square(0)-ийг дуудаж, 0-ийг гаргаад зогсоно. Санах ойд: b дотор range-ийн түгээгч 1-р байрлалд, 1 ба 2-ын квадрат хараахан тооцоологдоогүй.
Хэрэв бид b-ээс дахин асуухгүй бол 1 ба 2-ын квадрат хэзээ ч тооцоологдохгүй. Захиалгын ресторан захиалгагүй таваг хийдэггүй.
Жишээ 1: санах ойг хэмжих
Одоо алдартай мэдэгдлийг шалгах цаг боллоо: “generator expression санах ой хэмнэдэг.” Python-д санах ойг хэмжих хоёр хэрэгсэл ашиглана. Эхнийх нь sys.getsizeof(): нэг объект өөрөө хэдэн байт эзэлж байгааг хэлнэ.
python
import sys
N = 1_000_000
as_list = [x * x for x in range(N)]
as_gen = (x * x for x in range(N))
print(f"list: {sys.getsizeof(as_list):,} байт")
print(f"generator: {sys.getsizeof(as_gen):,} байт")Миний компьютер дээр гарсан нэг үр дүн:
list: 8,448,728 байт
generator: 200 байтList найман сая гаруй байт буюу 8 мегабайт орчим эзэлж байна. Generator 200 байт орчим. Тоонууд таны компьютер болон Python-ий хувилбараас хамааран бага зэрэг өөр гарах нь хэвийн. Ялангуяа generator-ийн хэмжээ хувилбар бүрт хэдэн арван байтаар ялгаатай байдаг. Гэхдээ харьцаа нь хаана ч ижил: хэдэн мянга дахин ялгаатай. Мөн N-ийг 10 дахин нэмэгдүүлэхэд list-ийн хэмжээ мөн 10 дахин өсөх бол generator-ийнх огт өөрчлөгдөхгүй, учир нь машин хэдэн утга гаргахаас үл хамааран ижил хэмжээтэй.
Гэхдээ энд шударга байх хэрэгтэй. sys.getsizeof() зөвхөн list-ийн "хүрээ"-г буюу утгууд руу заасан заагчуудыг хэмждэг, утгуудыг өөрсдийг нь тоолдоггүй. Сая тоо бүр өөрөө санах ойд 28-32 байт эзэлдэг тул бодит ялгаа үүнээс ч их. Бодит дүр зургийг харахын тулд хоёр дахь хэрэгсэл tracemalloc-ийг ашиглая. Энэ нь Python-ий стандарт сангийн нэг бөгөөд тодорхой код ажиллах үед хамгийн их хэдэн байт санах ой зэрэг эзлэгдсэнийг буюу оргил санах ой-г хэмждэг. Бид хоёр хэлбэрээр сая тооны квадратын нийлбэрийг бодно.
python
import tracemalloc
N = 1_000_000
def with_list():
return sum([x * x for x in range(N)])
def with_generator():
return sum(x * x for x in range(N))
def measure(label, func):
tracemalloc.start()
result = func()
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"{label}: үр дүн {result}, оргил {peak:,} байт")
measure("list comprehension ", with_list)
measure("generator expression", with_generator)Ажиллуулах команд:
python memory_compare.pyНэг ажиллуулалтын үр дүн:
list comprehension : үр дүн 333332833333500000, оргил 40,965,352 байт
generator expression: үр дүн 333332833333500000, оргил 912 байтmeasure() функцийг мөр мөрөөр нь уншъя. tracemalloc.start() санах ой хянаж эхэлнэ. func() бидний өгсөн функцийг ажиллуулна. tracemalloc.get_traced_memory() хоёр тоо буцаана: одоогийн хэрэглээ ба оргил хэрэглээ. Бидэнд зөвхөн оргил хэрэгтэй тул эхнийхийг _ гэдэг "ашиглахгүй" нэрт хувьсагчид хаяв. tracemalloc.stop() хяналтыг зогсооно.
Одоо үр дүнг уншъя. Хоёр хэлбэр яг ижил хариу өгсөн, тэгэхээр зөвхөн арга нь ялгаатай. List хэлбэр 41 мегабайт орчим оргил санах ой хэрэглэв: сая квадратыг бүгдийг нь нэг дор хадгалсан, дараа нь л sum() тэднийг нэмсэн. Generator хэлбэр нэг килобайтаас бага хэрэглэв: sum() машинаас нэг квадрат авч нэмээд, дараагийнхыг авах үед өмнөх нь аль хэдийн хаягдсан байв. Энэ бол "санах ой хэмнэдэг" гэсэн мэдэгдлийн бодит хэмжүүр: энэ жишээнд дөчин мянга орчим дахин. Таны компьютер дээр тоонууд бага зэрэг өөр гарах ч энэ харьцаа хадгалагдана. Хэрэв N-ийг 100 сая болговол list хувилбар хэдэн гигабайт шаардаж, сул компьютерийг гацааж ч магадгүй бол generator хувилбар хэдэн зуун байтаар л хэвээр үлдэнэ.
Жишээ 2: хугацааг хэмжих, шударга гайхшрал
Санах ой хэмнэдэг юм бол хурдан ч байх ёстой гэж бодох нь зүйн хэрэг. Шалгая. Python-д жижиг кодын хугацааг хэмжих стандарт хэрэгсэл бол timeit. Бид түүнийг Бүлэг 12-т бүрэн судлах бөгөөд одоохондоо зөвхөн терминалаас ашиглана. Команд нь python -m timeit ба араас нь хэмжих кодыг хашилтанд бичнэ. timeit кодыг олон удаа ажиллуулж, хамгийн сайн үр дүнг хэлдэг тул нэг удаагийн санамсаргүй саатал үр дүнг гажуудуулахгүй.
python -m timeit "sum([x * x for x in range(1_000_000)])"
python -m timeit "sum(x * x for x in range(1_000_000))"Миний компьютер дээрх нэг хэмжилт:
5 loops, best of 5: 52.4 msec per loop
5 loops, best of 5: 61.8 msec per loopГаралтыг уншъя: “5 loops, best of 5: 52.4 msec per loop” гэдэг нь "кодыг 5 удаа ажиллуулсан цувааг 5 удаа давтав, хамгийн сайн цувааны нэг ажиллуулалт 52.4 миллисекунд үргэлжилсэн" гэсэн утгатай. Гайхалтай нь list хувилбар илүү хурдан гарлаа, энэ жишээнд 15 хувь орчим. Таны компьютер дээр яг тоо өөр байх ч ихэнх Python хувилбар дээр list хувилбар бага зэрэг хурдан эсвэл ойролцоо гардаг.
Яагаад? Зүйрлэлийн сүүлийн өгүүлбэрийг санаарай: захиалга бүр бага зэрэг нэмэлт ажил шаарддаг. Generator утга бүрт машинаа сэрээж, утга гаргаж, дахин хөлдөөх ёстой. Энэ давтагдах "сэрэх, зогсох" ажил жижиг боловч сая удаа давтагдахад хуримтлагдана. List comprehension бүх утгыг тасралтгүй нэг урсгалаар тооцоолдог тул энэ нэмэлт ажилгүй. Тиймээс шударга дүгнэлт нь: generator expression санах ой хэмнэдэг, хугацаа биш. Санах ой хангалттай, өгөгдөл жижиг бол list comprehension бүрэн зөв сонголт. Өгөгдөл том, эсвэл хэмжээ нь урьдчилан мэдэгдэхгүй бол generator expression аюулгүй сонголт.
Жишээ 3: эрт зогсох үед гарах асар их ялгаа
Хугацааны хувьд generator ялах нэг тохиолдол бий бөгөөд тэр үед ялалт нь асар их. Энэ бол хариу эрт олдох тохиолдол. any() функц iterable-аас нэг True утга олмогц тэр даруй зогсдог. Арван сая тооноос 10-аас их тоо байгаа эсэхийг шалгая. Хариу 11 дэх тоон дээр л олдоно.
python -m timeit "any([x > 10 for x in range(10_000_000)])"
python -m timeit "any(x > 10 for x in range(10_000_000))"1 loop, best of 5: 412 msec per loop
500000 loops, best of 5: 498 nsec per loopЭхний мөрөнд “msec” буюу миллисекунд, хоёр дахь мөрөнд “nsec” буюу наносекунд байгааг анхаар. Нэг миллисекунд нь сая наносекунд. Тэгэхээр list хувилбар 412 сая наносекунд, generator хувилбар 498 наносекунд зарцуулсан, ойролцоогоор найман зуун мянга дахин ялгаа. Таны компьютер дээр тоонууд өөр байх ч энэ зэрэглэл хадгалагдана.
Шалтгааныг trace-ээр тайлбарлая. List хувилбар эхлээд арван сая True/False утга бүхий list бүрэн бүтээнэ: арван сая харьцуулалт, арван сая утгын санах ой. Дараа нь л any() түүнийг уншиж эхлээд, 12 дахь элемент дээр (x = 11) True олж зогсоно. Үлдсэн бараг арван сая утга бүтээгдсэн ч хэзээ ч уншигдаагүй. Generator хувилбарт any() машинаас нэг нэгээр асууна: x = 0, False; x = 1, False; ... x = 11, True. any() тэр даруй зогсоно. Машин нийтдээ 12 харьцуулалт л хийсэн, үлдсэн 9,999,988 тоо хэзээ ч хүрэгдсэнгүй. Энэ бол буфет ба захиалгын рестораны ялгааны хамгийн тод илрэл: үйлчлүүлэгч эхний тавагнаас цадсан.
any()-ээс гадна all(), өмнөх хичээлийн next(), for loop доторх break, мөн in оператор эрт зогсож чаддаг. Эдгээрийн аль нэгтэй хамт хэрэглэхэд generator expression хурдны хувьд ч мэдэгдэхүйц давуу тал өгдөг.
Жишээ 4: хаалтыг орхих дүрэм ба бодит хэрэглээ
Дээрх жишээнүүдэд та sum(x * x for x in range(N)) гэж бичсэнийг анзаарсан байх. Энд generator expression-ийн өөрийн дугуй хаалт алга, зөвхөн sum()-ийн хаалт л бий. Python-д дүрэм бий: generator expression функцийн цорын ганц argument бол түүний өөрийн хаалтыг орхиж болно. Бичлэг цэвэрхэн, уншихад хялбар болно. Харин өөр argument нэмэгдвэл хаалт заавал хэрэгтэй:
python
scores = [72, 95, 88, 61]
top = sorted((s for s in scores if s >= 70), reverse=True)
print(top)[95, 88, 72]Энд sorted() хоёр argument авч байна: generator expression ба reverse=True. Тиймээс generator expression өөрийн хаалтанд орсон. Хаалтгүй бичвэл Python алдаа өгнө, үүнийг Түгээмэл алдаанууд хэсэгт уншина.
Одоо бодит хэрэглээ. Өдрийн борлуулалтын CSV файл байна гэж бодъё: эхний мөр нь “product,amount” гэсэн гарчиг, үлдсэн мөр бүр "бүтээгдэхүүн,дүн" хэлбэртэй. Нийт дүнг тооцоолъё:
python
with open("sales.csv", encoding="utf-8") as f:
next(f)
total = sum(int(line.split(",")[1]) for line in f)
print(f"Нийт борлуулалт: {total:,} төгрөг")Энэ хоёр мөр Бүлэг 1 ба Бүлэг 2-ын мэдлэгийг нэгтгэж байна. next(f) гарчгийн мөрийг алгасна, Хичээл 1.3-ын загвар. Дараа нь generator expression файлын түгээгчээс мөр бүрийг асууж, таслалаар хувааж, хоёр дахь хэсгийг тоо болгоно. sum() тэдгээрийг нэг нэгээр нэмнэ. Файл нэг сая мөртэй байлаа ч санах ойд нэг удаад нэг л мөр байна. Хэрэв бид [...] хэлбэрээр бичсэн бол сая тоог бүгдийг нь эхлээд list-д хадгалах байсан. Санах ойн хэмнэлттэй, нэг мөрт, уншихад тодорхой энэ загвар бол бодит Python кодонд generator expression-ийн хамгийн түгээмэл хэрэглээ юм.
Хүсвэл Жишээ 1-ийн tracemalloc скриптийн N-ийг 100,000, 1,000,000, 5,000,000 гэж өөрчилж, хоёр хэлбэрийн оргил санах ой хэрхэн өөрчлөгдөхийг харж болно. List хувилбар N-тэй хамт өсөж, generator хувилбар хөдлөхгүй байх ёстой. Сонирхвол Жишээ 3-ын хэмжилтийг x > 10 биш x > 9_999_990 нөхцөлтэйгээр давтаж үзээрэй. Хариу хамгийн төгсгөлд олдох үед хоёр хэлбэрийн хурдны ялгаа юу болох вэ? Энэ туршилт эрт зогсолт ялгааны жинхэнэ шалтгаан гэдгийг өөрийн хэмжилтээр батална. Эдгээр нь зөвхөн санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
SyntaxError: Generator expression must be parenthesized. “Generator expression хаалтанд орсон байх ёстой” гэсэн утгатай. Generator expression-ийг цорын ганц бус argument болгон хаалтгүй бичсэн үед гарна, жишээлбэл sorted(s for s in scores, reverse=True). Шийдэл нь generator expression-ийг өөрийн дугуй хаалтанд оруулах: sorted((s for s in scores), reverse=True). Дүрмийг санахад хялбар: ганцаараа бол хаалт сонголттой, нөхөртэй бол заавал.
Хоёр дахь удаа 0 эсвэл хоосон үр дүн гарах. Мессежгүй алдаа. Generator expression-ийг хувьсагчид хадгалаад хоёр удаа хэрэглэхэд хоёр дахь удаа юу ч өгөхгүй. Жишээлбэл squares = (x * x for x in data) бичээд sum(squares), дараа нь max(squares) гэвэл sum() зөв ажиллах ч max() “ValueError: max() iterable argument is empty” буюу “max()-д өгсөн iterable хоосон байна” гэсэн алдаа өгнө. sum() хоосон generator дээр зүгээр л 0 буцаах тул бүр анзаарагдахгүй өнгөрч болно. Хоёр ба түүнээс олон удаа хэрэглэх өгөгдлийг list comprehension-оор хадгал, эсвэл generator expression-ийг хэрэглэх газар бүрт шинээр бич.
Дугуй хаалт tuple үүсгэнэ гэж бодох. [x for x in data] list, {x for x in data} set үүсгэдэг тул (x for x in data) tuple үүсгэнэ гэж таамаглах нь зүйн хэрэг. Гэвч Python-д “tuple comprehension” гэж байдаггүй, дугуй хаалт generator үүсгэдэг. Tuple хэрэгтэй бол generator expression-ийг tuple()-д өг: tuple(x for x in data).
TypeError: ‘generator’ object is not subscriptable. Өмнөх хичээлийн танил: generator expression-д индексээр хандах боломжгүй, squares[0] ажиллахгүй. Эхний утга хэрэгтэй бол next(), индекс хэрэгтэй бол list comprehension ашигла.
Generator expression бол list comprehension-ийн дугуй хаалттай хувилбар бөгөөд generator функцийн нэг мөрт товчлол юм. Бичигдэх мөчид юу ч тооцоолдоггүй, асуухад нь утга бүрийг нэг нэгээр бэлддэг. Бид гурван бодит хэмжилт хийлээ. Санах ойн хувьд сая тооны нийлбэрт list хувилбар 41 мегабайт орчим, generator хувилбар нэг килобайтаас бага хэрэглэсэн. Хугацааны хувьд энгийн нийлбэрт generator хурдан биш, харин бага зэрэг удаан байсан, учир нь утга бүрт машинаа сэрээх нэмэлт ажилтай. Эрт зогсох тохиолдолд, тухайлбал any()-д, generator хэдэн зуун мянга дахин хурдан байв. Шударга дүрэм: generator expression санах ой хэмнэдэг, хурд зөвхөн эрт зогсох үед л хэмнэдэг. Цорын ганц argument бол хаалтаа орхиж болно.
Энэ хичээлд бид нэг generator-ийг нэг үйлдэлд, нийлбэр эсвэл шалгалтад ашигласан. Гэтэл бодит ажилд өгөгдөл ихэвчлэн хэд хэдэн шатаар дамждаг: уншина, шүүнэ, хувиргана, нэгтгэнэ. Дараагийн хичээлд бид generator-уудыг хооронд нь холбож, нэгнийх нь гаралт нөгөөгийнх нь оролт болдог конвейер буюу pipeline бүтээнэ. Log файлын нэг мөр тэр конвейерийн шат бүрийг хэрхэн дамжин өнгөрөхийг бид trace-ээр алхам алхмаар дагах болно.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.