Введение в генераторы Python
Оглавление
Генераторы — это функции, которые можно приостанавливать и возобновлять во время их выполнения, при этом они возвращают объект, который можно итерировать. В отличие от списков, они ленивы и поэтому работают с текущим элемент только по запросу. Таким образом, они намного эффективнее используют память при работе с большими наборами данных. В этой статье подробно описывается, как создавать функции генератор и выражения генератор, а также рассматривается пример их использования.
Функции генератора
Чтобы создать генератор, необходимо определить функцию, как обычно, но использовать 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
В чем смысл os.walk загонять в генератор, если os.walk и есть генератор?
def walk(top, topdown=True, onerror=None, followlinks=False):
«»»Directory tree generator.
Очень понятно написано, спасибо автору.
Появился вопрос по примеру 1. А как вывести имена файлов, в котором найдены шаблоны?
print(__name__)
Отличная статья! Спасибо автору за точность и краткость.