Введение в генераторы Python

Spread the love

Оглавление

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

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

Чтобы создать генератор, необходимо определить функцию, как обычно, но использовать yield вместо return, указывая интерпретатору, что эту функцию следует рассматривать как итератор:

def countdown(num):
    print('Starting')
    while num > 0:
        yield num
        num -= 1

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

Что происходит, когда вы вызываете эту функцию?

>>> def countdown(num):
...     print('Starting')
...     while num > 0:
...         yield num
...         num -= 1
...
>>> val = countdown(5)
>>> val
<generator object countdown at 0x10213aee8>

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

Объекты генератора выполняются при вызове next():

>>> next(val)
Starting
5

При первом вызове next() выполнение начинается с начала тела функции и продолжается до следующего оператора yield, где возвращается значение справа от оператора, последующие вызовы next() продолжаются с оператора yield до конец функции, затем новый обход цикла и продолжение с начала тела функции, пока не будет вызван другой выход. Если yield не вызывается (что в нашем случае означает, что условие while не отрабатывается, потому что num <= 0), возникает исключение StopIteration:

>>> next(val)
4
>>> next(val)
3
>>> next(val)
2
>>> next(val)
1
>>> next(val)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Выражения-генератор (Generator Expressions)

Как и списки, генераторы также могут быть написаны таким же образом, за исключением того, что они возвращают объект генератора, а не список:

>>> my_list = ['a', 'b', 'c', 'd']
>>> gen_obj = (x for x in my_list)
>>> for val in gen_obj:
...     print(val)
...
a
b
c
d

Обратите внимание на круглые скобки по обеим сторонам второй строки, обозначающие выражение генератора, которое, по большей части, делает то же самое, что и генератор списка, но делает это лениво (то есть в момент запроса объекта):

>>> import sys
>>> g = (i * 2 for i in range(10000) if i % 3 == 0 or i % 5 == 0)
>>> print(sys.getsizeof(g))
72
>>> l = [i * 2 for i in range(10000) if i % 3 == 0 or i % 5 == 0]
>>> print(sys.getsizeof(l))
38216

Будьте внимательны, чтобы не перепутать синтаксис генератор списка с выражением генератора — [] vs () — поскольку выражения генератора могут работать медленнее, чем генератор списка (при не хватки памяти):

>>> import cProfile
>>> cProfile.run('sum((i * 2 for i in range(10000000) if i % 3 == 0 or i % 5 == 0))')
         4666672 function calls in 3.531 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  4666668    2.936    0.000    2.936    0.000 <string>:1(<genexpr>)
        1    0.001    0.001    3.529    3.529 <string>:1(<module>)
        1    0.002    0.002    3.531    3.531 {built-in method exec}
        1    0.592    0.592    3.528    3.528 {built-in method sum}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


>>> cProfile.run('sum([i * 2 for i in range(10000000) if i % 3 == 0 or i % 5 == 0])')
         5 function calls in 3.054 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    2.725    2.725    2.725    2.725 <string>:1(<listcomp>)
        1    0.078    0.078    3.054    3.054 <string>:1(<module>)
        1    0.000    0.000    3.054    3.054 {built-in method exec}
        1    0.251    0.251    0.251    0.251 {built-in method sum}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Это особенно заметно в приведенном выше примере.

ПРИМЕЧАНИЕ. Имейте в виду, что выражения генератора значительно быстрее, когда размер ваших данных превышает доступную память.

Случаи применения

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

Пример 1

def emit_lines(pattern=None):
    lines = []
    for dir_path, dir_names, file_names in os.walk('test/'):
        for file_name in file_names:
            if file_name.endswith('.py'):
                for line in open(os.path.join(dir_path, file_name)):
                    if pattern in line:
                        lines.append(line)
    return lines

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

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

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

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

Мы разделили весь наш процесс на три разных компонента:

  • Генерация множества имен файлов
  • Генерация всех строк из всех файлов
  • Фильтрация строк на основе сопоставления с образцом
def generate_filenames():
    """
    generates a sequence of opened files
    matching a specific extension
    """
    for dir_path, dir_names, file_names in os.walk('test/'):
        for file_name in file_names:
            if file_name.endswith('.py'):
                yield open(os.path.join(dir_path, file_name))

def cat_files(files):
    """
    takes in an iterable of filenames
    """
    for fname in files:
        for line in fname:
            yield line

def grep_files(lines, pattern=None):
    """
    takes in an iterable of lines
    """
    for line in lines:
        if pattern in line:
            yield line


py_files = generate_filenames()
py_file = cat_files(py_files)
lines = grep_files(py_file, 'python')
for line in lines:
    print (line)

В приведенном выше фрагменте мы не используем никаких дополнительных переменных для формирования списка строк, вместо этого мы создаем конвейер, который подает свои компоненты через процесс итерации по одному элементу за раз. grep_files принимает объект-генератор всех строк файлов *.py. Точно так же cat_file вставляет в объект генератора все имена файлов в каталоге. Таким образом весь конвейер склеивается с помощью итераций.

Пример 2

Генераторы отлично работают и для рекурсивного парсинга веб-страниц:

import requests
import re


def get_pages(link):
    links_to_visit = []
    links_to_visit.append(link)
    while links_to_visit:
        current_link = links_to_visit.pop(0)
        page = requests.get(current_link)
        for url in re.findall('<a href="([^"]+)">', str(page.content)):
            if url[0] == '/':
                url = current_link + url[1:]
            pattern = re.compile('https?')
            if pattern.match(url):
                links_to_visit.append(url)
        yield current_link


webpage = get_pages('http://sample.com')
for result in webpage:
    print(result)

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

Заключение

Генераторы позволяют нам запрашивать значения по мере необходимости, делая наши приложения более эффективными в использовании памяти и идеально подходящими для бесконечных потоков данных. Они также могут быть использованы для рефакторинга обработки из циклов, что приводит к более чистому, разъединенному коду. Если вы хотите увидеть больше примеров, ознакомьтесь с Generator Tricks for Systems Programmers и Iterator Chains as Pythonic Data Processing Pipelines.

Оригинальная статья:  Real Python  Introduction to Python Generators

Была ли вам полезна эта статья?
[31 / 3.9]

Spread the love
Подписаться
Уведомление о
guest
4 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Артём
Артём
4 лет назад

В чем смысл os.walk загонять в генератор, если os.walk и есть генератор?

def walk(top, topdown=True, onerror=None, followlinks=False):
«»»Directory tree generator.

Дарья
Дарья
4 лет назад

Очень понятно написано, спасибо автору.
Появился вопрос по примеру 1. А как вывести имена файлов, в котором найдены шаблоны?

David
David
4 лет назад
Reply to  Дарья

print(__name__)

Анонимно
Анонимно
4 лет назад

Отличная статья! Спасибо автору за точность и краткость.