How to Use Generators and yield in Python
Оглавление
- Использование генераторов
- Понимание генераторов
- Понимание инструкции Yield в Python
- Использование передовых методов генерации
- Создание Конвейеров передачи Данных с помощью Генераторов
- Заключение
Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Генераторы Python 101
Приходилось ли вам когда-нибудь работать с таким большим набором данных, что он перегружал память вашего компьютера? Или, может быть, у вас есть сложная функция, которой необходимо поддерживать внутреннее состояние при каждом вызове, но она слишком мала, чтобы оправдать создание собственного класса. В этих и многих других случаях на помощь приходят генераторы и оператор yield в Python.
К концу этой статьи вы будете знать:
- Что такое генераторы и как ими пользоваться
- Как создавать генераторные функции и выражения
- Как работает оператор Python yield
- Как использовать несколько Операторов вывода Python в функции-генераторе
- Как использовать продвинутые методы генерации
- Как создавать конвейеры передачи данных с несколькими генераторами
Если вы начинающий или специалист по питону среднего уровня и вам интересно научиться работать с большими наборами данных более понятным способом, то это руководство для вас.
Вы можете получить копию набора данных, использованного в этом руководстве, перейдя по ссылке ниже:
Загрузите набор данных: Нажмите здесь, чтобы загрузить набор данных, который вы будете использовать в этом руководстве, чтобы узнать о генераторах и yield в Python.
Использование генераторов
Введено с помощью PEP 255, функции генератора - это особый вид функций, которые возвращают ленивый итератор. Это объекты, по которым вы можете перебирать циклы, как по списку . Однако, в отличие от списков, ленивые итераторы не хранят свое содержимое в памяти. Для получения общего представления об итераторах в Python ознакомьтесь с Циклами Python “for” (определенная итерация).
Теперь, когда у вас есть общее представление о том, что делает генератор, вам может быть интересно, как он выглядит в действии. Давайте рассмотрим два примера. На первом вы увидите, как работают генераторы с высоты птичьего полета. Затем вы увеличите масштаб и изучите каждый пример более тщательно.
Пример 1: Чтение больших файлов
Генераторы обычно используются для работы с потоками данных или большими файлами, такими как CSV-файлы. В этих текстовых файлах данные разделены на столбцы с помощью запятых. Этот формат является распространенным способом обмена данными. Итак, что делать, если вы хотите подсчитать количество строк в CSV-файле? В приведенном ниже блоке кода показан один из способов подсчета этих строк:
csv_gen = csv_reader("some_csv.txt")
row_count = 0
for row in csv_gen:
row_count += 1
print(f"Row count is {row_count}")
Глядя на этот пример, можно предположить, что csv_gen
будет списком. Чтобы заполнить этот список, csv_reader()
открывает файл и загружает его содержимое в csv_gen
. Затем программа выполняет итерацию по списку и увеличивает row_count
для каждой строки.
Это разумное объяснение, но будет ли такая схема работать, если файл очень большой? Что делать, если размер файла превышает объем доступной памяти? Чтобы ответить на этот вопрос, давайте предположим, что csv_reader()
просто открывает файл и считывает его в массив:
def csv_reader(file_name):
file = open(file_name)
result = file.read().split("\n")
return result
Эта функция открывает заданный файл и использует file.read()
вместе с .split()
для добавления каждой строки в список как отдельного элемента. Если бы вы использовали эту версию csv_reader()
в блоке кода подсчета строк, который вы видели выше, то получили бы следующий результат:
Traceback (most recent call last):
File "ex1_naive.py", line 22, in <module>
main()
File "ex1_naive.py", line 13, in main
csv_gen = csv_reader("file.txt")
File "ex1_naive.py", line 6, in csv_reader
result = file.read().split("\n")
MemoryError
В этом случае open()
возвращает объект-генератор, который вы можете лениво перебирать строка за строкой. Однако file.read().split()
загружает все данные в память одновременно, вызывая MemoryError
.
Прежде чем это произойдет, вы, вероятно, заметите, что ваш компьютер замедляет сканирование. Возможно, вам даже придется завершить работу программы с помощью KeyboardInterrupt
. Итак, как вы можете обрабатывать эти огромные файлы данных? Взгляните на новое определение понятия csv_reader()
:
def csv_reader(file_name):
for row in open(file_name, "r"):
yield row
В этой версии вы открываете файл, просматриваете его и получаете строку. Этот код должен выдать следующий результат без ошибок памяти:
Row count is 64186394
Что здесь происходит? Итак, вы, по сути, превратили csv_reader()
в функцию-генератор. Эта версия открывает файл, перебирает каждую строку и выдает результат вместо того, чтобы возвращать его.
Вы также можете определить генераторное выражение (также называемое генераторным пониманием), синтаксис которого очень похож на перечислите понятия. Таким образом, вы можете использовать генератор без вызова функции:
csv_gen = (row for row in open(file_name))
Это более лаконичный способ создания списка csv_gen
. Вскоре вы узнаете больше об операторе yield в Python. А пока просто запомните это ключевое отличие:
- Использование
yield
приведет к созданию объекта generator. - Использование
return
приведет к появлению только первой строки файла .
Пример 2: Генерация бесконечной последовательности
Давайте переключим передачу и посмотрим на генерацию бесконечной последовательности. В Python, чтобы получить конечную последовательность, вы вызываете range()
и оцениваете ее в контексте списка:
>>> a = range(5)
>>> list(a)
[0, 1, 2, 3, 4]
Однако для создания бесконечной последовательности потребуется использовать генератор, поскольку объем памяти вашего компьютера ограничен:
def infinite_sequence():
num = 0
while True:
yield num
num += 1
Этот блок кода короткий и понятный. Сначала вы инициализируете переменную num
и запускаете бесконечный цикл. Затем вы немедленно yield num
, чтобы можно было зафиксировать исходное состояние. Это имитирует действие range()
.
После yield
вы увеличиваете num
на 1. Если вы попробуете это с помощью цикла for
, то увидите, что он действительно кажется бесконечным:
>>> for i in infinite_sequence():
... print(i, end=" ")
...
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
30 31 32 33 34 35 36 37 38 39 40 41 42
[...]
6157818 6157819 6157820 6157821 6157822 6157823 6157824 6157825 6157826 6157827
6157828 6157829 6157830 6157831 6157832 6157833 6157834 6157835 6157836 6157837
6157838 6157839 6157840 6157841 6157842
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyboardInterrupt
Программа будет продолжать выполняться до тех пор, пока вы не остановите ее вручную.
Вместо использования цикла for
вы также можете вызвать next()
непосредственно для объекта generator. Это особенно полезно для тестирования генератора в консоли:
>>> gen = infinite_sequence()
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
3
Здесь у вас есть генератор с именем gen
, который вы запускаете вручную, многократно вызывая next()
. Это отличная проверка работоспособности, позволяющая убедиться, что ваши генераторы выдают ожидаемый результат.
Примечание: Когда вы используете next()
, Python вызывает .__next__()
функцию, которую вы передаете в качестве параметра. Эта параметризация позволяет использовать некоторые специальные эффекты, но это выходит за рамки данной статьи. Поэкспериментируйте с изменением параметра, который вы передаете в next()
, и посмотрите, что получится!
Пример 3: Обнаружение палиндромов
Бесконечные последовательности можно использовать по-разному, но на практике они могут быть использованы для создания детекторов палиндромов. Детектор палиндромов обнаружит все последовательности букв или цифр, которые являются палиндромами. Это слова или цифры, которые читаются одинаково вперед и назад, например 121. Сначала определите свой цифровой детектор палиндромов:
def is_palindrome(num):
# Skip single-digit inputs
if num // 10 == 0:
return False
temp = num
reversed_num = 0
while temp != 0:
reversed_num = (reversed_num * 10) + (temp % 10)
temp = temp // 10
if num == reversed_num:
return num
else:
return False
Не беспокойтесь о том, чтобы понять математику, лежащую в основе этого кода. Просто обратите внимание, что функция принимает входное число, изменяет его на обратное и проверяет, совпадает ли обратное число с исходным. Теперь вы можете использовать свой генератор бесконечных последовательностей, чтобы получить текущий список всех числовых палиндромов:
>>> for i in infinite_sequence():
... pal = is_palindrome(i)
... if pal:
... print(i)
...
11
22
33
[...]
99799
99899
99999
100001
101101
102201
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "<stdin>", line 5, in is_palindrome
KeyboardInterrupt
В этом случае единственными числами, которые выводятся на консоль, являются те, которые совпадают как в прямом, так и в обратном направлении.
Примечание: На практике вы вряд ли напишете свой собственный генератор бесконечных последовательностей. Модуль itertools
предоставляет очень эффективный генератор бесконечных последовательностей с itertools.count()
.
Теперь, когда вы увидели простой пример использования генератора бесконечных последовательностей, давайте углубимся в то, как работают генераторы.
Понимание генераторов
На данный момент вы ознакомились с двумя основными способами создания генераторов: с использованием функций генератора и выражений генератора. Возможно, у вас даже есть интуитивное представление о том, как работают генераторы. Давайте воспользуемся моментом, чтобы сделать это знание немного более ясным.
Функции-генераторы выглядят и действуют так же, как обычные функции, но с одной отличительной особенностью. Функции-генераторы используют ключевое слово Python yield
вместо return
. Вспомните функцию генератора, о которой вы писали ранее:
def infinite_sequence():
num = 0
while True:
yield num
num += 1
Это выглядит как типичное определение функции, за исключением инструкции yield в Python и следующего за ней кода. yield
указывает, откуда значение отправляется обратно вызывающей стороне, но, в отличие от return
, после этого вы не выходите из функции.
Вместо этого запоминается состояние функции. Таким образом, когда next()
вызывается для объекта generator (явно или неявно в цикле for
), ранее полученная переменная num
увеличивается, а затем возвращается снова. Поскольку функции-генераторы выглядят как другие функции и действуют очень похоже на них, вы можете предположить, что выражения-генераторы очень похожи на другие выражения, доступные в Python.
Примечание: Вы плохо разбираетесь в списках, наборах и словарях Python? Вы можете ознакомиться с , эффективно используя понимание списка.
Построение генераторов с помощью генераторных выражений
Как и в случае со списками, выражения-генераторы позволяют быстро создать объект-генератор всего за несколько строк кода. Они также полезны в тех же случаях, когда используются представления списков, с дополнительным преимуществом: вы можете создавать их, не создавая и не удерживая весь объект в памяти перед итерацией. Другими словами, при использовании генераторных выражений у вас не будет нехватки памяти. Возьмем этот пример возведения некоторых чисел в квадрат:
>>> nums_squared_lc = [num**2 for num in range(5)]
>>> nums_squared_gc = (num**2 for num in range(5))
И nums_squared_lc
, и nums_squared_gc
выглядят в основном одинаково, но есть одно ключевое отличие. Вы можете его заметить? Взгляните на то, что происходит при проверке каждого из этих объектов:
>>> nums_squared_lc
[0, 1, 4, 9, 16]
>>> nums_squared_gc
<generator object <genexpr> at 0x107fbbc78>
Первый объект использовал скобки для создания списка, в то время как второй создал генераторное выражение, используя круглые скобки. Выходные данные подтверждают, что вы создали объект-генератор и что он отличается от списка.
Производительность генератора профилирования
Ранее вы узнали, что генераторы - отличный способ оптимизировать память. Хотя генератор бесконечных последовательностей является ярким примером такой оптимизации, давайте рассмотрим примеры возведения числа в квадрат, которые вы только что видели, и оценим размер результирующих объектов. Вы можете сделать это с помощью вызова sys.getsizeof()
:
>>> import sys
>>> nums_squared_lc = [i ** 2 for i in range(10000)]
>>> sys.getsizeof(nums_squared_lc)
87624
>>> nums_squared_gc = (i ** 2 for i in range(10000))
>>> print(sys.getsizeof(nums_squared_gc))
120
В этом случае размер списка, который вы получаете в результате анализа списка, составляет 87 624 байта, в то время как размер объекта generator составляет всего 120. Это означает, что размер списка более чем в 700 раз больше размера объекта generator!
Однако следует иметь в виду одну вещь. Если размер списка меньше, чем объем доступной памяти запущенной машины, то вычисление значений списка может быть быстрее, чем вычисление эквивалентного выражения генератора. Чтобы разобраться в этом, давайте суммируем результаты двух приведенных выше подходов. Вы можете сгенерировать вывод с помощью cProfile.run()
:
>>> import cProfile
>>> cProfile.run('sum([i * 2 for i in range(10000)])')
5 function calls in 0.001 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.001 0.001 0.001 0.001 <string>:1(<listcomp>)
1 0.000 0.000 0.001 0.001 <string>:1(<module>)
1 0.000 0.000 0.001 0.001 {built-in method builtins.exec}
1 0.000 0.000 0.000 0.000 {built-in method builtins.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(10000)))')
10005 function calls in 0.003 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
10001 0.002 0.000 0.002 0.000 <string>:1(<genexpr>)
1 0.000 0.000 0.003 0.003 <string>:1(<module>)
1 0.000 0.000 0.003 0.003 {built-in method builtins.exec}
1 0.001 0.001 0.003 0.003 {built-in method builtins.sum}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
Здесь вы можете видеть, что суммирование по всем значениям в списке для понимания заняло примерно треть времени по сравнению с суммированием по генератору. Если скорость является проблемой, а память - нет, то понимание списка, вероятно, является лучшим инструментом для этой работы.
Примечание: Эти измерения действительны не только для объектов, созданных с помощью генераторных выражений. Они также одинаковы для объектов, созданных с помощью аналогичной функции generator, поскольку результирующие генераторы эквивалентны.
Помните, что list-выражения возвращают полные списки, в то время как generator-выражения возвращают генераторы. Генераторы работают одинаково, независимо от того, построены ли они из функции или выражения. Использование выражения просто позволяет вам определить простые генераторы в одной строке, с предполагаемым значением yield
в конце каждой внутренней итерации.
Оператор yield в Python, безусловно, является стержнем, на котором основана вся функциональность генераторов, поэтому давайте углубимся в то, как yield
работает в Python.
Понимание инструкции Yield в Python
В целом, yield
является довольно простым оператором. Его основная задача - управлять потоком функций генератора способом, аналогичным операторам return
. Однако, как кратко упоминалось выше, у оператора yield в Python есть несколько хитростей в запасе.
Когда вы вызываете функцию-генератор или используете выражение-генератор, вы возвращаете специальный итератор, называемый генератором. Вы можете присвоить этот генератор переменной, чтобы использовать его. Когда вы вызываете специальные методы в генераторе, такие как next()
, код внутри функции выполняется до yield
.
Когда оператор Python выдает значение it, программа приостанавливает выполнение функции и возвращает полученное значение вызывающей стороне. (В отличие от этого, return
полностью останавливает выполнение функции.) Когда функция приостанавливается, состояние этой функции сохраняется. Это включает в себя любые привязки переменных, локальные для генератора, указатель команды, внутренний стек и любую обработку исключений.
Это позволяет вам возобновлять выполнение функции всякий раз, когда вы вызываете один из методов генератора. Таким образом, все вычисления функций возобновляются сразу после yield
. Вы можете увидеть это в действии, используя несколько операторов Python yield:
>>> def multi_yield():
... yield_str = "This will print the first string"
... yield yield_str
... yield_str = "This will print the second string"
... yield yield_str
...
>>> multi_obj = multi_yield()
>>> print(next(multi_obj))
This will print the first string
>>> print(next(multi_obj))
This will print the second string
>>> print(next(multi_obj))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Взгляните внимательнее на этот последний вызов next()
. Вы можете видеть, что выполнение завершилось с обратной трассировкой. Это связано с тем, что генераторы, как и все итераторы, могут быть исчерпаны. Если ваш генератор не бесконечен, вы можете выполнить итерацию только один раз. Как только все значения будут вычислены, итерация остановится и цикл for
завершится. Если вы использовали next()
, то вместо этого вы получите явное исключение StopIteration
.
Обратите внимание, что: StopIteration
- это естественное исключение, которое генерируется, чтобы сигнализировать об окончании итератора. for
циклы, например, строятся вокруг StopIteration
. Вы даже можете реализовать свой собственный for
петли с помощью while
петли:
>>> letters = ["a", "b", "c", "y"]
>>> it = iter(letters)
>>> while True:
... try:
... letter = next(it)
... except StopIteration:
... break
... print(letter)
...
a
b
c
y
Вы можете прочитать больше о StopIteration
в документации по Python, посвященной исключениям. Для получения дополнительной информации об итерации в целом ознакомьтесь с Циклами Python “for” (определенная итерация) и Циклами Python “while” (неопределенная итерация).
yield
может использоваться многими способами для управления процессом выполнения вашего генератора. Использование нескольких операторов Python yield можно использовать настолько, насколько позволяет ваш творческий потенциал.
Использование передовых методов генерации
Вы уже ознакомились с наиболее распространенными вариантами использования и конструкциями генераторов, но есть еще несколько хитростей, о которых следует рассказать. В дополнение к yield
, объекты-генераторы могут использовать следующие методы:
.send()
.throw()
.close()
Как использовать .send()
В следующем разделе вы создадите программу, которая использует все три метода. Эта программа будет печатать числовые палиндромы, как и раньше, но с некоторыми изменениями. При обнаружении палиндрома ваша новая программа добавит цифру и начнет поиск следующей. Вы также будете обрабатывать исключения с помощью .throw()
и останавливать генератор после заданного количества цифр с помощью .close()
. Сначала давайте вспомним код вашего детектора палиндромов:
def is_palindrome(num):
# Skip single-digit inputs
if num // 10 == 0:
return False
temp = num
reversed_num = 0
while temp != 0:
reversed_num = (reversed_num * 10) + (temp % 10)
temp = temp // 10
if num == reversed_num:
return True
else:
return False
Это тот же код, который вы видели ранее, за исключением того, что теперь программа возвращает строго True
или False
. Вам также потребуется изменить исходный генератор бесконечных последовательностей, например, так:
1def infinite_palindromes():
2 num = 0
3 while True:
4 if is_palindrome(num):
5 i = (yield num)
6 if i is not None:
7 num = i
8 num += 1
Здесь много изменений! Первое, что вы увидите, находится в строке 5, где i = (yield num)
. Хотя ранее вы узнали, что yield
- это утверждение, это еще не все.
Начиная с версии Python 2.5 (той же версии, в которой были представлены методы, о которых вы сейчас узнаете), yield
представляет собой выражение, а не оператор. Конечно, вы все еще можете использовать его как инструкцию. Но теперь вы также можете использовать его так, как вы видите в приведенном выше блоке кода, где i
принимает полученное значение. Это позволяет вам манипулировать полученным значением. Что еще более важно, это позволяет вам .send()
передавать значение обратно в генератор. Когда выполнение начнется после того, как yield
, i
примет отправленное значение.
Вы также проверите if i is not None
, что может произойти, если для объекта generator будет вызван next()
. (Это также может произойти, когда вы выполняете итерацию с помощью цикла for
.) Если i
имеет значение, то вы обновляете num
с новым значением. Но независимо от того, содержит ли i
значение или нет, вы затем увеличите значение num
и запустите цикл снова.
Теперь взгляните на код основной функции, который отправляет наименьшее число с другой цифрой обратно в генератор. Например, если палиндром равен 121, то он будет .send()
1000:
pal_gen = infinite_palindromes()
for i in pal_gen:
digits = len(str(i))
pal_gen.send(10 ** (digits))
С помощью этого кода вы создаете объект generator и выполняете итерацию по нему. Программа выдает значение только после того, как найден палиндром. Для определения количества цифр в этом палиндроме используется len()
. Затем он отправляет 10 ** digits
генератору. Это возвращает выполнение в логику генератора и присваивает 10 ** digits
значение i
. Поскольку i
теперь имеет значение, программа обновляет num
, увеличивает значение и снова проверяет наличие палиндромов.
Как только ваш код найдет и выдаст другой палиндром, вы выполните итерацию с помощью цикла for
. Это то же самое, что выполнить итерацию с помощью next()
. Генератор также выдает в строке 5 значение i = (yield num)
. Однако теперь значение i
равно None
,, потому что вы явно не отправляли значение.
То, что вы создали здесь, - это сопрограмма, или функция-генератор, в которую вы можете передавать данные. Они полезны для построения конвейеров передачи данных, но, как вы скоро увидите, в них нет необходимости. (Если вы хотите погрузиться глубже, то этот курс по сопрограммам и параллелизму является одним из наиболее полных доступных методов лечения.)
Теперь, когда вы узнали о .send()
, давайте взглянем на .throw()
.
Как использовать .throw()
.throw()
позволяет генерировать исключения с помощью генератора. В приведенном ниже примере вы создаете исключение в строке 6. Этот код выдаст ValueError
, как только digits
достигнет 5:
1pal_gen = infinite_palindromes()
2for i in pal_gen:
3 print(i)
4 digits = len(str(i))
5 if digits == 5:
6 pal_gen.throw(ValueError("We don't like large palindromes"))
7 pal_gen.send(10 ** (digits))
Это то же самое, что и в предыдущем коде, но теперь вы проверите, равно ли digits
5. Если это так, то у вас будет .throw()
ValueError
. Чтобы убедиться, что это работает должным образом, взгляните на выходные данные кода:
11
111
1111
10101
Traceback (most recent call last):
File "advanced_gen.py", line 47, in <module>
main()
File "advanced_gen.py", line 41, in main
pal_gen.throw(ValueError("We don't like large palindromes"))
File "advanced_gen.py", line 26, in infinite_palindromes
i = (yield num)
ValueError: We don't like large palindromes
.throw()
полезен в любых областях, где вам может понадобиться перехватить исключение. В этом примере вы использовали .throw()
для управления моментом прекращения выполнения итерации по генератору. Вы можете сделать это более элегантно с помощью .close()
.
Как использовать .close()
Как следует из названия, .close()
позволяет остановить генератор. Это может быть особенно удобно при управлении генератором бесконечной последовательности. Давайте обновим приведенный выше код, изменив .throw()
на .close()
, чтобы остановить итерацию:
1pal_gen = infinite_palindromes()
2for i in pal_gen:
3 print(i)
4 digits = len(str(i))
5 if digits == 5:
6 pal_gen.close()
7 pal_gen.send(10 ** (digits))
Вместо вызова .throw()
вы используете .close()
в строке 6. Преимущество использования .close()
заключается в том, что оно вызывает StopIteration
, исключение, используемое для сигнализации об окончании конечного итератора:
11
111
1111
10101
Traceback (most recent call last):
File "advanced_gen.py", line 46, in <module>
main()
File "advanced_gen.py", line 42, in main
pal_gen.send(10 ** (digits))
StopIteration
Теперь, когда вы узнали больше о специальных методах, которые поставляются с генераторами, давайте поговорим об использовании генераторов для построения конвейеров передачи данных.
Создание конвейеров передачи данных с помощью генераторов
Конвейеры обработки данных позволяют создавать код для обработки больших наборов данных или потоков данных без увеличения объема памяти вашего компьютера. Представьте, что у вас есть большой CSV-файл:
permalink,company,numEmps,category,city,state,fundedDate,raisedAmt,raisedCurrency,round
digg,Digg,60,web,San Francisco,CA,1-Dec-06,8500000,USD,b
digg,Digg,60,web,San Francisco,CA,1-Oct-05,2800000,USD,a
facebook,Facebook,450,web,Palo Alto,CA,1-Sep-04,500000,USD,angel
facebook,Facebook,450,web,Palo Alto,CA,1-May-05,12700000,USD,a
photobucket,Photobucket,60,web,Palo Alto,CA,1-Mar-05,3000000,USD,a
Этот пример взят из набора данных TechCrunch Continental USA, в котором описаны раунды финансирования и суммы в долларах для различных стартапов, базирующихся в США. Нажмите на ссылку ниже, чтобы загрузить набор данных:
Загрузите набор данных: Нажмите здесь, чтобы загрузить набор данных, который вы будете использовать в этом руководстве, чтобы узнать о генераторах и yield в Python.
Пришло время выполнить некоторую обработку на Python! Чтобы продемонстрировать, как создавать конвейеры с генераторами, вы проанализируете этот файл, чтобы получить общее и среднее значение для всех раундов серии A в наборе данных.
Давайте придумаем стратегию:
- Прочитайте каждую строку файла.
- Разделите каждую строку на список значений.
- Извлеките имена столбцов.
- Используйте названия столбцов и списки для создания словаря.
- Отфильтруйте раунды, которые вас не интересуют.
- Рассчитайте общее и среднее значения для интересующих вас раундов.
Обычно это можно сделать с помощью такого пакета, как pandas
, но вы также можете реализовать эту функциональность всего с помощью нескольких генераторов. Вы начнете с чтения каждой строки из файла с помощью генераторного выражения:
1file_name = "techcrunch.csv"
2lines = (line for line in open(file_name))
Затем вы будете использовать другое генераторное выражение совместно с предыдущим, чтобы разбить каждую строку на список:
3list_line = (s.rstrip().split(",") for s in lines)
Здесь вы создали генератор list_line
, который выполняет итерацию по первому генератору lines
. Это обычная схема, используемая при проектировании конвейеров генераторов. Далее вы будете извлекать имена столбцов из techcrunch.csv
. Поскольку имена столбцов, как правило, составляют первую строку в CSV-файле, вы можете использовать короткий next()
вызов:
4cols = next(list_line)
Этот вызов next()
один раз продвинет итератор по сравнению с генератором list_line
. Сложите все это вместе, и ваш код должен выглядеть примерно так:
1file_name = "techcrunch.csv"
2lines = (line for line in open(file_name))
3list_line = (s.rstrip().split(",") for s in lines)
4cols = next(list_line)
Чтобы подвести итог, вы сначала создаете генераторное выражение lines
для получения каждой строки в файле. Затем вы выполняете итерацию по этому генератору в рамках определения другого выражения генератора , называемого list_line
, которое превращает каждую строку в список значений. Затем вы выполняете повторение list_line
всего один раз с помощью next()
, чтобы получить список имен столбцов из вашего CSV-файла.
Примечание: Следите за завершением новой строки! Этот код использует .rstrip()
в выражении генератора list_line
, чтобы убедиться в отсутствии завершающих символов новой строки, которые могут присутствовать в CSV-файлах.
Чтобы упростить фильтрацию и выполнение операций с данными, вы создадите словари, в которых ключами будут имена столбцов из CSV:
5company_dicts = (dict(zip(cols, data)) for data in list_line)
Это генераторное выражение выполняет итерацию по спискам, созданным list_line
. Затем он использует zip()
и dict()
для создания словаря, как указано выше. Теперь вы будете использовать четвертый генератор, чтобы отфильтровать нужный раунд финансирования и также выбрать raisedAmt
:
6funding = (
7 int(company_dict["raisedAmt"])
8 for company_dict in company_dicts
9 if company_dict["round"] == "a"
10)
В этом фрагменте кода ваше генераторное выражение выполняет итерацию по результатам company_dicts
и принимает raisedAmt
для любого company_dict
, где ключом round
является "a"
.
Помните, что вы не повторяете все это сразу в генераторном выражении. На самом деле, вы ничего не повторяете, пока на самом деле не используете цикл for
или функцию, которая работает с повторяемыми, например sum()
. Фактически, вызовите sum()
сейчас, чтобы выполнить итерацию по генераторам:
11total_series_a = sum(funding)
Собрав все это вместе, вы получите следующий скрипт:
1file_name = "techcrunch.csv"
2lines = (line for line in open(file_name))
3list_line = (s.rstrip().split(",") for s in lines)
4cols = next(list_line)
5company_dicts = (dict(zip(cols, data)) for data in list_line)
6funding = (
7 int(company_dict["raisedAmt"])
8 for company_dict in company_dicts
9 if company_dict["round"] == "a"
10)
11total_series_a = sum(funding)
12print(f"Total series A fundraising: ${total_series_a}")
Этот скрипт объединяет все созданные вами генераторы, и все они функционируют как один большой конвейер передачи данных. Вот разбивка по строкам:
- Строка 2 считывается в каждой строке файла.
- Строка 3 разбивает каждую строку на значения и помещает значения в список.
- В строке 4 используется
next()
для хранения имен столбцов в списке. - Строка 5 создает словари и объединяет их с помощью вызова
zip()
:- Ключами являются имена столбцов
cols
из строки 4. - Значения являются строками в виде списка, созданного в строке 3.
- Ключами являются имена столбцов
- В строке 6 указаны суммы финансирования каждой компании серии А. В ней также отфильтровываются любые другие привлеченные суммы.
- Строка 11 начинает процесс итерации с вызова
sum()
, чтобы получить общую сумму финансирования серии А, найденную в CSV.
Когда вы запустите этот код на techcrunch.csv
, вы увидите, что в рамках раундов финансирования серии А было собрано в общей сложности 4 376 015 000 долларов.
Примечание: Методы обработки CSV-файлов, разработанные в этом руководстве, важны для понимания того, как использовать генераторы и оператор yield в Python. Однако, когда вы работаете с CSV-файлами в Python, вам следует вместо этого использовать модуль csv
, входящий в стандартную библиотеку Python. Этот модуль оптимизировал методы эффективной обработки CSV-файлов.
Чтобы копнуть еще глубже, попробуйте подсчитать среднюю сумму, привлеченную одной компанией в раунде серии А. Это немного сложнее, поэтому вот несколько советов:
- Генераторы исчерпывают себя после полной итерации.
- Вам все равно понадобится функция
sum()
.
Удачи!
Заключение
В этом руководстве вы узнали о генераторных функциях и генераторных выражениях.
Теперь вы знаете:
- Как использовать и записывать генераторные функции и генераторные выражения
- Как важнейший оператор yield в Python включает генераторы
- Как использовать несколько Операторов вывода Python в функции генератора
- Как использовать
.send()
для отправки данных в генератор - Как использовать
.throw()
для создания исключений генератора - Как использовать
.close()
для остановки итерации генератора - Как создать конвейер генераторов для эффективной обработки больших CSV-файлов
Вы можете получить набор данных, который вы использовали в этом руководстве, по ссылке ниже:
Загрузите набор данных: Нажмите здесь, чтобы загрузить набор данных, который вы будете использовать в этом руководстве, чтобы узнать о генераторах и yield в Python.
Как генераторы помогли вам в вашей работе или проектах? Если вы только начинаете изучать их, то как вы планируете использовать их в будущем? Нашли ли вы хорошее решение проблемы конвейера передачи данных? Сообщите нам об этом в комментариях ниже!
Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Генераторы Python 101
Back to Top