Что такое генераторы в python
Генераторы Python. Их создание и использование
Приходилось ли вам когда-либо работать с настолько большим набором данных, что он переполнял память вашего компьютера? Или быть может у вас была сложная функция, для которой нужно было бы сохранять внутреннее состояние при вызове? А если при этом функция была слишком маленькой, чтобы оправдать создание собственного класса? Во всех этих случаях вам придут на помощь генераторы Python и ключевое слово yield.
Прочитав эту статью, вы узнаете:
Если вы являетесь Питонистом начального или среднего уровня и вы заинтересованы в том, чтобы научиться работать с большими наборами данных в питоновском стиле, то скорее всего это руководство для вас.
По ссылке ниже вы можете скачать копию файла с данными, используемыми в этом руководстве.
Использование Генераторов
Функции генераторов (их описание можно почитать в PEP 255) представляют собой особый вид функций, которые возвращают «ленивый итератор». И хотя содержимое этих объектов вы можете перебирать также как и списки, но при этом, в отличие от списков, ленивые итераторы не хранят свое содержимое в памяти. Чтобы составить общее представление об итераторах в Python взгляните на статью Python “for” Loops (Definite Iteration).
Теперь, когда вы имеете примерное представление о том, чем является генератор, у вас наверняка появилось желание увидеть как он работает. Давайте рассмотри два примера. В первом вы увидите общий принцип работы генераторов. В последующих у вас будет возможность изучить работу генераторов более подробно.
Пример 1: Чтение больших файлов
Списки Python
Работа с потоками данных и большими файлами, такими например как CSV, являются наиболее распространенными вариантами использования генераторов. Давайте возьмем CSV файл (CSV является стандартным форматом для обмена данными, колонки в нем разделяются при помощи запятых). Предположим, что вы хотите посчитать количество имеющихся в нем рядов. Код ниже предлагает один из путей для, того, чтобы осуществить это:
Это вполне приемлемое решение, но будет ли этот подход работать, если файл окажется слишком большим? А что если файл окажется больше чем вся доступная память, которая есть в нашем распоряжении? Для того чтобы ответить на этот вопрос, давайте предположим, что csv_reder() будет открывать файл и считывать его в массив.
В этом случае open() возвращает объект генератора, который вы можете «лениво» (не обсчитывая заранее) перебирать ряд за рядом. Тем не менее, file.read().split() загружает все данные в память сразу, вызывая ошибку памяти (MemoryError).
До того как это произойдет, вы можете заметить, что ваш компьютер замедлился. Возможно вам потребуется даже вручную остановить программу. Но что нам делать, если мы хотим этого избежать?
Генераторы Python
Давайте взглянем на новое определение функции csv_reader() :
В этой версии вы открываете файл и проходите его содержимое, возвращая ряд за рядом. Этот код выводит следующий результат без каких-либо ошибок:
Почему так получилось? Да потому что вы по сути превратили функцию csv_reader() в генератор. Эта версия кода открывает файл, проходит по строкам и извлекает для чтения лишь отдельный ряд, вместо того, чтобы возвращать весь файл целиком.
Также вы можете определить выражение создающее генератор, которое очень похоже по синтаксису на выражение создающее список. В таком виде вы можете использовать генератор без вызова функции:
Такой способ создания генератора csv_gen является более лаконичным.
Более подробно о yield мы расскажем позже, а пока запомните основные отличия между использованием ключевых слов yield и return:
Пример 2: Создание бесконечной последовательности
Создание же бесконечной последовательности стопроцентно потребует от нас использования генератора. Причина проста — ограниченность памяти нашего компьютера.
Если вы попробуете запустить этот код в теле цикла for, то увидите, что на самом деле он бесконечный:
Эта программа будет исполняться, до тех пор, пока вы ее вручную не остановите.
Пример 3: Нахождение палиндромов
Вы можете использовать бесконечные последовательности множеством различных способов. Одним из них, который мы отметим особенно, является создание детектора палиндромов. Детектор палиндромов выявляет все последовательности букв и цифр, которые являются палиндромами. Это слова или числа, которые читаются одинаково вперед и назад, как «121» например. Сперва давайте зададим наш числовой детектор палиндромов:
Не особо беспокойтесь о понимании вычислений, лежащих в основе данного кода. Просто заметьте, что функция принимает введенное число, переворачивает его, и сравнивает с оригиналом. Теперь вы можете использовать генератор бесконечной последовательности для получения бегущего списка со всеми числовыми палиндромами:
В консоли выводятся только те номера, которые читаются одинаково и вперед и назад.
Примечание: на практике вам вряд ли придется писать свой собственный бесконечный генератор последовательностей, по той простой причине, что есть уже очень эффективный генератор itertools.count() из модуля itertools.
Теперь, когда вы познакомились с простым примером использования генератора бесконечной последовательности, давайте рассмотрим более детально работу этого генератора.
Понимание работы генератора Python
К этому моменту вы уже познакомились с двумя основными способами создания генераторов: с помощью функции и с помощью выражения. У вас также должно было сформироваться интуитивное представление о том, как работает генератор. Давайте теперь уделим некоторое время тому, чтобы сделать наши знания более четкими.
Примечание. Если вы хотите больше узнать о генераторах списков, множеств и словарей в Python, можете прочитать статью Эффективное использование генераторов списков (англ).
Создание генератора с помощью выражения
Как и выражения создающие списки, выражения создающие генераторы позволяют быстро получить объект генератора с помощью всего одной строчки кода. Использоваться они могут в тех же случаях, что и выражения создающие списки, но при этом у них есть одно дополнительное преимущество. Их можно создавать не удерживая весь объект в памяти перед итерацией. Если перефразировать, вы не будете расходовать память при использовании генератора.
Давайте для примера возьмем возведение в квадрат некоторых чисел:
Это подтверждает тот факт, что с помощью круглых скобок вы создали объект генератора, а также то, что он отличается от списка.
Профилирование эффективности генератора
Ранее мы узнали, что использование генераторов является отличным способом оптимизации памяти. И хотя генератор бесконечной последовательности является наиболее ярким примером этой оптимизации, давайте рассмотрим еще один пример с возведением числа в квадрат и проверим размер полученных объектов.
Вы можете сделать это с помощью вызова функции sys.getsizeof () :
В этом случае размер списка, полученного с помощью выражения составляет 87 624 байта, а размер генератора — только 120. То есть, список занимает памяти в 700 раз больше, чем генератор! Однако нужно помнить одну вещь. Если размер списка меньше доступной памяти на работающей машине, тогда обработка его будет занимать меньше времени, чем аналогичная обработка генератора. Чтобы удостовериться в этом, давайте просуммируем результаты приведенных выше выражений. Вы можете использовать для анализа функцию cProfile.run () :
Здесь вы можете видеть, что суммирование всех значений, содержащихся в списке заняло около трети времени аналогичного суммирования с помощью генератора. Поэтому если скорость является для вас проблемой, а память — нет, то список, возможно, окажется лучшим инструментом для работы.
Примечание. Эти измерения действительны не только для генераторов, созданных с помощью выражений. Они абсолютно идентичны и для генераторов, созданных с помощью функции. Ведь, как мы уже говорили выше, эти генераторы эквивалентны.
Генераторы списков в Python для начинающих
Перевод статьи «List Comprehension in Python Explained for Beginners».
Генератор списков – это простой для чтения, компактный и элегантный способ создания списка из любого существующего итерируемого объекта. По сути, это более простой способ создания нового списка из значений уже имеющегося списка.
Обычно это одна строка кода, заключенная в квадратные скобки. Вы можете использовать генератор для фильтрации, форматирования, изменения или выполнения других небольших задач с существующими итерируемыми объектами, такими как строки, кортежи, множества, списки и т.д.
Сегодня мы разберем несколько способов создания генератора списков и увидим некоторые их вариации, например:
Помимо этого, мы также рассмотрим следующие концепции:
Что же такое генераторы списков в Python?
Итак, начнем с синтаксиса генератора списка. Генератор списка – это одна строка кода, которую вы пишете в квадратных скобках. Он состоит из трех компонентов:
Пример простого генератора списка
Приведенный ниже фрагмент кода является примером простейшего генератора списка. Здесь мы просто перебираем lst и сохраняем все его элементы в списке a :
Этот код полностью эквивалентен следующему:
Генератор списка с одиночным и вложенным условием if
Этот код выполняет то же самое, что и приведенный ниже:
Этот код эквивалентен данному:
Генератор списка с одним и несколькими условиями if и else
Следующий код выполняет ту же задачу:
Как это работает? Чтобы понять это, мы можем разделить всё условие на три части, после каждого else:
‘Two’ if x%2 == 0 else «Three» if x%3 == 0 else ‘not 2 & 3’
Мы можем добиться того же результата, написав код следующим образом:
Теперь-то вы видите силу генератора списков? Он выполняет задачу всего в одной строке, в то время как традиционный цикл for состоит из 7.
Генератор списка с вложенным циклом for
Хорошо! Теперь давайте разберем использование вложенного цикла for в генераторе списка.
Традиционным способом эта задача решалась бы так:
Хорошо, а теперь, как и обещали, давайте сравним обычный цикл for и генератор списков.
Циклы vs. генератор списков
Выше мы видели, как генератор списков позволяет выполнять задачу всего в одну строчку, в то время как цикл for требует написания нескольких строк.
Однако если вы хотите выполнить более одного простого условия, генератор списков не сможет справиться с этим без ущерба для удобочитаемости. Это одна из его основных проблем.
Преимущества генераторов списков
Когда использовать генератор списков (а когда его лучше избегать)
Вы можете использовать генератор списков, если выполняете простую фильтрацию, модификации или форматирование итерируемых объектов. Он также будет хорошим выбором, если вы хотите, чтобы ваш код был компактным и читабельным.
Кроме того, вы можете использовать генератор, когда для вас важно даже небольшое увеличение производительности.
Однако следует избегать использования генератора списков, если вам нужно добавить слишком много условий для фильтрации или модификации, поскольку это сделает ваш код излишне сложным и трудным для чтения.
Примечание редакции Pythonist: узнать больше об использовании генераторов списков можно в статье «Когда использовать List Comprehension в Python».
Заключение
Подробно про генераторы Python – что такое и как работают
Что такое генераторы в Python?
Генераторы Python – это функции, которые возвращают объект обхода и используются для создания итераторов, просматривают сразу все элементы. Генератор также может быть выражением, синтаксис которого аналогичен пониманию списка в Python.
Создание итерации в Python сопряжено с большими трудностями; нам нужно реализовать методы __iter __() и __next __() для отслеживания внутренних состояний.
Создание итераторов – длительный процесс. Вот почему генератор играет важную роль в упрощении этого процесса. Если в итерации не найдено значение, возникает исключение StopIteration.
Как создать функцию генератора в Python?
Создать генератор на Python довольно просто. Он похож на обычную функцию, определяемую ключевым словом def, и использует ключевое слово yield вместо return. Или мы можем сказать, что если тело любой функции содержит оператор yield, он автоматически становится функцией-генератором. Рассмотрим следующий пример:
Yield вместо return
Оператор yield отвечает за управление потоком функции генератора. Он приостанавливает выполнение функции, сохраняя все состояния и уступая вызывающему. Позже он возобновляет выполнение при вызове следующей функции.
Оператор return возвращает значение и завершает работу всей функции, оператор return может использоваться в функции только один раз. Оператор yield в функции генератора мы можем использовать неоднократно.
Рассмотрим следующий пример.
Разница между функцией генератора и нормальной функцией
Генератор выражения
Мы можем легко создать выражение генератора без использования пользовательской функции. Это то же самое, что и лямбда-функция, которая создает анонимную функцию; выражения генератора создают анонимную функцию генератора.
Представление выражения генератора похоже на понимание списка Python. Единственное отличие состоит в том, что квадратные скобки заменены круглыми скобками. Понимание списка вычисляет весь список, тогда как выражение генератора вычисляет один элемент за раз.
Рассмотрим следующий пример:
В приведенной выше программе list comprehension вернуло список элементов в третьей степени, тогда как выражение генератора вернуло ссылку на вычисленное значение. Вместо применения цикла for мы также можем вызвать next() для объекта-генератора. Рассмотрим другой пример:
Примечание: – Когда мы вызываем next(), Python вызывает __next __() для функции, в которую мы передали его в качестве параметра.
В приведенной выше программе мы использовали функцию next(), которая вернула следующий элемент списка.
Пример программы для печати таблицы заданного числа с помощью генератора:
В приведенном выше примере функция генератора выполняет итерацию с использованием цикла for.
Преимущества
Есть различные преимущества генераторов. Некоторые из них приведены ниже:
Генераторы проще реализовать по сравнению с итератором. В итераторе мы должны реализовать функцию __iter __() и __next __().
Генераторы эффективно используют память для большого количества последовательностей. Обычная функция возвращает последовательность из списка, которая создает всю последовательность в памяти перед возвратом результата, а функция генератора вычисляет значение и приостанавливает их выполнение и возобновляется для следующего вызова.
Генератор бесконечной последовательности – отличный пример оптимизации памяти. Давайте обсудим это в приведенном ниже примере, используя функцию sys.getsizeof().
Из вышеприведенного вывода видно, что для list comprehension используется 4508 байт памяти, тогда как generator expression использует 56 байт памяти. Это означает, что объекты-генераторы намного эффективнее, чем сжатие списков.
Data Pipeline предоставляет возможность обрабатывать большие наборы данных или поток данных без использования дополнительной памяти компьютера.
Предположим, у нас есть файл журнала известного ресторана. В файле журнала есть столбец(4-й столбец), в котором отслеживается количество гамбургеров, проданных каждый час, и мы хотим просуммировать его, чтобы найти общее количество гамбургеров, проданных за 4 года. В этом сценарии генератор может создать конвейер с серией операций. Ниже приведен его код:
Генератор может производить бесконечное количество предметов. Бесконечные последовательности не могут содержаться в памяти, и поскольку генераторы производят только один элемент за раз, рассмотрим следующий пример:
В этом руководстве мы узнали о генераторах Python.
Функциональное программирование в Python. Генераторы, как питонячий декларативный стиль
Общее введение
Говоря о Python, обычно используется процедурный и ООП стиль программирования, однако это не значит, что другие стили невозможны. В презентации ниже мы рассмотрим ещё пару вариантов — Функциональное программирование и программирование с помощью генераторов. Последние, в том числе, привели к появлению сопрограмм, которые позднее помогли создать асинхронность в Python. Сопрограммы и асинхронность выходят за рамки текущего доклада, поэтому, если интересно, можете ознакомиться об этом самостоятельно. Лично я рекомендую книгу «Fluent Python», в которой разговор начинается от итераторов, плавно переходит в темы о генераторах, сопрограммах и асинхронности.
Введение в ФП
Говоря о ФП сразу следует подчеркнуть, что программирование через функции далеко не всегда ФП, чаще всего это всего лишь процедурный стиль программирования. Чтобы попробовать понять ФП — необходимо разобраться, что это такое, и помогут нам в этом теоретические знания.
Выделяют две крупные парадигмы программирования: императивная и декларативная.
Императивное программирование предполагает ответ на вопрос “Как?”. В рамках этой парадигмы вы задаете последовательность действий, которые нужно выполнить, для того чтобы получить результат. Результат выполнения сохраняется в ячейках памяти, к которым можно обратиться впоследствии.
Декларативное программирование предполагает ответ на вопрос “Что?”. Здесь вы описываете задачу, даете спецификацию, говорите, что вы хотите получить в результате выполнения программы, но не определяете, как этот ответ будет получен. Каждая из этих парадигм включает в себя более специфические модели.
В продуктовой разработке наибольшее распространение получили процедурное и объектно-ориентированное программирование из группы “императивное программирование” и функциональное программирование из группы “декларативное программирование”.
В рамках процедурного подхода к программированию основное внимание сосредоточено на декомпозиции – разбиении программы / задачи на отдельные блоки / подзадачи. Разработка ведётся пошагово, методом “сверху вниз”. Наиболее распространенным языком, который предполагает использование процедурного подхода к программирования является язык C, в нем, основными строительными блоками являются функции.
В рамках объектно-ориентированного (ООП) подхода программа представляется в виде совокупности объектов, каждый из которых является экземпляром определенного класса, классы образуют иерархию наследования. ООП базируется на следующих принципах: инкапсуляция, наследование, полиморфизм, абстракция. Примерами языков, которые позволяют вести разработку в этой парадигме являются C#, Java.
В рамках функционального программирования выполнение программы – процесс вычисления, который трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании). Языки, которые реализуют эту парадигму – Haskell, Lisp.
Языки, которые можно отнести в функциональной парадигме обладают определенным набором свойств. Если язык не является чисто функциональным, но реализует эти свойства, то на нем можно разрабатывать, как говорят, в функциональном стиле.
Основные принципы ФП
Функции высшего порядка принимают в качестве аргументов другие функции. В стандартную библиотеку Python входит достаточно много таких функций, в качестве примера приведем функцию map. Она принимает функцию и Iterable объект, применяет функцию к каждому элементу Iterable объекта и возвращает Iterator объект, который итеративно возвращает все модифицированные после функции элементы.
В Python это не выполняется. Необходимо самостоятельно следить за тем, чтобы функция была чистой.
Основные термины
Не все термины ниже необходимы для понимания доклада, но необходимы для понимания ФП (спасибо одному другу за их подборку)
Ссылочная прозрачность — свойство функциональных программ, в котором любое выражение может быть заменено вычисленным им значением без изменения поведения программы. Ссылочная прозрачность позволяет проводить рефакторинг программ без изменения их поведения.
Детерминированная функция возвращает тот же результат для одних и тех же входных данных.
Чистая функция трансформирует входные данные в выходные и не взаимодействует с миром вне функции каким-либо образом, который можно наблюдать. Все чистые функции детерминированы, но не все детерминированные функции чисты.
Тотальная функция возвращает вывод для каждого ввода.
Тотальные функции всегда завершаются и никогда не вызывают исключений.
Композиция функций — применение одной функции к результату другой
Сайд эффект возникает, когда выполнение выражения делает что-то большее, чем вычисление значения. Сайд эффекты — взаимодействия, которые можно наблюдать за пределами функции или выражения. Распространённые сайд эффекты включают в себя доступ к базе данных, доступ к файлам, доступ к сети, системные вызовы, изменение изменяемой памяти или вызов функций, которые выполняют любое из вышеперечисленных действий.
Полиморфизм — особенность языков программирования, которая позволяет переменной или функции принимать множество различных форм. Рассмотрим один из видов полиморфизма.
Параметрический полиморфизм, иногда называемый универсальным, является особенностью некоторых языков программирования, который позволяет универсально количественно определять функцию или тип данных по одному или нескольким параметрам типа. Такие полиморфные функции и полиморфные типы данных называются параметризованными их параметрами типа.
Параметрический полиморфизм позволяет создавать общий код, который работает со многими различными типами данных, и универсальными типами данных, таких как коллекции. Параметрически полиморфный код должен вести себя единообразно при любом выборе параметров типа, что дает мощный способ рассуждать о таком коде, называемый параметрическим рассуждением.
Замыкание — процедура вместе с привязанной к ней совокупностью данных. © Steve Majewski
Замыкание — функция, которая ссылается на свободные переменные в своей области видимости.
Встроенное ФП поведение в Python
В соответствии с вышесказанным, попробуем сделать несколько хаков, чтобы наш код был более ФПшный. Избавимся от if / elif / else в Python
Используем lambda для присваивания таких условных выражений
Замена циклов на выражения так же проста, как и замена условных блоков. for может быть переписана с помощью map().
То же самое мы можем сделать и с функциями.
Перевести while впрямую немного сложнее, но вполне получается.
ФП вариант while все еще требует функцию while_block(), которая сама по себе может содержать не только выражения, но и утверждения (statements). Но мы могли бы продолжить дальнейшее исключение утверждений в этой функции (как, например, замену блока if/else в вышеописанном шаблоне).
К тому же, обычная проверка на месте (наподобие while myvar == 7) вряд ли окажется полезной, поскольку тело цикла (в представленном виде) не может изменить какие-либо переменные (хотя глобальные переменные могут быть изменены в while_block()). Один из способов применить более полезное условие — заставить while_block() возвращать более осмысленное значение и сравнивать его с условием завершения.
Стоит взглянуть на реальный пример исключения утверждений:
Мы достигли того, что выразили небольшую программу, включающую ввод/вывод, циклы и условия в виде чистого выражения с рекурсией (фактически — в виде функционального объекта, который при необходимости может быть передан куда угодно).
Мы все еще используем служебную функцию monadic_print(), но эта функция совершенно общая и может использоваться в любых функциональных выражениях, которые мы создадим позже. Заметим, что любое выражение, содержащее monadic_print(x) вычисляется так же, как если бы оно содержало просто x.
После всей проделанной работы по избавлению от совершенно осмысленных конструкций и замене их на невразумительные вложенные выражения, возникает естественный вопрос — «Зачем?!». Перечитывая описания характеристик ФП, мы можем видеть, что все они достигнуты в Python. Но важнейшая (и, скорее всего, в наибольшей степени реально используемая) характеристика — исключение побочных эффектов или, по крайней мере, ограничение их применения специальными областями наподобие монад. Огромный процент программных ошибок и главная проблема, требующая применения отладчиков, случается из-за того, что переменные получают неверные значения в процессе выполнения программы.
Функциональное программирование обходит эту проблему, просто вовсе не присваивая значения переменным.
Секции, комментированные как #. more stuff. — места, где побочные эффекты с наибольшей вероятностью могут привести к ошибкам.
Очевидно, что инкапсуляция в функциях/объектах и тщательное управление областью видимости могут использоваться, чтобы защититься от этого рода проблем. Вы также можете всегда удалять (del) ваши переменные после использования.
Но на практике указанный тип ошибок весьма обычен. Функциональный подход к задаче полностью исключает ошибки, связанные с побочными эффектами. Возможное решение могло бы быть таким:
Реальное преимущество этого функционального примера в том, что в нем абсолютно ни одна переменная не меняет своего значения. Какое-либо неожиданное побочное влияние на последующий код (или со стороны предыдущего кода) просто невозможно. Конечно, само по себе отсутствие побочных эффектов не гарантирует безошибочность кода, но в любом случае это преимущество. Вместо вышеприведенных примеров — императивного или функционального — наилучшая (и функциональная) техника выглядит следующим образом:
Да, всё верно, list, tuple, set, dict comprehensions и generator expressions — изначально ФП техники, которые, как и многое другое, перекочевало в другие не-ФП языки
Библиотека Xoltar Toolkit
Сразу оговоримся, что библиотека достаточно старая и подходит лишь для Python 2, однако для ознакомления её достаточно. Библиотека Xoltar Toolkit Брина Келлера (Bryn Keller) покажет нам больше возможностей ФП.
Основные возможности ФП Келлер представил в виде небольшого эффективного модуля на чистом Python. Помимо модуля functional, в Xoltar Toolkit входит модуль lazy, поддерживающий структуры, вычисляемые «только когда это необходимо». Множество функциональных языков программирования поддерживают отложенное вычисление, поэтому эти компоненты Xoltar Toolkit предоставят вам многое из того, что вы можете найти в функциональном языке наподобие Haskell.
Ничто в Python не запрещает переприсваивания другого значения имени, ссылающемуся на функциональное выражение. В ФП под именами понимается всего лишь буквенное сокращение более длинных выражений, при этом подразумевается, что одно и то же выражение всегда приводит к одному и тому же результату. Если же уже определенному имени присваивается новое значение, это допущение нарушается.
К несчастью, одно и то же выражение sum2(range(10)) вычисляется к разным результатам в двух местах программы, несмотря на то, что аргументы выражении не являются изменяемыми переменными.
К счастью, модуль functional предоставляет класс Bindings, предотвращающий такое переприсваивание.
Разумеется, реальная программа должна перехватить и обработать исключение BindingError, однако сам факт его возбуждения позволяет избежать целого класса проблем.
Библиотека returns
Уже более современная библиотека, предлагаемая Никитой Соболевым, нашим соотечественником, также позволяет использовать возможности ФП в Python. Декоратор maybe позволяет переходить к следующей итерации только при успешном завершении предыдущей
Более реальный пример, из императивного стиля в декларативный можно переписать так:
Или, например, позволяет обходить возможные подводные камни напрямую через пайплайн
И то же самое, только с помощью returns
Теперь у нас есть чистый, безопасный и декларативный способ выразить потребности нашего бизнеса: Мы начинаем с запроса, который может потерпеть неудачу в любой момент, затем анализирем ответ, если запрос был успешным, а потом возвращаем результат.
Подробнее об этой библиотеке можно уточнить из её достаточно большой и подробной документации, указанной в литературе.
Прим. Я, как автор этой статьи, не использовал returns в проде и не думаю, что буду когда-либо использовать, но не упомянуть об этой библиотеке в рамках данной статьи просто нельзя.
Литература
Генераторы
Введение в итераторы
Итерация — по сути является перебором значений. Вот обычный пример, который встречается повсюду.
Также, как мы знаем, итерация может происходить по многим типам объектов (не только спискам). Причина, почему мы можем итерироваться по объектам — реализация специального протокола
Внутри под капотом обычной итерации.
Происходит примерно следующее:
Например, посмотрим, как имплементировать такое:
Его реализация будет такова:
Введение в генераторы
Генератор — функция, которая генерирует последовательность результатов вместо одного значения
Вместо того, чтобы возвращать значение, мы создаём серию значений (с использованием оператора yield). Вызов функции генератора создает объект-генератор. Однако функция не запускается.
Синтаксис генераторного выражения также прост
Генераторы vs итераторы
Функция генератор немного отличается от объекта, поддерживающего итерацию. Генератор — разовая операция. Мы можем перебирать сгенерированные данные один раз, но если мы хотим сделать это снова, мы должны снова вызвать функцию генератор. Это отличается, например, от списка (который мы можем перебирать столько раз, сколько хотим)
Генераторы как пайплайн
Теперь у нас есть два основных строительных блока, которые мы можем применить для создания пайплайна. Например, имеется задача:
Узнайте, сколько байтов данных было передано, суммируя последний столбец данных в журнале веб-сервера Apache. И да, размер лог файла может измеряться в гигабайтах
Каждая строка в логах выглядит примерно так:
Число байтов находится в последней колонке:
Это может быть число или отсутствующее значение.
Сконвертируем полученный результат в число
В итоге, может получиться примерно такой код
Мы читаем строка за строкой и обновляем сумму. Но это старый стиль. Посмотрим, как эту задачу можно решить с помощью генераторов.
Этот подход отличается от предыдущего, меньше строк, напоминает функциональный стиль Мы получили пайплайн
На каждом этапе конвейера мы объявляем операцию, которая будет применяться ко всему входному потоку. Вместо того чтобы сосредоточиться на проблеме построчно, мы просто разбиваем ее на большие операции, которые работают со всем файлом. Это декларативный подход.
Безусловно, этот генераторный подход имеет разновидности причудливой медленной магии. Файл из 1.3 ГБ код в старом стиле выполнил за 18.6 секунд, код на генераторах был выполнен за 16,7 секунд.
AWK с той же задачей справился гораздо медленее, за 70.5 секунд
Генераторное решение было основано на концепции конвейерной передачи данных между разными компонентами. Что, если бы у нас были более продвинутые виды компонентов для работы? Возможно, мы могли бы выполнять разные виды обработки, просто подключив различные компоненты друг за другом.
Концепт yield from
‘yield from’ может использоваться для делегирования итерации
Маршрутизация данных на генераторах (мультиплексирование, броадкастинг)
Задача — считать логи в режиме реального времени из разных источников, и транслировать результат нескольким потребителям
На данной диаграмме видно, что нам потребуется мультиплексирование (всё в одно) и броадкастинг (одно во всё). Что ж, не будем томить и напишем сразу решение.
Как мы видим из этого примера — ничего сложного в этом нет, вполне легко можно решать самые разнообразные задачи.
Пример трейсинга генератора
Тут хотелось бы показать достаточно простой пример, если у тебя используется сотня генераторов и не понятно, в каком из них происходит сбой, например, с помощью принта.
Как мы видим — такой дебаг генератор можно встроить в любой из генераторов, не изменяя его, получив необходимую информацию, например, выведя результат каждый итерации
Стандартные инструменты генераторы
Выводы
Плюсы:
Минусы
Литература
Итоги
В ООП и ФП есть как свои плюсы, так и свои минусы. Например, на чистом ФП не напишешь красивый и выразительный код на Python, в котором можно легко разобраться, из-за чего преимущества Python сходят на нет. Однако, это не значит, что мы не можем использовать преимущества функционального подхода.
Как можно заметить, если мы пишем код на генераторах, то он получается вполне себе декларативным. Можно сказать, что код на генераторах — в некоторой степени ФП, так элегантно вошедший в Python.
Наша главная задача — писать ясный, понятный, красивый, тестируемый код и выбирать для этого подходящие инструменты. ФП — не самоцель, а лишь средство, как и всегда, чтобы мочь написать ещё лучший код!