Php yield


Php yieldНесмотря на то, что php-генераторы доступны с php 5.5.0, они все еще почти не используются. Более того, большинство разработчиков, которых я знаю, понимают, как работают генераторы, но не видят, когда они могут быть полезны в реальной жизни.

Да, генераторы определенно смотрятся хорошо, но знаете…  Я не понимаю, где они могут быть полезными для меня, разве что для расчета последовательности Фибоначчи.

И они не ошибаются, ведь даже примеры в php-документации слишком упрощены. Они только объясняют, как эффективно реализовать range или итерировать по строкам файла.

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

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

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

Сначала немного контекста


Я работаю в TEA. В основном, мы разрабатываем экосистему для электронных книжек. Это покрывает весь путь от получения файлов нужного формата от издателей до размещения их на e-commerce сайте и предоставления конечному потребителю возможности читать онлайн (используя браузер, написанный @johanpoirier) или с электронной книги.

Для возможности продавать книги и отображения релевантной информации потребителям, нам нужно много метаданных продуктов (заголовок, формат, цена, издатели, автор(ы), …).

В большинстве примеров кода дальше я буду называть эти метаданные $ebooks.

Поехали!

Итерация по крупному множеству данных

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

По традиции я должен был бы написать что-то вроде:

  <?php    private function getEbooksEligibleToWebReader($ebooks)  {   $rule = 'format = "EPUB" AND protection != "Adobe DRM"';   $filteredEbooks = [];     foreach ($ebooks as $ebook) {   if ($this->rulerz->satisfies($ebook, $rule)) {   $filteredEbooks[] = $ebook;   }   }     return $filteredEbooks;  }  

Проблему легко увидеть: чем больше книг, тем больше нужно памяти для $filteredEbooks.

Одно из решений — создать итератор, который бы итерировал $ebooks и возвращал подходящие. Но для этого нам нужно было бы создать новый класс, кроме того, итераторы реализируются немного утомительно… К счастью, с php 5.5.0 мы можем использовать генераторы!

  <?php    private function getEbooksEligibleToWebReader($ebooks)  {   $rule = 'format = "EPUB" AND protection != "Adobe DRM"';     foreach ($ebooks as $ebook) {   if ($this->rulerz->satisfies($ebook, $rule)) {   yield $ebook;   }   }  }

Да, рефакторинг метода getEbooksEligibleToWebReader для использования генератора очень прост: заменяем передачу значений в переменную $filteredEbooks конструкцией yield.

Предположив, что $ebooks не массив книг, а итератор, или генератор (даже лучше!), потребление памяти теперь будет константой, не важно, сколько книг нужно вернуть, и мы уверены, что книги будут искаться только когда реально понадобятся.

Бонус: RulerZ внутри использует генераторы, так что мы можем переписать метод и остаться с той же оптимизацией по выделению памяти.

  <?php    private function getEbooksEligibleToWebReader($ebooks)  {   $rule = 'format = "EPUB" AND protection != "Adobe DRM"';     return $this->rulerz->filter($ebooks, $rule);  }  

Агрегация нескольких источников данных

Теперь рассмотрим момент получения $ebooks. Я вам не сказал, но они по факту приходят с разных источников: реляционной БД и Elasticsearch.

Мы можем написать простой метод, агрегирующий эти два источники:

  <?php    private function getEbooks()  {   $ebooks = [];     // fetch from the DB   $stmt = $this->db->prepare("SELECT * FROM ebook_catalog");   $stmt->execute();   $stmt->setFetchMode(PDO::FETCH_ASSOC);     foreach ($stmt as $data) {   $ebooks[] = $this->hydrateEbook($data);   }     // and from Elasticsearch (findAll uses ES scan/scroll)   $cursor = $this->esClient->findAll();     foreach ($cursor as $data) {   $ebooks[] = $this->hydrateEbook($data);   }     return $ebooks;  }

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

Мы можем начать использовать генераторы и возвратить результат:

  <?php    private function getEbooks()  {   // fetch from the DB   $stmt = $this->db->prepare("SELECT * FROM ebook_catalog");   $stmt->execute();   $stmt->setFetchMode(PDO::FETCH_ASSOC);     foreach ($stmt as $data) {   yield $this->hydrateEbook($data);   }     // and from Elasticsearch (findAll uses ES scan/scroll)   $cursor = $this->esClient->findAll();     foreach ($cursor as $data) {   yield $this->hydrateEbook($data);   }  }  

Так, конечно, лучше, но у нас все равно есть проблема: наш метод getBooks выполняет слишком много работы! Мы должны разделить две ответственности (считывание данных с БД и вызов Elasticsearch) в два метода:

  <?php    private function getEbooks()  {   yield from $this->getEbooksFromDatabase();   yield from $this->getEbooksFromEs();  }    private function getEbooksFromDatabase()  {   $stmt = $this->db->prepare("SELECT * FROM ebook_catalog");   $stmt->execute();   $stmt->setFetchMode(PDO::FETCH_ASSOC);     foreach ($stmt as $data) {   yield $this->hydrateEbook($data);   }  }    private function getEbooksFromEs()  {   // and from Elasticsearch (findAll uses ES scan/scroll)   $cursor = $this->esClient->findAll();     foreach ($cursor as $data) {   yield $this->hydrateEbook($data);   }  }

Вы могли заметить использование yield from оператора (доступен с php 7.0), который позволяет делегировать использование генераторов. Это идеально, к примеру, для агрегации нескольких источников данных, которые используют генераторы.

yield from оператор работает с любым Traversable объектом, так что массивы и итераторы также могут быть использованы с этим оператором.

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

   

<?php private function getEbooks() { yield new Ebook(…); yield from [new Ebook(…), new Ebook(…)]; yield from new ArrayIterator([new Ebook(…), new Ebook(…)]); yield from $this->getEbooksFromCSV(); yield from $this->getEbooksFromDatabase(); }

Сложная ленивая (по требованию) гидрация записей БД

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

Мне нужно было импортировать сотни тысяч заказов с давней БД в нашу систему, каждый заказ содержал несколько пунктов.

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

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

  <?php    public function loadOrdersWithItems()  {   $oracleQuery = <<<SQL  SELECT o.*, item..  

($currentOrderId !== null) { yield $currentOrder; } $currentOrderId = $row['ID']; $currentOrder = $row; $currentOrder['lines'] = []; } $currentOrder['lines'][] = $row; } yield $currentOrder; }

Используя генераторы, мне удалось реализовать метод, который получает заказы с БД и присоединяет соответствующие пункты заказа. Все это потребляет стабильное количество памяти. Генератор избавил от надобности держать все заказы и их пункты: текущий заказ — это все, что мне нужно, чтобы сагрегировать все данные.

Имитация асинхронных задач

Последнее, но тем не менее важное: генераторы так же могут быть использованы для имитации асинхронных задач. Пока я писал эту заметку, я наткнулся на пост @nikita_ppv на такую же тематику, и так как он первый реализовал генераторы в php, я просто оставлю ссылку на его пост.

Он быстро объясняет что такое генераторы и (в деталях) как мы можем получить преимущества из-за того, что они могут быть прерваны и отправлять/принимать данные для реализации сопрограмм и даже многозадачности.

Подводя итог

Генераторы


  • … упрощенные итераторы;
  • … могут возвращать неограниченные объемы данных без дополнительного потребления памяти;
  • … могут быть агрегированы с помощью делегирования генераторов;
  • … могут быть использованы для реализации многозадачности;
  • … просто прикольные!

Оригинал статьи

folkprog.net

Самым большим нововведением (и очень важным для языка) в PHP 5.5.0 будут генераторы. Я установил себе альфа-версию 5.5.0, хотел посмотреть поближе что да как, генераторы мне нравятся в Пайтоне и я рад, что PHP ими обзаведётся.

Посмотрите на код, который я написал, пока мы ждали всех участников встречи, на которую я был приглашён:

<? function enumerate($arr) {  $i = 0;   foreach ($arr as $value) {  yield [$i++, $value];  } }  function ifilter(callable $predicate = null, $arr) {  if ($predicate === null) {  $predicate = 'boolval';  }   foreach ($arr as $value) {  if ($predicate($value)) {  yield $value;  }  } }  function islice($arr, $start, $stop) {  if (is_array($arr)) {  reset.  

($x % 10) === 0; }, $array )), 1, 4 ); foreach ($gen as $value) { print_r($value); }

Три генератора, пропускающие через себя данные, обрабатывают массив, всё работает. Названия взяты из Пайтона. Первый нумерует входящий поток данных (возвращает пары номер и значение), второй фильтрует значения через функцию, третий отрезает заданную часть от входных данных.

Концепция генераторов проста, как герань. PHP поддерживает два способа их создания (как и Пайтон), но мне милее оператор yield. Работает он примерно как return, но при повторном входе в функцию её выполнение происходит не сначала, а с того места, откуда управление было возвращено при помощи yield. Можно ещё передавать значения внутрь генератора, но я про это сейчас не буду. Вцелом, получаются настоящие ленивые вычисления — всё это вызывается ровно тогда, когда это реально нужно.

Генераторы, обычно, работают с итерируемыми объектами — возращают их и/или принимают. Итерируемый объект, проще говоря, это то, по чему можно пробежаться циклом. Генераторы, кстати, тоже итерируемые.


И вот тут опять проявляются недостатоки PHP: отсутствие общей логики, непродуманность.

Массив тут до сих пор примитивный тип и он требует иной способ обработки. Посмотрите на генератор islice, тут проблема видна во всей красе. Начинается она уже с хинтинга, я не могу потребовать от $arr быть типа array, потому что на вход может прийти другой генератор, и указать класс генератора тоже не могу — массив ему не принадлежит. Думаю, следует ожидать появления нового типа «iterable», как это уже случилось с callable.

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

Мне кажется, PHP следует развивать класс ArrayObject, который уже присуствует в языке и делать прозрачное преобразование примитивных типов в объекты (как это происходит в JavaScript), это решило бы некоторые проблемы.

bolknote.ru

При вождении автомобиля – скорость это далеко не все. Но в WEB все решает скорость. Чем быстрее ваше приложение, тем лучше пользовательский опыт. Хорошо, эта статья о генераторах в PHP, так почему же мы говорим о скорости? Как вы увидите вскоре, генераторы привносят большие изменения по части скорости и потреблении памяти приложением.


Что такое PHP генераторы?

Добавленные в PHP в версии 5.5, генераторы представляют собой функции, обеспечивающие простой механизм для циклической обработки данных, без необходимости создавать массив данных в памяти. Все еще не понимаете о чем речь? Тогда давайте посмотрим на PHP генераторы в действии.

Создаем файл generator_test.php со следующим содержанием:

<?php

  function getRange( $max = 10 ) {
   $array = [];
   for( $i = 0; $i < $max; $i++ ) {
    $array[] = $i;
   }
   return $array;
  }

  foreach (getRange(15) as $range) {
   echo "Данные {$range} <br>";
  }

?>

Затем в папке где у нас лежит этот файл открываем консоль и пишем следующее

  php -S localhost:8000

Дальше открываем браузер и идем по следующему адресу:

  http://localhost:8000/generator_test.php

Результат будет такой:

  Данные 1
  Данные 2
  ….
  Данные 15

Код выше достаточно прост. Однако, давайте сделаем небольшое изменение в нем:

<?php

  foreach (getRange(PHP_INT_MAX) as $range) {
   echo "Данные {$range} <br>";
  }

?>

Теперь диапазон генерируемых чисел находится в пределах от 0 до константы PHP_INT_MAX, которая представляет собой наибольшее целое число, которое способен представить интерпретатор PHP. После этого опять идем в браузер и обновляем страницу. Однако на этот раз, вместо обычного текста получаем сообщение о том, что превышен объем доступной памяти, вследствие чего работа скрипта была аварийно завершена.

Что за досада – у PHP закончилась память! Первое что приходит на ум – это редактировать настройку memory_limit в php.ini. Но давайте спросим себя – действительно ли это так эффективно? Неужели мы хотим, чтобы какой-то единственный скрипт занимал всю доступную память?

Используем генераторы

Давайте напишем ту же самую функцию, что и выше, вызовем ее с тем же значением PHP_INT_MAX и запустим снова. Но в этот раз мы создадим функцию-генератор.

<?php

  function getRange( $max = 10 ) {
   for( $i = 1; $i < $max; $i++ ) {
    yield $i;
   }
  }

  foreach (getRange(PHP_INT_MAX) as $range) {
   echo "Данные {$range} <br>";
  }

?>

Определяя функцию getRange на этот раз, мы всего лишь проходим по значениям и генерируем вывод. Ключевое слово yield похоже на инструкцию return тем, что возвращает значение из функции, но единственное отличие заключается в том, что yield возвращает значение только тогда, когда это необходимо и не пытается вместить весь массив данных в память за один раз. Перейдя к браузеру, вы должны увидеть данные, отображаемые на странице. Обратите внимание на тот факт, что генераторы в PHP могут быть использованы только лишь из функции.

Зачем нужны генераторы?

Время от времени возникают такие задачи, когда нам необходимо обработать большие объемы данных (например, файлы логов), выполнить вычисления на больших выборках из базы и т.д. И мы отнюдь не хотим, чтобы эти операции занимали всю доступную память, так мы должны стараться сохранять память насколько это возможно. Данные не обязательно должны быть большими – PHP генераторы эффективны вне зависимости от размера данных. И не забывайте, что наша цель – сделать приложение быстрым и при этом таким, чтобы оно потребляло как можно меньше памяти.

Возврат ключей

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

<?php

  function getRange( $max = 10 ) {
   for( $i = 0; $i < $max; $i++ ) {
    $value = $i * mt_rand();
    yield $i => $value;
   }
  }
?>

Использовать данную функцию мы можем также как и простой массив:

<?php

  foreach (getRange(PHP_INT_MAX) as $key => $value ) {
   echo "Ключ {$key} имеет значение {$value}";
  }

?>

Отсылка значений генераторам

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

<?php

  function getRange( $max = 10 ) {
   for( $i = 1; $i < $max; $i++ ) {
    $inject = yield $i;
    if( $inject === 'stop' ) return;
   }
  }

  $generator = getRange(PHP_INT_MAX);

  foreach( $generator as $range ) {
   if($range === 10000) {
   // посылаем сообщение генератору
   $generator -> send('stop');
   }
   print "Значение {$range} <br>";
  }

?>

Отмечу, что использование инструкции return в функции-генераторе приведет к немедленному выходу из этой функции.

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

Кстати, о генераторах я подробно рассказываю в моем курсе PHP и MySQL с Нуля до Гуру 2.0. Там есть и примеры и задания, которые помогут лучше усвоить материал.

myrusakov.ru

Что такое yield?

Ключевое слово yield возвращает данные из функции генератора:

Сердцем функции генератора является ключевое слово yield. В своей простейшей форме оператор yield выглядит так же, как оператор return, за исключением того, что вместо того, чтобы прекратить выполнение функции и возврата, yield вместо этого дает значение циклу цикла над генератором и приостанавливает выполнение функции генератора.

Что такое функция генератора?

Функция генератора является фактически более компактным и эффективным способом записи Iterator. Он позволяет вам определить функцию (ваш xrange), которая будет вычислять и возвращать значения, пока вы перебираете ее:

foreach (xrange(1, 10) as $key => $value) {  echo "$key => $value", PHP_EOL; } 

Это создаст следующий вывод:

0 => 1 1 => 2 … 9 => 10 

Вы также можете управлять $key в foreach с помощью

yield $someKey => $someValue; 

В генераторной функции $someKey — это то, что вы хотите, для $key и $someValue, являющегося значением в $val. В примере вопроса $i.

Какая разница с нормальными функциями?

Теперь вы можете задаться вопросом, почему мы не просто используем PHP native range function для достижения этого результата. И ты прав. Результат будет таким же. Разница в том, как мы туда попали.

Когда мы используем range PHP, выполним его, создадим весь массив чисел в памяти и return весь массив в цикле foreach, который затем перейдет к нему и выведет значения. Другими словами, foreach будет работать на самом массиве. Функция range и foreach только «разговора» один раз. Подумайте, как получить пакет по почте. Парень доставки подаст вам пакет и уйдет. И затем вы разворачиваете весь пакет, вынимая все, что там есть.

Когда мы используем функцию генератора, PHP будет входить в функцию и выполнять ее до тех пор, пока она не встретит конец или ключевое слово yield. Когда он встретится с yield, он затем вернет все, что есть значение в это время, к внешнему циклу. Затем он возвращается в функцию генератора и продолжается от того места, где он уступил. Поскольку ваш xrange содержит цикл for, он будет выполняться и работать до тех пор, пока не будет достигнут $max. Подумайте об этом, как foreach и генератор, играющий в пинг-понг.

Зачем мне это нужно?

Очевидно, что генераторы могут использоваться для ограничения объема памяти. В зависимости от вашей среды выполнение range(1, 1000000) приведет к смерти вашего script, тогда как то же самое с генератором будет работать нормально. Или, как пишет Википедия:

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

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

Другой пример использования для генераторов — это асинхронные сопрограммы. Ключевое слово yield не только возвращает значения, но также принимает их. Подробнее об этом см. В двух замечательных сообщениях в блоге, которые приведены ниже.

С каких пор можно использовать yield?

Генераторы были введены в PHP 5.5. Попытка использовать yield до этой версии приведет к различным ошибкам разбора, в зависимости от кода, следующего за ключевым словом. Поэтому, если вы получаете ошибку синтаксического анализа из этого кода, обновите свой PHP.

qaru.site

Как работают генераторы?

Согласно Википедии генератор “очень похож на функцию, которая возвращает массив, так как генератор имеет входные параметры, его можно вызвать и он генерирует последовательность значений”. В общих чертах генератор — это обычная функция, но вместо возврата одного значения она выдает столько значений, сколько от нее может потребоваться. Генератор выглядит как функция, но ведет себя как итератор.

В генераторах используется ключевое слово yield вместо слова return. Оно ведет себя похожим образом, и возвращает значение в точку вызова, с тем отличием, что функция не удаляется из стека, а ее состояние сохраняется в памяти. Это позволяет функции продолжить работу с предыдущего состояния при следующем ее вызове. На самом деле, вы не можете использовать ключевое слово return для возврата значения из генератора, а для того, чтобы прекратить выполнение генератора.

Руководство PHP гласит: “Когда вызывается генератор, он возвращает объект, который может быть проитерирован”. Он является объектом внутреннего класса Generator, который имплементирует интерфейс Iterator, и ведет себя, как однонаправленный итератор. Пока вы проводите итерацию над объектом, PHP вызывает генератор каждый раз, когда ему нужно получить значение. Состояние сохраняется каждый раз, как генератор выдает значение, так что в следующий раз, когда PHP затребует значение, генератор восстановит свое предыдущее состояние.

Данный код выведет следующее:

Наш первый генератор

Генераторы — не новый концепт, они уже есть в таких языках, как C#, Python, JavaScript и Ruby (счетчики), их обычно можно определить по использованию ключевого слова yield. Данный код — пример на языке Python:

Давайте перепишем образец генератора Python на языке PHP. (заметьте, что оба куска кода не заботятся о проверках ошибок).

Функция-генератор открывает файл и выдает каждую линию файла, если ее затребовали. Каждый раз при вызове генератора он продолжает работу с предыдущего состояния. Работа функции не начинается сначала, так как его состояние было сохранено перед выполнением оператора yield. Как только все строки были перебраны — генератор завершает свою работу, завершается и цикл.

Возврат ключей

Итераторы в PHP состоят из пар “ключ/значение”. В нашем примере мы возвращаем только значение, а ключи в этом случае будут числовыми (ключи по-умолчанию числовые). Если вам необходимо вернуть ассоциативные пары — просто измените формат оператора yield так, чтобы он включал и ключ, используя синтаксис массивов.

Внедрение значений

Оператор yield не только возвращает значения — он может также принимать значения извне. Это делается путем вызова метода send() у объекта генератора с передачей необходимого значения в виде параметра. Это значение может быть использовано в вычислениях или других операциях. Метод передает значение в генератор как результат выполнения yield, и возобновляет выполнение.

Вывод будет следующим:

Экономим память с помощью генераторов

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

Представьте результат работы функции file(), которая возвращает все строки читаемого файла в виде массива. Если сравнить результаты работы функции file() и нашей функции file_lines() над одним и тем же файлом со 100 случайными параграфами текста, то функция file() будет использовать примерно в 110 раз больше памяти, чем генератор.

Заключение

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

ruseller.com

With yield you can easily describe the breakpoints between multiple tasks in a single function. That’s all, there is nothing special about it.

$closure = function ($injected1, $injected2, ...){  $returned = array();  //task1 on $injected1  $returned[] = $returned1; //I need a breakpoint here!!!!!!!!!!!!!!!!!!!!!!!!!  //task2 on $injected2  $returned[] = $returned2;  //...  return $returned; }; $returned = $closure($injected1, $injected2, ...); 

If task1 and task2 are highly related, but you need a breakpoint between them to do something else:

  • free memory between processing database rows
  • run other tasks which provide dependency to the next task, but which are unrelated by understanding the current code
  • doing async calls and wait for the results
  • and so on …

then generators are the best solution, because you don’t have to split up your code into many closures or mix it with other code, or use callbacks, etc… You just use yield to add a breakpoint, and you can continue from that breakpoint if you are ready.

Add breakpoint without generators:

$closure1 = function ($injected1){  //task1 on $injected1  return $returned1; }; $closure2 = function ($injected2){  //task2 on $injected2  return $returned1; }; //... $returned1 = $closure1($injected1); //breakpoint between task1 and task2 $returned2 = $closure2($injected2); //... 

Add breakpoint with generators

$closure = function (){  $injected1 = yield;  //task1 on $injected1  $injected2 = (yield($returned1));  //task2 on $injected2  $injected3 = (yield($returned2));  //...  yield($returnedN); }; $generator = $closure(); $returned1 = $generator->send($injected1); //breakpoint between task1 and task2 $returned2 = $generator->send($injected2); //... $returnedN = $generator->send($injectedN); 

note: It is easy to make mistake with generators, so always write unit tests before you implement them! note2: Using generators in an infinite loop is like writing a closure which has infinite length…

stackoverflow.com

Что такое yield ?

Ключевое слово yield возвращает данные из функции генератора:

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

Что такое функция генератора?

Функция генератора представляет собой более компактный и эффективный способ записи Iterator . Он позволяет вам определить функцию (ваш xrange ), которая будет вычислять и возвращать значения, пока вы зацикливаетесь на ней :

foreach (xrange(1, 10) as $key => $value) {  echo "$key => $value", PHP_EOL; } 

Это создаст следующий результат:

0 => 1 1 => 2 … 9 => 10 

Вы также можете управлять $key в foreach , используя

yield $someKey => $someValue; 

В функции генератора $someKey — это то, что вы хотите, для $key а $someValue — значение в $val . В примере вопроса это $i .

Какая разница с нормальными функциями?

Теперь вы можете задаться вопросом, почему мы не просто используем собственную функцию range PHP для достижения этого результата. И ты прав. Результат будет таким же. Разница в том, как мы туда попали.

Когда мы будем использовать range PHP, он выполнит его, создаст весь массив чисел в памяти и return весь массив в цикл foreach который затем переместит его и выведет значения. Другими словами, foreach будет работать на самом массиве. Функция range и foreach только «разговора» один раз. Подумайте, как получить пакет по почте. Парень доставки подаст вам пакет и уйдет. И затем вы разворачиваете весь пакет, вынимая все, что там.

Когда мы используем функцию генератора, PHP будет входить в функцию и выполнять ее до тех пор, пока она не встретит конец или ключевое слово yield . Когда он удовлетворяет yield , он затем вернет то, что является значением в это время для внешнего цикла. Затем он возвращается в функцию генератора и продолжается от того места, где он уступил. Поскольку ваш xrange имеет цикл for , он будет выполняться и уступать до достижения $max . Подумайте об этом, как в foreach и генераторе, играющем в пинг-понг.

Зачем мне это нужно?

Очевидно, что генераторы могут использоваться для ограничения объема памяти. В зависимости от вашей среды выполнение range(1, 1000000) приведет к фатальному сценарию, тогда как то же самое с генератором будет работать нормально. Или, как пишет Википедия:

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

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

Другой пример использования для генераторов — это асинхронные сопрограммы. Ключевое слово yield не только возвращает значения, но также принимает их. Подробнее об этом см. В двух замечательных сообщениях в блоге, приведенных ниже.

С каких это пор я могу использовать yield ?

Генераторы были представлены в PHP 5.5 . Попытка использовать yield до этой версии приведет к различным ошибкам разбора, в зависимости от кода, следующего за ключевым словом. Поэтому, если вы получите ошибку синтаксического анализа из этого кода, обновите свой PHP.

code-examples.net

The “yield” keyword is new in PHP 5.5. This keyword allows you to program “generators”. Wikipedia explains generators accurately:

A generator is very similar to a function that returns an array, in that a generator has parameters, can be called, and generates a sequence of values. However, instead of building an array containing all the values and returning them all at once, a generator yields the values one at a time, which requires less memory and allows the caller to get started processing the first few values immediately. In short, a generator looks like a function but behaves like an iterator.

The concept of generators is not new. The “yield” keyword exists in other programming languages as well. As far as I know C#, Ruby, Python, and JavaScript have this keyword. The first usage that comes to mind for me is when I want to read a big text file line-by-line (for instance a log file). Instead of reading the whole text file into RAM you can use an iterator and still have a simple program flow containing a “foreach” loop that iterates over all the lines. I wrote a small script in PHP that shows how to do this (efficiently) using the “yield” keyword:

  <?php  class File {     private $file;   private $buffer;     function __construct($filename, $mode) {   $this->file = fopen($filename, $mode);   $this->buffer = false;   }     public function chunks() {   while (true) {   $chunk = fread($this->file,8192);   if (strlen($chunk)) yield $chunk;   elseif (feof($this->file)) break;   }   }     function lines() {   foreach ($this->chunks() as $chunk) {   $lines = explode("n",$this->buffer.$chunk);   $this->buffer = array_pop($lines);   foreach ($lines as $line) yield $line;   }   if ($this->buffer!==false) {    yield $this->buffer;   }   }     // ... more methods ...  }    $f = new File("data.txt","r");  foreach ($f->lines() as $line) {   echo memory_get_usage(true)."|$linen";  }  

One of my colleagues asked me why I used “fread” and did not simply call PHP’s “fgets” function (which reads a line from a file). I assumed that he was right and that it would be faster. To my surprise the above implementation is (on my machine) actually faster than the “fgets” variant that is shown below:

  <?php  class File {     private $file;     function __construct($filename, $mode) {   $this->file = fopen($filename, $mode);   }     function lines() {   while (($line = fgets($this->file)) !== false) {   yield $line;   }   }     // ... more methods ...  }    $f = new File("data.txt","r");  foreach ($f->lines() as $line) {   echo memory_get_usage(true)."|$line";  }  

I played around with the two implementations above,  and found out that the execution speed and memory usage of the first implementation is dependent on the amount of bytes read by “fread”. So I made a benchmark script:

  <?php  class File {     private $file;   private $buffer;   private $size;     function __construct($filename, $mode, $size = 8192) {   $this->file = fopen($filename, $mode);   $this->buffer = false;   $this->size = $size;   }     public function chunks() {   while (true) {   $chunk = fread($this->file,$this->size);   if (strlen($chunk)) yield $chunk;   elseif (feof($this->file)) break;   }   }     function lines() {   foreach ($this->chunks() as $chunk) {   $lines = explode("n",$this->buffer.$chunk);   $this->buffer = array_pop($lines);   foreach ($lines as $line) yield $line;   }   if ($this->buffer!==false) {    yield $this->buffer;   }   }  }    echo "size;memory;timen";  for ($i=6;$i<20;$i++) {   $size = ceil(pow(2,$i));   // "data.txt" is a text file of 897MB holding 40 million lines   $f = new File("data.txt","r", $size);   $time = microtime(true);   foreach ($f->lines() as $line) {   $line .= '';   }   echo $size.";".(memory_get_usage(true)/1000000).";".(microtime(true)-$time)."n";  }  

You can generate the “data.txt” file yourself. First step is to take the above script and save it as “yield.php”. After that you have to save the following bash code in a file and run it:

  #!/bin/bash  cp /dev/null data_s.txt  for i in {1..1000}  do   cat yield.php >> data_s.txt  done  cp /dev/null data.txt  for i in {1..1000}  do   cat data_s.txt >> data.txt  done  rm data_s.txt  

I executed the benchmark script on my workstation and loaded its output into a spreadsheet so I could plot the graph below.

yield_graph

As you can see, the best score is for the 16384 bytes (16 kB) fread size. With that fread size the 40 million lines from the 897 MB text file were iterated at 11.88 seconds using less than 1 MB of RAM. I do not understand why the performance graph looks like it does. I can reason that reading small chunks of data is not efficient, since it requires many I/O operations that each have overheads. But why is reading large chunks inefficient? It is a mystery to me, but maybe you know why? If you do, then please use the comments and enlighten me (and the other readers).

www.leaseweb.com


You May Also Like

About the Author: admind

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.