Shanraq.org Shanraq.org
Итераторлар мен генераторлар: 800 984 байтқа қарсы 208
IT

Python: деректен өз есебіңізге дейін 16-сабақ (барлығы 56)

Итераторлар мен генераторлар: 800 984 байтқа қарсы 208

Python курсының он алтыншы сабағы. Жүз мың квадрат тізіммен төрт мегабайтқа жуық орын алады, генератормен — 376 байт: барлық сандарымен қоса өлшенді. Айырма қалай санауда емес, мәндердің қашан пайда болуында. Плюс басты қате: генератор бір реттік, әрі онымен екінші аралау үнсіз бос нәтиже береді.

Не үшін керек

Бесінші сабақ уәдемен аяқталған еді: «тізім өрнектері — он алтыншыда». Он біріншісі гигабайттық файл туралы қарыз қосты, он екіншісі — жүз мыңдаған жолдық деректер экспорты туралы. Бұның бәрі — бір сұрақ, әрі ол бүгін жабылады.

Сұрақ мынау: мәндер қашан пайда болады. Тізім оларды бірден түгел жасайды да, жадта ұстайды. Генератор сұраған кезде бір-бірлеп жасайды, әрі ештеңе ұстамайды.

Дерек аз болғанда айырма байқалмайды. Көп болғанда, ол бағдарламаның жұмыс істейтінін немесе жад жетпей құлайтынын шешеді.

Бірден тұтас

agyn.py файлы. Іске қосу: ортадан python agyn.py.

Міндеттісі — екінші және үшінші блок: yield бар генератор және тізім мен генератордың айырмасы. Біріншісі for осы уақытқа дейін не істеп келгенін көрсетеді, төртіншісі — сабақты соңына дейін оқуға тұрарлық қатені.

"""16-сабақ: генератор — жадта әлі жоқ дерек.

Жүз мың жолдық файл жадқа сыяды, ал гигабайттық — жоқ. Айырма қалай оқуда емес,
мәндердің қашан пайда болуында: бірден бәрі ме, әлде бір-бірлеп пе.
"""

import sys
import tracemalloc
from pathlib import Path

HERE = Path(__file__).parent
data = HERE / "vygruzka.csv"

# Оқу деректер экспорты: «жыл;мән» деген жүз мың жол.
with data.open("w", encoding="utf-8") as target:
    for year in range(2000, 2100):
        for number in range(1000):
            target.write(f"{year};{number % 20 + 1}.5\n")

print("== for шын мәнінде не істейді")
numbers = [1, 2, 3]
step = iter(numbers)
print("бір-бірлеп:", next(step), next(step), next(step))
try:
    next(step)
except StopIteration:
    print("StopIteration — осы жерде for жай ғана аяқталады")

print()
print("== генератор мәндерді бір-бірлеп береді")


def values(path):
    """Файлды тұтас оқымай, (жыл, мән) жұбын бір-бірлеп береді."""
    with path.open(encoding="utf-8") as source:
        for line in source:
            year, _, text = line.strip().partition(";")
            yield int(year), float(text)


stream = values(data)
print("типі:", type(stream).__name__)
print("бірінші мән:", next(stream))
print("екінші мән:", next(stream))

print()
print("== жақша шешеді: тізім бе, генератор ба")
# getsizeof объектінің өзін өлшейді, ал тізім — сандарға сілтемелер ғана.
# Шын мәнінде қанша орын алынғанын tracemalloc санайды. Мегабайтпен басамыз:
# tracemalloc-тың дәл байты әр машинада өзгеше, ал «төрт мегабайт» — бәрінде бір.
tracemalloc.start()
base = tracemalloc.get_traced_memory()[0]
squares_list = [number * number for number in range(100_000)]
list_all = tracemalloc.get_traced_memory()[0] - base
squares_gen = (number * number for number in range(100_000))
gen_all = tracemalloc.get_traced_memory()[0] - base - list_all
tracemalloc.stop()
print(f"тізім:     объектінің өзі {sys.getsizeof(squares_list)} байт, барлық сандарымен {list_all / 1_000_000:.1f} МБ")
print(f"генератор: объектінің өзі {sys.getsizeof(squares_gen)} байт, ұстап тұрғанының бәрімен {gen_all} байт")
print("сома бірдей:", sum(squares_list) == sum(squares_gen))

print()
print("== генератор бір реттік")
stream = values(data)
total = 0.0
count = 0
for _, value in stream:
    total += value
    count += 1
print(f"жол: {count}, сома: {total:.1f}")
try:
    print(max(value for _, value in stream))
except ValueError as error:
    print("сол генератормен екінші аралау:", error)

data.unlink()

Шығатыны:

== for шын мәнінде не істейді
бір-бірлеп: 1 2 3
StopIteration — осы жерде for жай ғана аяқталады

== генератор мәндерді бір-бірлеп береді
типі: generator
бірінші мән: (2000, 1.5)
екінші мән: (2000, 2.5)

== жақша шешеді: тізім бе, генератор ба
тізім:     объектінің өзі 800984 байт, барлық сандарымен 4.0 МБ
генератор: объектінің өзі 208 байт, ұстап тұрғанының бәрімен 376 байт
сома бірдей: True

== генератор бір реттік
жол: 100000, сома: 1100000.0
сол генератормен екінші аралау: max() iterable argument is empty

Талдау

for осы уақытқа дейін next-ті шақырып келді

бір-бірлеп: 1 2 3
StopIteration — осы жерде for жай ғана аяқталады

for — сиқыр емес. Ол объектіден итератор алады (iter), содан кейін StopIteration келгенше next-ті тартады да, сонымен бітеді.

Осыдан маңызды қорытынды шығады: for тізіммен емес, бір-бірлеп бере алатынның бәрімен жұмыс істейді. Файл, сөздік, range, жол, генератор — бәрі бірдей араланады, әрі range(1_000_000) миллион сан жасамайды, оларды жол-жөнекей санайды.

Елестетіп көріңіз. Кассадағы кезек. Кассир барлығы неше адам екенін сұрамайды; келесісі бар болса, келесісіне қызмет көрсете береді.

yield — бір-бірлеп беретін және орнын есте сақтайтын функция

def values(path):
    with path.open(encoding="utf-8") as source:
        for line in source:
            ...
            yield int(year), float(text)

yield бар функция — генераторлық. Оны шақыру ештеңе есептемейді: values(data) генератор объектісін қайтарады, әрі сол сәтте файл тіпті ашылмаған. Мәндер бірінші next-те пайда болады — әрі әрі қарай бір-бірлеп.

return функцияны аяқтайды, yieldтоқтата тұрады: келесі next-те ол сол жерден, сол айнымалылармен және сол ашық файлмен жалғастырады. Сондықтан генератор гигабайттық файлды бір жолдық жадпен оқиды: онда әрқашан тек ағымдағы жол тұрады.

Мұндай генератор — сүзгі қоятын жер де. Талдауға келмеген жолды ол сыртқа мүлде бермеуі мүмкін, әрі оны оқып отырғанның ол туралы білуінің қажеті жоқ.

Жақша шешеді: тізім бе, генератор ба

squares_list = [number * number for number in range(100_000)]
squares_gen = (number * number for number in range(100_000))

Тік жақша — тізім өрнегі: тізім жинайтын циклдің қысқа жазылуы. [x * 2 for x in prices] — бұл бір жолмен жазылған append бар for. Шартымен де болады: [x for x in prices if x > 500].

Дөңгелек жақша — генератор өрнегі: сол нәрсе, бірақ мәндер жиналмайды, бір-бірлеп беріледі. Айырма өлшеуде көрінеді:

тізім:     объектінің өзі 800984 байт, барлық сандарымен 4.0 МБ
генератор: объектінің өзі 208 байт, ұстап тұрғанының бәрімен 376 байт

Мұнда екі рет өлшенген, әрі екі сан да керек. sys.getsizeof объектінің өзін көрсетеді: тізімде бұл 800 килобайт — жүз мың сілтеме, басқа ештеңе емес, өйткені сандардың өзі бөлек жатыр. tracemalloc бөлінген нәрсенің бәрін санайды, әрі шындық сол жерде көрінеді: төрт мегабайтқа жуық пен үш жүз жетпіс алты байт.

Айырма он мың есеге жуық, әрі ол деректің өсуімен бірге тек бір жақта өспейді. Генератор кез келген санда сонша орын алады: ол мәндерді емес, тоқтаған орнын — өзінің жергілікті айнымалылары мен ағымдағы қадамын — сақтайды. «Ештеңе сақтамайды» деу дұрыс болмас еді; шындығы — ол дайын нәтижені сақтамайды.

Таңдау ережесі қарапайым: нәтиже тұтас әрі бірнеше рет керек болса — тізім; бір рет әрі жол-жөнекей керек болса — генератор. sum(x * x for x in range(100_000)) тізімді мүлде жасамайды.

Генератор бір реттік

жол: 100000, сома: 1100000.0
сол генератормен екінші аралау: max() iterable argument is empty

Міне, сабақты соңына дейін оқуға тұрарлық қате. Генератор бітеді. Онымен бір рет жүріп өтсеңіз, оны сарқып болдыңыз, әрі екінші аралау бостық береді — қате емес, нөл элемент.

Мұнда max дауыстап шағымданды, өйткені бостың максимумы жоқ. Бірақ sum бос нәрседен 0 береді, list[], ал цикл бірде-бір рет орындалмайды. Есеп нөлдермен шығады да, ешкім себебін айтпайды.

Екі шешімнің бірімен емделеді: не керектің бәрін бір аралауда санайды (мысалдағыдай: сома да, саны да), не генераторды қайта жасайды — values(data) — да, файлды екінші рет оқумен төлейді. Қайсысы арзан екені файлдың көлеміне тәуелді; қайсысы адал екені кодтан әрқашан көрініп тұрады.

Бүгін алмайтынымыз

itertools — ағындарға арналған кітапхана: islice алғашқы N-ін алады, chain жалғайды, groupby қатар тұрғанын топтайды. Бұның бәрі генератормен жұмыс істейді әрі бір мәннен ғана. Бүгін модульдің бар екенін білу жеткілікті.

Сабақ картасы

Сабақ картасы: тізім, генератор және бір аралау

Өз сөзіңізбен айтыңыз

Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптары — сабақтың соңында.

  1. for іште не істейді, әрі файлды неге тізім сияқты аралауға болады?
  2. [x for x in ...] (x for x in ...)-тен немен өзгеше — әрі қайсысын қашан алады?
  3. Генератормен екінші аралау неге бостық береді, әрі онымен не істейді?

Жаттығу

Тапсырмаға дейінгі үш қысқа қадам: болжау, толықтыру, жөндеу. Жауаптары — сабақтың соңында, бірақ алдымен өзіңіз жауап беріңіз.

1. Болжаңыз. Бұл бағдарлама не басып шығарады? Екі соманы да бір өрнек санайды.

squares = (n * n for n in range(3))
print(sum(squares), sum(squares))

2. Бос орынды толтырыңыз. ... орнына әр бағаны екі есе арттыратын өрнекті қойыңыз.

prices = [260, 620, 1890]
doubled = [... for price in prices]
print(doubled)

3. Жөндеңіз. Бағдарлама құлайды: генератордың ұзындығы жоқ. Тізім жинамай, санын есептеңіз.

values = (n for n in range(5))
print(len(values))

Тапсырма

Міндетті. Берілгені:

rows = ["2024;8.7", "2025;n/a", "2025;11.4", "2026;15.0", "қоқыс", "2026;12.0"]

Осы жолдарды бағдарламаның қасындағы файлға жазыңыз. (жыл, мән) жұбын бір-бірлеп беретін және талдауға келмейтін жолдарды үнсіз аттап өтетін values(path) генераторын жазыңыз. Одан next арқылы алғашқы екі мәнді алып, басып шығарыңыз. Содан кейін бір аралауда неше жол алынғанын және екі таңбалы орташаны санаңыз. Соңында тізім өрнегімен 2024-тен кейінгі жылдарды басып шығарыңыз. Файлды өзіңізден кейін жинаңыз.

Күтілетін шығу:

біріншісі: (2024, 8.7)
екіншісі: (2025, 11.4)
алынған жол: 4, орташа: 11.78
2024-тен кейінгі жылдар: [2025, 2026, 2026]

Дайын болғаны: шығу жол-жолмен сәйкес келеді; бағдарламада yield бар, ал файл ешбір жерде тұтас оқылмайды; сома мен саны бір аралауда саналған; жарамсыз жолдар генератордан кейін емес, оның ішінде сүзіледі.

Өз деректеріңізде. Өз файлыңызды алыңыз — деректер экспорты, лог, жол-жолмен жазылған кез келген нәрсе. Одан тек өзіңізге керегін беретін генератор жазып, сол арқылы бір санды есептеңіз. Сол кезде файлдың неше жолы бір мезгілде жадта болғанын дауыстап айтыңыз.

Қалауыңызша.

  • Миллион элементте тізім мен генератор үшін sys.getsizeof-ты салыстырыңыз.
  • [x for x in range(20) if x % 3 == 0] жинаңыз да, сол нәрсені генератормен жасап, типтерін салыстырыңыз.
  • itertools.islice(values(path), 3) алып, файлдың неше жолы оқылғанын қараңыз.

Жобада бұл қайда тұрады

Шолу деректер экспортының көлеміне тәуелді болуын қояды. Бұрын ол файлды тізімге оқып, сол бойынша санайтын; енді жолдар генератор арқылы ағады, әрі жадта әрқашан біреуі. Сол код жүз жолда да, гигабайтта да жұмыс істейді.

Қарыздар. Бір аралау — бұл тәртіп: деректен не керегін алдын ала шешуге тура келеді. Ал itertools-ты атадық, бірақ талдамадық; шолуға бірнеше дереккөзді жалғау керек болғанда ораламыз.

Жауаптар

Жауаптарды көрсету

Сұрақтарға

  1. iter арқылы итератор алады да, StopIteration келгенше next-ті тартады. Сондықтан for мәндерді бір-бірлеп бере алатынның бәрімен бірдей жұмыс істейді — тізіммен де, файлмен де, сөздікпен де, генератормен де.
  2. Тік жақша тізімді бірден жинайды да, оны жадта ұстайды; дөңгелегі мәндерді бір-бірлеп беретін және ештеңе сақтамайтын генератор береді. Нәтиже тұтас әрі бірнеше рет керек болғанда тізімді алады; бір рет әрі жол-жөнекей керек болғанда — генераторды.
  3. Өйткені генератор тоқтаған орнын есте сақтайды, ал тоқтағаны — соңы. Екінші аралау қате емес, бостық — әрі бұл қатеден қауіптірек. Не бәрін бір аралауда санайды, не генераторды қайта жасайды.

Жаттығуға

  1. 5 0. Бірінші сома генераторды соңына дейін аралады — 0 + 1 + 4. Екіншісіне онда ештеңе қалмады, ал бос нәрседен sum бір сөз демей нөл береді.
5 0
  1. price * 2. Тізім өрнегінде for-дың сол жағында тізімге салынатын нәрсе, оң жағында — қайдан алынатыны тұрады.
prices = [260, 620, 1890]
doubled = [price * 2 for price in prices]
print(doubled)
[520, 1240, 3780]
  1. TypeError: object of type 'generator' has no len(). Генератордың ұзындығы жоқ: ол өзі де неше мән беретінін білмейді. Жол-жөнекей санайды:
values = (n for n in range(5))
print(sum(1 for _ in values))
5

Дереккөздер

Мәтінде қате не теру қатесі кездессе, бізге айтыңыз

Тапсырманы тексеру

Алдымен VS Code-та шешіп, іске қосыңыз — редактор қатені сол жерде көрсетеді. Дайын шешімді осында қойыңыз. Тексеретін — модель: ол қатені атап көрсетеді, бірақ дайын жауапты бермейді.

Тексеру үшін кіру керек. Кіру

Пікірлер (0)

Әзірге пікір жоқ. Бірінші болыңыз.