Программирование на Python

Генераторы

Разбить на страницы
Показывать лекцию целиком

Итератор позволяет получить коллекцию элементов, генератор позволяет на основе этой коллекции создать новую коллекцию. Рассмотрим следующую задачу. Пусть Р - итерируемый объект. Цикл for позволяет получить коллекцию элементов $$р_1, р_2, \dots р_n$$, связанную с итерируемым объектом. Нам требуется создать новую коллекцию - список, состоящих из элементов рk, возможно прошедших фильтрацию, преобразованных в соответствии с заданным выражением. Например, дан список целых чисел, мы хотим отобрать из списка четные элементы и возвести их в квадрат, получив новый список. Нетрудно написать соответствующий код, решающий эту задачу:

def test1():    
    source = [3, 8, 12, 5, 4, 7]
    """Создание списка: Классический код """
    even_quadrat = []
    for ev in source:
        if ev % 2 == 0:
            even_quadrat.append (ev * ev)
    
    """Создание списка: Генератор (упакованный код)"""
    gen_even_quadrat = [ev * ev for ev in source if ev % 2 == 0]

    Print(even_quadrat)
    Print(gen_even_quadrat)

Результаты запуска этого теста:

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

В мире программистов Python генераторы широко используются. В чем их преимущество:

  • Краткость кода. Код короче, но менее понятный. Только ради краткости не стоило бы городить огород.
  • Генератор является выражением, поэтому его можно использовать всюду, где допустимы выражения. Это тоже не столь значимое преимущество, поскольку генератор - это длинное выражение, которое разумно именовать, как это сделано в нашем примере, прежде чем использовать в других выражениях.
  • Эффективность выполнения кода. Это серьезный аргумент в пользу генераторов. Код генератора выполняется быстрее, чем классический код, использующий цикл. Когда речь идет о работе со массивными итерируемыми объектами, эффективность выполнения кода может играть важную роль.
  • У генераторов есть и дополнительное преимущество, о котором поговорим чуть позже.
  • Пример генератора, который мы построили, не отражает всех возможностей этого инструмента. Генератор может быть значительно сложнее. Выражение генерируемого списка может зависеть от нескольких переменных, каждая из которых пробегает значения своей итерируемой последовательности, что требует реализации вложенных циклов. Генератор справляется и с такой ситуацией. Рассмотрим пример построения таблицы истинности логической функции, зависящей от нескольких переменных. Этот пример мы уже рассматривали при демонстрации работы итератора map. Теперь покажем решение, основанное на классическом коде и соответствующем генераторе:

    def test2():
        """ Построение таблицы истинности логической функции"""
        """ Классический код"""
        L = [0, 1]
        T = [] 
        for x in L:
            for y in L:
                for z in L:
                    T.append(x and y or z)
        """ Генератор """
        gen_T =[x and y or z for x in L for y in L for z in L]
    
        Print(T)
        Print(gen_T)
    

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

    Синтаксис генератора в общем случае задается следующим определением:

    <генератор списка> ::= [<выражение(v1, v2, …vk)> 
    for v1 in <итератор_1> if filter_1
    …
    for vk in <итератор_k> if filter_k]
    

    В квадратных скобках записывается выражение, зависящее в общем случае от k переменных. Для каждой переменной записывается оператор for с соответствующим итератором и возможно с заданием фильтра для элементов.

    Заметьте, никаких разделителей кроме пробелов в этой записи нет.

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

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

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

    Приведу пример создания генератора словаря:

    def test3(): 
        """ Пример генератора словаря """
        Names = ['Петров', 'Леонов', 'Егоров']
        Marks = [5, 4, 5]
        dict = {name : mark for name, mark in zip(Names, Marks)}
        for key in dict:
            print (key, ' : ', dict[key], end = ' ')
        print()
    

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

    Вот результаты запуска этого теста:

    С генератором множеств все проще и понятнее:

    def test4():
        """ Пример генератора множества """
        Marks = [5, 4, 5, 3, 5, 4]
        dif_marks = {dif_mark for dif_mark in Marks}
        Print(dif_marks)
    

    Вот результаты работы теста:

    Выражение-генератор

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

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

    Синтаксически превратить генератор в выражение элементарно. Достаточно квадратные скобки заменить на круглые.

    Семантически выражение-генератор - это итератор, так что применим метод next, цикл for -in позволяет выполнить итерирование, а метод list возвращает список, хотя в этом случае все преимущества выражения теряются, поскольку вся идея состоит в том, чтобы список явно не строить.

    Давайте вернемся к нашему примеру построения таблицы истинности логической функции. Когда у функции три переменных, то число значений равно 8, но при десяти переменных - их уже тысяча, а при двадцати - миллион (число значений равно 2^n). Поэтому разумно генератор превратить в выражение-генератор. Вот соответствующий код:

    def test5():
        """ Построение таблицы истинности логической функции   
        Генератор-выражение"""
        L = [0, 1]
        gen_T = (x and y or u and v 
                for x in L for y in L for u in L for v in L)
        Print(gen_T)
    

    Приведу результаты запуска этого теста:

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

    Функции - генераторы

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

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

    Синтаксическое отличие такой функции от обычной функции незначительно, - оператор return заменяется оператором yield. Семантика выполнения отличается существенно. Такая функция, возвращая результат, не заканчивает работу, а приостанавливает выполнение, запоминая свое сoстояние. Функция в цикле, получив очередной результат, выдает его и приостанавливает работу до очередного вызова. Давайте приведу код такой функции, а потом подробнее обсудим семантику выполнения функции:

    def fun_gen():
        """Функция генератор. Возвращает значение 
           и приостанавливает работу до следующего вызова.
       """
        L = [0, 1]   
        for x in L:
           for y in L:
              for u in L:
                 for v in L:
                     yield x and y or u and v
    

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

    def test6():
        for value in fun_gen():
            print(value, end = ' ')
        print()
        values = []
        for value in fun_gen():
            values.append(value)
        Print(values)
        values = list(fun_gen())
        Print(values)

    Что происходит, когда в тесте в первом цикле for вызывается функция-генератор fun_gen? В момент вызова начнет работать цикл четырехкратной вложенности. Телом этого цикла является оператор yield, который вычислит значение и вернет его вызывающей программе - test6. Заметьте, функция генератор fun_gen не завершила работу, а только ее приостановила, сохраняя текущее состояние. Цикл for в тесте снова вызовет функцию fun_gen, и она продолжит работу, выдав новое значение. Такой циклический процесс вызова функции- генератора будет продолжаться пока не будет исчерпана коллекция элементов, возвращаемых этой функцией.

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

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

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

    def SumMatr(A, B):
        """Сложение квадратных матриц """
        n = len(A)
        LL = []
        for row in range(n):
            L = []
            for col in range(n):            
                 L.append(A[row][col] + B[row][col])
            LL.append(L)
        return LL
    

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

    def GenSumMatr(A, B):
        """Сложение квадратных матриц. Генераторы списков"""
        n = len(A)
        L = [[A[row][col] + B[row][col]
              for col in range(n)] for row in range(n)]
        return L  
    

    Можно сделать следующий шаг и обычную функцию GenSumMatr преобразовать в функцию-генератор, возвращающую при каждом вызове очередную строку суммарной матрицы:

    def FunGenSumMatr(A, B):
        """
        Сложение квадратных матриц. Функция-генератор
        При каждом вызове возвращается 
        очередная строка суммарной матрицы.
        Возвращаемое значение является генератором списка
        """
        n = len(A)
        for row in range(n):
            yield [A[row][col] + B[row][col] for col in range(n)]
             
    

    Приведу тест, в котором вызываются все приведенные варианты суммирования матриц:

    def test7():
        A = [[3, 5, 7], [2, 4, 2], [1, 2, 3]]
        B = [[1, 2, 3], [3, 2, 1], [2, 1, 2]]
        L = SumMatr(A, B)
        Print(L)
        L = GenSumMatr(A,B)
        Print(L)
        for row in FunGenSumMatr(A,B):
            Print(row)
    

    Приведу результаты работы этого теста:

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

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

    Страницы:

    Итератор позволяет получить коллекцию элементов, генератор позволяет на основе этой коллекции создать новую коллекцию. Рассмотрим следующую задачу. Пусть Р - итерируемый объект. Цикл for позволяет получить коллекцию элементов $$р_1, р_2, \dots р_n$$, связанную с итерируемым объектом. Нам требуется создать новую коллекцию - список, состоящих из элементов рk, возможно прошедших фильтрацию, преобразованных в соответствии с заданным выражением. Например, дан список целых чисел, мы хотим отобрать из списка четные элементы и возвести их в квадрат, получив новый список. Нетрудно написать соответствующий код, решающий эту задачу:

    def test1():    
        source = [3, 8, 12, 5, 4, 7]
        """Создание списка: Классический код """
        even_quadrat = []
        for ev in source:
            if ev % 2 == 0:
                even_quadrat.append (ev * ev)
        
        """Создание списка: Генератор (упакованный код)"""
        gen_even_quadrat = [ev * ev for ev in source if ev % 2 == 0]
    
        Print(even_quadrat)
        Print(gen_even_quadrat)

    Результаты запуска этого теста:

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

    В мире программистов Python генераторы широко используются. В чем их преимущество:

  • Краткость кода. Код короче, но менее понятный. Только ради краткости не стоило бы городить огород.
  • Генератор является выражением, поэтому его можно использовать всюду, где допустимы выражения. Это тоже не столь значимое преимущество, поскольку генератор - это длинное выражение, которое разумно именовать, как это сделано в нашем примере, прежде чем использовать в других выражениях.
  • Эффективность выполнения кода. Это серьезный аргумент в пользу генераторов. Код генератора выполняется быстрее, чем классический код, использующий цикл. Когда речь идет о работе со массивными итерируемыми объектами, эффективность выполнения кода может играть важную роль.
  • У генераторов есть и дополнительное преимущество, о котором поговорим чуть позже.
  • Пример генератора, который мы построили, не отражает всех возможностей этого инструмента. Генератор может быть значительно сложнее. Выражение генерируемого списка может зависеть от нескольких переменных, каждая из которых пробегает значения своей итерируемой последовательности, что требует реализации вложенных циклов. Генератор справляется и с такой ситуацией. Рассмотрим пример построения таблицы истинности логической функции, зависящей от нескольких переменных. Этот пример мы уже рассматривали при демонстрации работы итератора map. Теперь покажем решение, основанное на классическом коде и соответствующем генераторе:

    def test2():
        """ Построение таблицы истинности логической функции"""
        """ Классический код"""
        L = [0, 1]
        T = [] 
        for x in L:
            for y in L:
                for z in L:
                    T.append(x and y or z)
        """ Генератор """
        gen_T =[x and y or z for x in L for y in L for z in L]
    
        Print(T)
        Print(gen_T)
    

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

    Синтаксис генератора в общем случае задается следующим определением:

    <генератор списка> ::= [<выражение(v1, v2, …vk)> 
    for v1 in <итератор_1> if filter_1
    …
    for vk in <итератор_k> if filter_k]
    

    В квадратных скобках записывается выражение, зависящее в общем случае от k переменных. Для каждой переменной записывается оператор for с соответствующим итератором и возможно с заданием фильтра для элементов.

    Заметьте, никаких разделителей кроме пробелов в этой записи нет.

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

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

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

    Приведу пример создания генератора словаря:

    def test3(): 
        """ Пример генератора словаря """
        Names = ['Петров', 'Леонов', 'Егоров']
        Marks = [5, 4, 5]
        dict = {name : mark for name, mark in zip(Names, Marks)}
        for key in dict:
            print (key, ' : ', dict[key], end = ' ')
        print()
    

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

    Вот результаты запуска этого теста:

    С генератором множеств все проще и понятнее:

    def test4():
        """ Пример генератора множества """
        Marks = [5, 4, 5, 3, 5, 4]
        dif_marks = {dif_mark for dif_mark in Marks}
        Print(dif_marks)
    

    Вот результаты работы теста:

    Выражение-генератор

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

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

    Синтаксически превратить генератор в выражение элементарно. Достаточно квадратные скобки заменить на круглые.

    Семантически выражение-генератор - это итератор, так что применим метод next, цикл for -in позволяет выполнить итерирование, а метод list возвращает список, хотя в этом случае все преимущества выражения теряются, поскольку вся идея состоит в том, чтобы список явно не строить.

    Давайте вернемся к нашему примеру построения таблицы истинности логической функции. Когда у функции три переменных, то число значений равно 8, но при десяти переменных - их уже тысяча, а при двадцати - миллион (число значений равно 2^n). Поэтому разумно генератор превратить в выражение-генератор. Вот соответствующий код:

    def test5():
        """ Построение таблицы истинности логической функции   
        Генератор-выражение"""
        L = [0, 1]
        gen_T = (x and y or u and v 
                for x in L for y in L for u in L for v in L)
        Print(gen_T)
    

    Приведу результаты запуска этого теста:

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

    Функции - генераторы

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

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

    Синтаксическое отличие такой функции от обычной функции незначительно, - оператор return заменяется оператором yield. Семантика выполнения отличается существенно. Такая функция, возвращая результат, не заканчивает работу, а приостанавливает выполнение, запоминая свое сoстояние. Функция в цикле, получив очередной результат, выдает его и приостанавливает работу до очередного вызова. Давайте приведу код такой функции, а потом подробнее обсудим семантику выполнения функции:

    def fun_gen():
        """Функция генератор. Возвращает значение 
           и приостанавливает работу до следующего вызова.
       """
        L = [0, 1]   
        for x in L:
           for y in L:
              for u in L:
                 for v in L:
                     yield x and y or u and v
    

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

    def test6():
        for value in fun_gen():
            print(value, end = ' ')
        print()
        values = []
        for value in fun_gen():
            values.append(value)
        Print(values)
        values = list(fun_gen())
        Print(values)

    Что происходит, когда в тесте в первом цикле for вызывается функция-генератор fun_gen? В момент вызова начнет работать цикл четырехкратной вложенности. Телом этого цикла является оператор yield, который вычислит значение и вернет его вызывающей программе - test6. Заметьте, функция генератор fun_gen не завершила работу, а только ее приостановила, сохраняя текущее состояние. Цикл for в тесте снова вызовет функцию fun_gen, и она продолжит работу, выдав новое значение. Такой циклический процесс вызова функции- генератора будет продолжаться пока не будет исчерпана коллекция элементов, возвращаемых этой функцией.

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

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

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

    def SumMatr(A, B):
        """Сложение квадратных матриц """
        n = len(A)
        LL = []
        for row in range(n):
            L = []
            for col in range(n):            
                 L.append(A[row][col] + B[row][col])
            LL.append(L)
        return LL
    

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

    def GenSumMatr(A, B):
        """Сложение квадратных матриц. Генераторы списков"""
        n = len(A)
        L = [[A[row][col] + B[row][col]
              for col in range(n)] for row in range(n)]
        return L  
    

    Можно сделать следующий шаг и обычную функцию GenSumMatr преобразовать в функцию-генератор, возвращающую при каждом вызове очередную строку суммарной матрицы:

    def FunGenSumMatr(A, B):
        """
        Сложение квадратных матриц. Функция-генератор
        При каждом вызове возвращается 
        очередная строка суммарной матрицы.
        Возвращаемое значение является генератором списка
        """
        n = len(A)
        for row in range(n):
            yield [A[row][col] + B[row][col] for col in range(n)]
             
    

    Приведу тест, в котором вызываются все приведенные варианты суммирования матриц:

    def test7():
        A = [[3, 5, 7], [2, 4, 2], [1, 2, 3]]
        B = [[1, 2, 3], [3, 2, 1], [2, 1, 2]]
        L = SumMatr(A, B)
        Print(L)
        L = GenSumMatr(A,B)
        Print(L)
        for row in FunGenSumMatr(A,B):
            Print(row)
    

    Приведу результаты работы этого теста:

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

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

    Вернуться к учебному плану