Как стать автором
Поиск
Написать публикацию
Обновить

Ленивые вычисления в PHP: как генераторы и итераторы экономят память и ускоряют код

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров3.3K
Всего голосов 8: ↑5 и ↓3+2
Комментарии53

Комментарии 53

Почему генераторы ассоциируют всегда с экономией памяти, хотя никакую память сами по себе они не экономят?

Замените контракт

function fetchCars():\Generator<Car>

На

function fetchCars(int $ofset, int $limit): array<Car>

И получите такой же буст по экономии памяти, просто менее удобное использование.

Или вообще уберите метод и поднимите логику на уровень выше, т.е. сделайте ровно тоже самое только без обертки функции.

Опять же можно сделать свой итератор, который так же ходит по апи и не коллекционирует весь объем, просто это больше кода, а генератор хорошая удобная замена.

В итоге, когда разговариваешь с людьми про генераторы, то от них один ответ: он для экономии памяти, а так как память экономить не всегда приходится, так как крудошлепы, или просто есть другие методы экономии, то и генератор не понятно для чего (максимум расскажут пример с csv) и не используют. Хотя это просто побочный эффект одного из кейсов использования, который при желании можно решить и без генератора.

Генераторы нужны не только для экономии памяти, но, например, для экономии времени. Например, я хочу получить из базы миллион строк, если бы я сделал запрос с limit 1000000, то время получения результата было бы намного больше, чем если бы я сделал 10 запросов по 100000 строк, потому что экономятся ресурсы базы, ресурсы сети и, опять же, память. Все это экономит время (да, за счет того, что экономится память, но все же)

while ($row = $stmt->fetch()) {
   // обрабатываем строку
}

Тут нет никакого генератора, а память не расходуется. Что я делаю не так?

И это я ещё не упоминаю, что про "если бы я сделал 10 запросов по 100000 строк" вас нагло и цинично обманули.

Тут используются те же механизмы, что и в генераторе.

Вы сейчас редкую чушь написали. Генератор к этому коду вообще никакого отношения не имеет. Механизмы, которые используются в генераторе - это остановить выполнение, запомнить стек, и вернуть значение.

Некая конкретная реализация генератора может использовать этот код, но и тут не надо переворачивать с ног на голову. Это реализация генератора будет использовать, причём не "те же", а именно этот механизм. Который и без генератора прекрасно справляется. А вот генератор без него не сэкономит и байта. Получается, что память экономит "механизм". А смысл генератора в том, что он может перенести "механизм" в другое место.

Генератор сохраняет состояние, чтобы при повторном обращении продолжить с того же места, а не сначала. fetch именно так и работает.

Да. Но к экономии памяти это отношения не имеет. Есть гении, которые внутри генератора читают файл в массив (ачотакова, генератор сам же всё сэкономит!) и генератор дальше прекрасно отдаёт этот массив, "сохраняя состояние". Но не экономя ни байта памяти.

Генератор не всегда служит для экономии памяти. Когда же речь заходит про "экономию памяти", то это всегда делает цикл, который у генератора него внутри. А удобство генератора в том, что он позволяет выполнять этот цикл не сразу, а в другом месте. А не в "экономии памяти".

Но к экономии памяти это отношения не имеет.

Именно что имеет. Иначе зачем вам продолжать с того же места?

Генератор не всегда служит для экономии памяти.
это всегда делает цикл, который у генератора него внутри

Генератор, который не служит для экономии памяти и не имеет этого цикла, низачем не нужен.

он позволяет выполнять этот цикл не сразу, а в другом месте

Это прекрасно достигается обычной функцией, которая возвращает массив. В одном месте получили массив из базы, в другом его обработали.

Память не расходуется, потому что fetch()в PDO уже работает лениво, возвращает строки по одной и не загружает весь результат в память.

Но вы бесконечно продолжаете приводить примеры с циклами и говорите: "и так работает".

Но смысл не в самом цикле, а в правильном использовании генератора.

Видимо, не до конца понятно, когда генераторы реально нужны, а когда нет.

Попробую объяснить ещё раз.

Заходим на малоизвестный и почти неиспользуемый сайт:
👉 https://www.php.net/manual/en/language.generators.overview.php

И там находим:

Генератор поддерживает удобную передачу данных в циклы foreach без предварительной загрузки массива в память (Тут прям очень важно это понять), что иногда вызывает превышение программой предела памяти или значительно увеличивает время обработки, которое уходит на генерацию результата.

Дальше читаем :

Простой пример этого — переопределение функции range() как генератора. Стандартная функция range() генерирует массив, который состоит из значений, и возвращает его, что приводит к генерации больших массивов: например, вызов range(0, 1000000) займёт более 100 МБ оперативной памяти.

А теперь посмотрим на ваш код:

while ($row = $stmt->fetch()) {
    // обрабатываем строку
}

Это обычный цикл, который и так работает построчно благодаря fetch() в PDO.
Сравнивать его с генератором и спрашивать «что я делаю не так?» некорректно - это разные механизмы и разные задачи.

Ну как разные, если ваш генератор работает только потому, что у него внутри это самое fetch()? 😂
Ну уже смешно, ей-богу :)

Генератор - это обёртка для кода, который экономит память. Конвертирующая этот код в универсальный интерфейс. Но сам по себе он ничего не экономит. Память экономит чтение из базы по одной строке. Генератор же - это такой конвертор, который позволяет результат любого цикла представить в виде массива.

Если исходный цикл экономит память, то и генератор на его основе будет экономить. Если исходный цикл не экономит память, то и генератор на его основе ничего не сэкономит. Ну это уже если совсем на пальцах объяснять эту примитивную логику. Даже так до вас не доходит?

Генератор же - это такой конвертор, который позволяет результат любого цикла представить в виде массива.

Это полностью неверное утверждение. Это не является ни назначением генератора, ни описанием его работы.

Чтобы результат любого цикла представить в виде массива, можно просто складывать в массив, генератор для этого не нужен. Поэтому делали его для другого.

А в виде массива (одномерного) он представляет не "результаты цикла", а значения после оператора yield. Которые могут быть в одном цикле, в нескольких вложенных циклах, или вообще без циклов.

Если исходный цикл не экономит память, то и генератор на его основе ничего не сэкономит.

Да ни при чем тут исходный цикл. Для экономии памяти генератор не оборачивает исходный цикл, а добавляет новый.

Исходный цикл:

$orders = getOrders();
foreach ($orders as $order) {
    processOrder($order);
}

function getOrders() {
  return $db->query('SELECT * FROM products');
}

Цикл с генератором:

$orders = getOrders();
foreach ($orders as $order) {
    processOrder($order);
}

function getOrders() {
  $offset = 0;
  $limit = 100;
  do {
    $res = $db->query('SELECT * FROM products LIMIT $offset, $limit');
    foreach ($res as $row) {
      yield $row;
    }
    $offset += $limit;
  } while(count($res) > 0);
}

Исходный цикл как был так и остался, а не стал основой генератора.

Вы здесь из пальца высосали некий метод $db->query(), который исходно возвращает массив. И начали оптимизировать getOrders() через нелепый лимит.

Но оптимизировать здесь надо было сам метод $db->query(). Который сейчас в цикле while получает из бд строки по одной, и зачем-то запихивает их в массив. Вот этот-то цикл и является исходным.

Вместо массива, метод query() должен возвращать переменную, предоставляющую доступ результатам запроса. А дальше у нас есть два варианта:

  1. Использовать этот цикл как есть, как делают миллионы крудошлёпов по всему миру:

$result = $db->query('SELECT * FROM products');
while ($row = $result->fetch()) {
    // что-то делаем со строкой
} 
  1. Обернуть этот цикл в генератор

function getOrders() {
    $result = $db->query('SELECT * FROM products');
    while ($row = $result->fetch()) {
        yield $row;
    }
}

И спокойно использовать его без нелепых ужимок с лимитами, которые заставляют базу каждый раз выполнять один и тот же запрос, а потом ходить по его результату с самого начала всё дальше и дальше, в полном соответствии с алгоритмом маляра Шлемиэля.

И здесь легко увидеть, что исходный цикл стал основой генератора. И поскольку именно этот цикл экономит память, то и генератор, вслед за ним - тоже. И даже вы, если сможете немного умерить свою спесь, тоже это увидите.

Вы здесь из пальца высосали некий метод $db->query(), который исходно возвращает массив.

Ну так это и есть основной сценарий использования генераторов.

Вместо массива, метод query() должен возвращать переменную

Нет не должен, мне нафиг не надо раскидывать эти детали реализации в вызывающем коде по всему приложению. Я сделал функцию query именно затем, чтобы их туда спрятать.

И здесь легко увидеть, что исходный цикл стал основой генератора.

Это не легко увидеть, потому исходный цикл остался в вызывающем коде, который вызывает вашу getOrders(). Вы опять использовали логическую манипуляцию и привели пример кода не полностью. Если бы исходный цикл стал основой генератора, то он бы не остался в вызывающем коде.

И даже вы, если сможете немного умерить свою спесь

Спесь проявляете тут вы. Оскорбляете других и применяете логические манипуляции, чтобы иметь хотя бы какое-то обоснование высокомерного поведения. Игнорируете прямое объяснение из документации от разработчиков языка, зачем нужны генераторы. Там прямым текстом написано, что назначение генераторов - уменьшить расход памяти в приложении. На этом можно закончить.

Неправда. В документации написано "уменьшить расход памяти при использовании foreach". А закончить действительно стоит.

Я вас уже спрашивал, в чем вы видите разницу, вы не ответили. Видимо ответа у вас нет.
Естественно, при использовании foreach, большие объемы данных обычно обрабатывают через foreach. Он подразумевается по контексту. И нет, генераторы можно использовать и в других циклах.

И по поводу мануала. В нём написано.

передачу данных в циклы foreach без предварительной загрузки массива в память

У множества людей какая-то поразительная слепота, они в упор не видят foreach, а видят только "вызывает превышение программой предела памяти". И в итоге пишут полную чушь.

В вашей статье память упоминается десятки раз, но как в мануале - в контексте использования foreach - ни разу. Если бы вы сами следовали информации с "малоизвестного и почти неиспользуемого сайта", каждый раз упоминая foreach в контексте экономии памяти, то претензий бы к вам не было. То есть вместо

значительно сократить использование памяти при обработке больших наборов данных

правильно будет

значительно сократить использование памяти при обработке больших наборов данных через foreach

Замените контракт на offset limit

Генератор это и есть удобная обертка для offset limit, чтобы не надо было копипастить второй цикл по страницам везде где обрабатывается список cars.

Почему генераторы ассоциируют всегда с экономией памяти

Потому что они позволяют использовать меньше памяти по сравнению с таким же вызывающим кодом, но без генераторов.

от них один ответ: он для экономии памяти

А для чего он по-вашему? Именно для того, чтобы загружать данные не все сразу, а по мере необходимости. Это и называют экономия памяти.

Генератор это и есть удобная обертка

Правильно. Генератор это - это удобная обертка. Это про упаковочный материал, а не про память. В эту обёртку, кроме прочего, можно завернуть тривиальный механизм экономии памяти. Но при этом надо понимать, что генератор - это не про сэкономить, а про обернуть то, что экономит.

[генератор нужен] для того, чтобы загружать данные не все сразу

Я попробую последний раз, дальше повторять не вижу смысла. "Чтобы загружать данные не все сразу, а по мере необходимости" служит цикл, который читает из потока по одной ложке. Генератор к этому тривиальному алгоритму никакого отношения не имеет.

Я попробую последний раз, дальше повторять не вижу смысла. Генератор экономит память именно потому что он содержит код, который читает из потока по одной ложке. Без этого они нафиг не нужны.

генератор - это про обернуть

Чтобы обернуть, есть обычные функции. Это будет просто обычная функция, которая возвращает массив.

Приведу пример: есть у нас какая то функция, которая что то делает с данными из генератора, а генератор, при обращении к нему, проверяет очередь каких нибудь команд, если команд нет - ждет, пока они кем то не добавятся.

Вопрос: много памяти мы сэкономили?

Для этого вам не нужен генератор, ждать может и обычная функция.

Предположим, что нам не обойтись без foreach

Непонятно, что вы имеете в виду. Обычная функция может возвращать массив, который можно использовать в foreach.

Вообще у вас как-то слишком много предположений для того, что в этой ветке заявляется как основное назначение генератора.

А для чего он по-вашему? Именно для того, чтобы загружать данные не все сразу, а по мере необходимости. Это и называют экономия памяти.


Это один из сценариев и реализация удобной обертки. Без генератор можно написать итератор руками (генератор имплементирует итератор), изменить контракт, не оборачивать в функцию.

Для чего? Для удобного использования. Кейсы:
- датапровайдеры в тестах удобнее на генераторах
- создание итератора с сложной логикой, которая менее неудобна при формировании полного массива.
- посмотрите статью про генераторы и корутины https://habr.com/ru/articles/164173/
- функция send, возволяет отправлять комманды в генератор, управляя итератором

Собственно вы только подтвердили мои слова, что люди видят в генераторах только 1 использование "чтение csv/api для экономии памяти"

Без генератор можно написать итератор руками

А для чего он по-вашему?

изменить контракт

Это не эквивалентное действие. С изменением контракта в вызывающем коде появится новый цикл. Вы не сможете изменить контракт и не менять структуру вызывающего кода.

не оборачивать в функцию

Не оборачивать в функцию что конкретно? Код, который не загружает все данные для цикла сразу, то есть экономит память.

датапровайдеры в тестах удобнее на генераторах

Это просто побочный эффект одного из кейсов использования, который при желании можно решить и без генератора.

создание итератора с сложной логикой, которая менее неудобна при формировании полного массива.

Да ничего вам не мешает вместо yield складывать в массив и возвращать его из функции, чтобы вызывающий код вместо сложной логики сделал простой foreach.

посмотрите статью про генераторы и корутины

"Таким образом вы можете работать с очень большими наборами данных, не загружая их всех в память."

Я сильно сомневаюсь, что разработчики языка планировали имитацию многопоточности как основное назначение генераторов. Я вообще не вижу у корутин конкретно в PHP возможного практического применения.

люди видят в генераторах только 1 использование

Ваш изначальный вопрос был в другом. "Почему генераторы ассоциируют всегда с экономией памяти?"

https://www.php.net/manual/en/language.generators.overview.php

"A generator offers a convenient way to provide data to foreach loops without having to build an array in memory ahead of time, which may cause the program to exceed a memory limit"

Как видите, сами разработчики языка говорят, что это способ использовать меньше памяти.

Разработчики говорят, что это способ использовать меньше памяти с циклом foreach. Увы, у большинства читателей этой фразы проблемы с глазами, и часть про foreach они в упор не видят.

И в чем разница? Неассоциативные массивы обычно нужны, чтобы обработать их через foreach. Большинство читателей знают значение слова "контекст", и подразумевают foreach по умолчанию.

На редкость бессмысленная статья. Выглядит так, как будто мне хотят продать кусок дерева под видом чудодейственной реликвии, а местами напоминает анекдот. Первая же строчка,

Вы когда-нибудь пытались загрузить в память CSV-файл на миллион строк?

сразу напоминает бессмертное "Доктор, когда я делаю так, мне больно." А мысль не загружать в память CSV-файл на миллион строк в голову не приходила?

Но самое главное, нигде никак не объясняется, зачем делать

function fetchCars(): Generator {
    $page = 1;
    do {
        $data = apiRequest('cars', ['page' => $page]);
        foreach ($data['items'] as $car) {
            yield $car;
        }
        $page++;
    } while (!empty($data['items']));
}

если можно сделать

    $page = 1;
    do {
        $data = apiRequest('cars', ['page' => $page]);
        // process data
        $page++;
    } while (!empty($data['items']));
}

Ведь если цель - экономия памяти, то в каждом случае можно обойтись исходным циклом!

while (($row = fgetcsv($handle)) !== false) {
    // Обрабатываем строку
}

И никаких генераторов не нужно!

На середине я было понадеялся, что прочитаю что-то новое про итераторы - там обещалось всякое, про "хранить состояние, управлять ключами или даже динамически менять источник данных" - но увы, из всего этого великолепия в наличии оказалась только копипаста примера из мануала.

Для кого эта статья - непонятно. Зачем она автору - загадка.

Спасибо за подробный разбор 🙌

Вы правы: если задача — один раз пройти CSV и сразу обработать строки, то достаточно while (($row = fgetcsv(...))) {...} и никаких генераторов не нужно. В статье я хотел показать другой ракурс: когда важно разделить источник данных и обработку, чтобы не привязывать логику к месту, где читаем/пагинируем, и чтобы потребитель мог остановиться раньше, отфильтровать и т. п.

Про пример с API: да, можно «прямо в цикле» ходить по страницам. Генератор тут полезен тем, что делает пагинацию ленивой и переиспользуемой — потребитель сможет взять ровно столько, сколько нужно, и теми же конструкциями работать с любым источником (файл, БД, API):

function fetchCars(): Generator {
    for ($page = 1; ; $page++) {
        $data = apiRequest('cars', ['page' => $page]);
        if (empty($data['items'])) break;
        foreach ($data['items'] as $car) yield $car;
    }
}

// Пример: взять первые 100 машин дороже 1 млн, лишние страницы не дергаются
foreach (take(filter(fetchCars(), fn($c) => $c['price'] > 1_000_000), 100) as $car) {
    process($car);
}

function filter(Traversable $src, callable $pred): Generator {
    foreach ($src as $x) if ($pred($x)) yield $x;
}
function take(Traversable $src, int $n): Generator {
    $i = 0; foreach ($src as $x) { if ($i++ >= $n) break; yield $x; }
}

Если цель — именно экономия памяти, она и так достигается стримингом. Фишка генераторов — унификация интерфейса: можно вернуть «поток» из функции без аккумуляции массива или без передачи коллбэка, а дальше свободно комбинировать map/filter/take, менять источник динамически и останавливать обработку раньше (меньше I/O).

Всё это очень хорошо, но почему-то вы мне об этом рассказываете только сейчас в комментариях. Это до боли напоминает реакцию генеративного ИИ, когда тыкаешь его носом в его bullshit.

В статье я хотел показать

Если честно, то сложно поверить, что вы действительно что-то такое хотели. Если бы хотели, то статья была бы построена совершенно по-другому, с упором на "разделить источник данных и обработку" и "унификацию интерфейса". Но в исходном варианте вы об этом даже не заикнулись - там один только стандартный бубнёж про "экономию памяти". При этом исправленный вариант не стал лучше, акцент так и остался на экономии памяти.

Пример: взять первые 100 машин дороже 1 млн, лишние страницы не дергаются

Это какой-то высосанный из пальца пример. Если в API есть сортировка или условия, то их и надо использовать. Если нет - то надо не с умным лицом делать вот это вот take filter, а тупо загнать всю инфу в локальную БД и уже по ней искать нормальными средствами, а не перебором.
И про "лишние страницы не дёргаются" вас обманули. Вы дёргаете лишние страницы до тех пор, пока не наберете случайно разбросанные строки, попадающие под условие. Фасад красивый, а внутри та же неоптимальность. Если задать не миллион, а, скажем, 10, то ваш хвалёный генератор так и будет молотить, пока не переберёт все страницы.

И напоследок - совет: никогда не скармливайте фидбек по статье той ЛЛМ, которая её написала. Она запутается и будет ещё хуже.

В первом сезоне в качестве великого решения всех проблем, позволяющего стать царицею морскою "хранить состояние, управлять ключами или даже динамически менять источник данных", вы нам продавали итератор. А в исправленном и дополненном издании со всеми этими задачами прекрасно справился... генератор! И статья в итоге приобрела лёгкий оттенок шизофрении. Вы уж тогда что ли совсем задвиньте эти итераторы пяткой под шкаф, чтобы не отсвечивали - всё равно они тут были ни к селу, ни к городу.

зачем делать если можно сделать

А вот вы напишите вызывающий код полностью, а не с магическим "process data", тогда и будет понятно зачем.

fgetcsv
И никаких генераторов не нужно!

fgetcsv внезапно работает аналогично генератору.
И у вас handle опять сам магически откуда-то появляется. Если вы захотите поместить логику его получения и закрытия в отдельную функцию, чтобы ее не было в коде с циклом, то без генераторов вам надо будет загружать все строки в память.

Да не, сам по себе генератор - это не бином Ньютона. И при правильном применении сделает код удобнее и проще. Вот только автору статьи про это неизвестно. Ну точнее не было известно на момент написания статьи.

У вас стандартная позиция - “всё бессмысленно”, и объяснили вы всё не так, как я хотел. Окей, зафиксировал.

Просто обратите внимание, статья изначально писалась для среднего уровня.

Статья ваша подходит только для более менее продвинутого уровня, когда программист уже сам разобрался с генераторами и понимает что в статье написана написана чушь (извините уж за резкость). Для новичков и среднего уровня такие статьи очень вредны, потому что закладывают ложные убеждения.

Комментаторы выше абсолютно правы - генераторы это не про память, а про организацию кода.

Это какая-то дурацкая отмазка, я её слышу уже 20 лет. "статья делалась для начинающих" (и это каким-то образом оправдывает наличие в коде SQL инъекций). "Статья писалась для среднего уровня" (и поэтому в ней заявляется рассказ об итераторах, но никакой информации по ним прочему-то не приводится). Впору воскликнуть, вслед за Вовочкой из анекдота, "Где логика, где разум?".

Отсылка к воображаемой аудитории не является универсальным оправданием для кривого кода или дурацкой статьи.

Я вам привел конкретные замечания - упор на экономию памяти, полное отсутствие упоминания настоящих причин использовать генераторы, отсутствие заявленной информации про итераторы. Вы пошли к своему ИИ за исправлениями и ещё больше запутались. В итоге обиделись почему-то на меня и вернули статью в исходное положение. Не думали для разнообразия обидеться на ИИ, который вам это всё подсунул? Или на свою попытку написать статью по теме, в которой сами ничего не понимаете, а полагаетесь на мнение ИИ?

Вы ИИ упомянули уже 3-6 раза.

И не просто упомянули, а прям уже утверждаете 😅

Потеряли веру в людей? 🙂

На самом деле я согласился с вами и добавил то, на что вы указали (это до сих пор есть в статье).

Вы общаетесь не с ИИ, а с реальным человеком, который потратил своё время, знания и силы, чтобы написать эту статью. Я с уважением воспринимаю критику и очень рад довести материал до идеала. Но когда комментарий содержит что-то вроде: "На редкость бессмысленная статья. Выглядит так, как будто мне хотят продать кусок дерева", "Вы пошли к своему ИИ за исправлениями и ещё больше запутались. "…

Честно говоря, вы настроены на негатив в каждом комментарии, и общаться просто пропадает желание.

Таких статей уже на Хабре (и не только) было пачками... Но почему-то их пишут вновь и вновь.

Нет, генераторы не экономят память.
Да, экономить память можно и с обычными циклами безо всяких генераторов.
Генераторы это про организацию кода и разделение бизнес логики. И кстати, если смотреть в этом разрезе - иногда бывает семантичнее применить файберы вместо генераторов.

Вместо создания массива генератор экономит память, ведь он не держит все элементы сразу.

Покажи, как тут сэкономить память без отказа от массива?

$arr = range(1, 10_000_000); // это сразу съедает сотни МБ и частo упирается в memory_limit

Никак: сам факт range() уже «прожёг» память.

А вот лениво - без промежуточного массива:

function numbers(int $n): Generator {
    for ($i = 1; $i <= $n; $i++) {
        yield $i;
    }
}

$sum = 0;
foreach (numbers(10_000_000) as $x) {
    $sum += $x;
}

Здесь элементы не хранятся в памяти пачкой - выдаются по одному. Пик памяти остаётся малым и не зависит от "размера массива".

Ну вам сто раз уже отвечали на этот вопрос.

    for ($i = 1; $i <= $n; $i++) {
        $sum += $x;
    }

Генераторы. Не. Про. Экономию. Памяти.
Память они экономить не умеют.
Память экономит цикл, который работает у него внутри.
Этот цикл без генератора прекрасно сэкономит память.
Генератор без этого цикла не с экономит вам ни байта.
Реальная польза генераторов не про память.
А про то, что вы сами же писали в комментарии выше: про "разделение источника данных и их обработчика" и "унификацию интерфейса".

Этот цикл без генератора прекрасно сэкономит память.

Ну да, цикл можно заставить работать и без генератора и при этом экономить память, если переписать цикл и работать просто с числами.
Или как вы советуете выше "тупо загнать всё в локальную БД..." 😅

Только вот вопрос был: "Покажи, как тут сэкономить память без отказа от массива".

Читаем внимательно: "без отказа от массива".

Кстати, вы так и не ответили на него, потому что читаете невнимательно.

Согласен, я прочитал невнимательно. Но опять же, вы об этом пишете в комментарии. В статье это ваше "без отказа от массива" исходно не упоминалось вообще, а потом вы добавили короткую отписку "на отвали", всю суть оставив прежней, "ололо, генераторы экономят память!!!".

Потом начитавшись таких статей очередной гений засовывает в генератор массив и гордо пишет "Memory Efficient: Built with generators to process massive XML without running out of memory." И ведь он прав! С точки зрения утверждения "генераторы экономят память" у него всё в порядке. Засунул массив внутрь генератора - сэкономил память.

Именно поэтому ваша статья приносит вред

Причём вред её многократно умножается тем, что она повторяет ту же глупость, что и множество других.

А то что вы начинаете где-то в комментариях добавлять "без отказа от массива" суть статьи не меняет вообще никак.

Теперь давайте разбираться с этим вашим "без отказа от массива". Получается, что генератор экономит память тогда, когда обработчик по какой-то причине требует массив. А требовать он может только ради более оптимального/универсального кода. То есть и получается, что генератор позволяет писать более универсальный код, разделять источник получения данных и их обработчик, сохраняя при этом эффективность по памяти. В от это была бы нормальная формулировка, и вам бы сказали большое спасибо за статью.

Если бы вы сначала показали эффективный по памяти, но организационно неоптимальный код, потом предложили сделать его более универсальным, с разделением источника и обработчика данных с помощью массива - но при этом упёрлись бы в неоптимальность по памяти, и в финале представили бы генератор, завернув в него исходный оптимальный по памяти код, то это была бы прекрасная статья, в которой правильно расставлены акценты: память экономит всё тот же старый while или for, а генератор просто позволяет сделать этот код мобильным, предоставляет универсальный интерфейс для любых циклов.

А так у вас получился просто мусор, непонятно зачем написанный (таких статей и так как грязи) и непонятно для кого.

И отдельно я напишу про

"тупо загнать всё в локальную БД..." 😅

Я на этом не останавливался раньше, но стоило, да. Если мне сотрудник принесёт вот такой вот код из вашего комментария,

// Пример: взять первые 100 машин дороже 1 млн, лишние страницы не дергаются
foreach (take(filter(fetchCars(), fn($c) => $c['price'] > 1_000_000), 100) as $car) {
    process($car);
}

Код, который каждый раз ходит за выборкой напрямую во внешнее API - а не закэширует сначала данные локально - то такой сотрудник вылетит с работы без выходного пособия.

Покажи, как тут сэкономить память без отказа от массива?


Вы что и правда не понимаете? просто копируете ответы из LLM?
Будущее уже наступило...

Чтобы в приведенном примере сэкономить память - генератор не нужен. Достаточно не пытаться создать гигантский массив. Хотите посчитать сумму чисел от 1 до 10М? Ну вот её и считайте, без массивов и генераторов.

Ещё один человек, ссылающийся на LLM - это сейчас так модно?

Вопрос был не об "Хотите посчитать сумму чисел от 1 до 10M?". Вопрос был в другом. Вы, кстати, так и не смогли на него ответить, а просто ушли в сторону.

Одно не могу понять, зачем из файла извлекать миллион записей, что бы их сохранить в бд? Извлекаете порциями и сохраняйте после извлечения

Ну так генератор это и есть место, чтобы поместить туда этот код, сохраняя простой foreach в вызывающем коде.

зачем из файла извлекать миллион записей, что бы их сохранить в бд?

Потому что бизнес-требования такие?

Вы или троллите, или не можете воспринять два предложения вместе. Оба варианта вас не красят.

Здесь нет вопроса, куда поместить этот код. И поэтому ваш ответ, "генератор это и есть место, чтобы поместить туда этот код" не имеет смысла.

Если читать вопрос целиком, то он звучит очень просто - зачем сначала читать ВЕСЬ миллион записей, и только потом начинать записывать в БД? Почему нельзя это делать поблочно? И это очень хороший, логичный вопрос. На который автор не дал ответа.

Не дали его и вы. Вопрос, повторюсь, был совсем не про взывающий код. А про осмысленность исходного алгоритма - сначала читать, а потом только писать.

Правильным подходом здесь будет прочитать первые десять тысяч и записать их в БД. Потом прочитать следующие 10, и снова записать. И так до конца файла. Это основной, базовый алгоритм. При этом генератор для него совершенно некритичен, он здесь является синтаксическим сахаром. И с ним, и без него, алгоритм поблочной записи работает одинаково. Разница только в "вызывающем коде", который ни к алгоритму, ни вопросу в исходном комментарии отношения не имеет.

Потому что бизнес-требования такие?

И здесь опять вы опять вырвали предложение из контекста. Вопрос формулируется из двух предложений. Второе уточнят первое. То есть вопрос не "зачем извлекать и записывать", а "почему не извлекать порциями?". При том что весь миллион вы всё равно за раз не запихнёте - попросту упрётесь в max_allowed_packet. То есть читать весь миллион - хоть с генератором, хоть без - нет никакого смысла.

Здесь нет вопроса, куда поместить этот код.

И? Там есть предложение написать этот код. Мое высказывание это возражение на это утверждение, а не ответ на какой-то вопрос. Не все высказывания являются ответами на вопросы.

И поэтому ваш ответ, "генератор это и есть место, чтобы поместить туда этот код" не имеет смысла.

Имеет. Если вы его не поняли, это не значит, что его нет.
Это указание на то, что предложение "Извлекаете порциями и сохраняйте после извлечения" и так всем понятно, и это оно не имеет смысла.

зачем сначала читать ВЕСЬ миллион записей, и только потом начинать записывать в БД?

Затем, чтобы не менять вызывающий код, в котором уже написан foreach. Или не усложнять, если еще не написан.

Не дали его и вы.

Дал, просто вы невнимательно читаете.
А в этой ветке этот ответ не нужен.

Это основной, базовый алгоритм.

Нет, это уже оптимизация базового алгоритма.

Разница только в "вызывающем коде", который ни к алгоритму, ни вопросу в исходном комментарии отношения не имеет.

Он имеет отношение к генераторам. В контексте которых написан исходный комментарий.

И здесь опять вы опять вырвали предложение из контекста.

Не вырвал из контекста, а показал, что это глупый вопрос. Если бы он был задан как-то по-другому, то возможно он не выглядел бы глупо. Из файла нужно извлечь миллион записей и все их сохранить в БД, нет смысла спрашивать зачем.

То есть вопрос не "зачем извлекать и записывать", а "почему не извлекать порциями?"

Ну вот так и надо было спрашивать.

То есть читать весь миллион - хоть с генератором, хоть без - нет никакого смысла.

Есть, если памяти достаточно, проще сделать функцию, которая извлекает из файла сразу все данные, и не возиться с генератором.

Чет пробовал оптимизацию через генераторы - не пройти существующтй массив с меньшей памятью. Что до работы с csv - тут не скажу

Всё правильно. Потому что генераторы вообще не про память. А про более удобную организацию кода.

Потому что генераторы вообще не про память.

Я вам уже доказал со ссылкой на документацию, что это утверждение ложно. Меньший расход памяти в определенных операциях является основной причиной их добавления.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации