yield
Хорошая идея - иметь ли возможность пользоваться языком генератора ?
Я хотел бы ответить на это с точки зрения Python с решительным да, это отличная идея .
Сначала я рассмотрю некоторые вопросы и предположения в вашем вопросе, а затем продемонстрирую распространенность генераторов и их необоснованную полезность в Python.
Вы можете вызывать обычную функцию, не являющуюся генератором, и если ей будет дан тот же вход, он вернет тот же результат. С помощью yield он возвращает различный вывод в зависимости от своего внутреннего состояния.
Это неверно Методы на объектах могут рассматриваться как сами функции с их собственным внутренним состоянием. В Python, поскольку все является объектом, вы можете фактически получить метод из объекта и передать этот метод (который привязан к объекту, из которого он получен, поэтому он запоминает свое состояние).
Другие примеры включают намеренно случайные функции, а также методы ввода, такие как сеть, файловая система и терминал.
Как такая функция вписывается в языковую парадигму?
Если языковая парадигма поддерживает такие вещи, как функции первого класса, а генераторы поддерживают другие языковые функции, такие как протокол Iterable, то они легко вписываются.
Это на самом деле нарушает какие-либо соглашения?
Нет. Так как он встроен в язык, соглашения построены вокруг и включают (или требуют!) Использование генераторов.
Нужно ли компиляторам / интерпретаторам языка программирования нарушать какие-либо соглашения для реализации такой функции?
Как и с любой другой функцией, компилятор просто должен быть разработан для поддержки этой функции. В случае с Python функции уже являются объектами с состоянием (такими как аргументы по умолчанию и аннотации функций).
должен ли язык реализовывать многопоточность, чтобы эта функция работала, или это можно сделать без технологии многопоточности?
Интересный факт: стандартная реализация Python вообще не поддерживает многопоточность. Он имеет глобальную блокировку интерпретатора (GIL), поэтому на самом деле ничего не выполняется одновременно, если вы не запускаете второй процесс для запуска другого экземпляра Python.
примечание: примеры в Python 3
За пределами урожайности
Хотя yield
ключевое слово можно использовать в любой функции, чтобы превратить его в генератор, это не единственный способ его создания. Python содержит выражения генератора, мощный способ четко выразить генератор в терминах другого итерируемого (включая другие генераторы)
>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155
Как видите, синтаксис не только чист и читаем, но и встроенные функции, такие как sum
генераторы принятия.
С
Ознакомьтесь с предложением по улучшению Python для оператора With . Это сильно отличается от того, что вы можете ожидать от оператора With на других языках. С небольшой помощью стандартной библиотеки генераторы Python прекрасно работают как контекстные менеджеры для них.
>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
print("preprocessing", arg)
yield arg
print("postprocessing", arg)
>>> with debugWith("foobar") as s:
print(s[::-1])
preprocessing foobar
raboof
postprocessing foobar
Конечно, печатать вещи - это самая скучная вещь, которую вы можете здесь сделать, но она показывает видимые результаты. Более интересные варианты включают автоматическое управление ресурсами (открытие и закрытие файлов / потоков / сетевых подключений), блокировку параллелизма, временное завершение или замену функции и распаковку, а затем повторное сжатие данных. Если вызов функций подобен внедрению кода в ваш код, то операторы подобны переносу частей вашего кода в другой код. Как бы вы ни использовали его, это хороший пример легкой привязки к языковой структуре. Генераторы на основе доходности - не единственный способ создания контекстных менеджеров, но они, безусловно, удобны.
И частичное истощение
Для циклов в Python работать интересно. Они имеют следующий формат:
for <name> in <iterable>:
...
Во-первых, выражение, которое я вызвал <iterable>
, вычисляется для получения итерируемого объекта. Во-вторых, итерируемый __iter__
вызвал это, и получающийся итератор сохранен за кулисами. Затем __next__
вызывается итератор, чтобы получить значение для привязки к имени, которое вы вводите <name>
. Этот шаг повторяется до тех пор, пока вызов __next__
кидает a StopIteration
. Исключение поглощается циклом for, и выполнение продолжается оттуда.
Возвращаясь к генераторам: когда вы вызываете __iter__
генератор, он просто возвращает себя.
>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272
Это означает, что вы можете отделить итерации по чему-то от того, что вы хотите с ним сделать, и изменить это поведение на середине пути. Ниже обратите внимание, как один и тот же генератор используется в двух циклах, а во втором он начинает выполняться с того места, где он остановился с первого.
>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
print(ord(letter))
if letter > 'p':
break
109
111
114
>>> for letter in generator:
print(letter)
e
b
o
r
i
n
g
s
t
u
f
f
Ленивая оценка
Одна из недостатков генераторов по сравнению со списками - единственное, что вы можете получить в генераторе, это следующее, что из этого получается. Вы не можете вернуться к предыдущему результату или перейти к следующему без промежуточных результатов. Положительным моментом является то, что генератор может почти не занимать память по сравнению с аналогичным списком.
>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
File "<pyshell#10>", line 1, in <module>
sys.getsizeof([x for x in range(10000000000)])
File "<pyshell#10>", line 1, in <listcomp>
sys.getsizeof([x for x in range(10000000000)])
MemoryError
Генераторы также могут быть лениво прикованы цепью.
logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))
Первая, вторая и третья строки просто определяют каждый генератор, но не выполняют никакой реальной работы. Когда вызывается последняя строка, sum запрашивает у numericcolumn значение, numericcolumn требуется значение из lastcolumn, lastcolumn запрашивает значение из файла журнала, который затем фактически читает строку из файла. Этот стек раскручивается, пока сумма не получит свое первое целое число. Затем процесс повторяется для второй строки. На данный момент сумма имеет два целых числа и складывает их вместе. Обратите внимание, что третья строка еще не была прочитана из файла. Затем Sum продолжает запрашивать значения у числового столбца (полностью игнорируя остальную часть цепочки) и добавлять их, пока числовой столбец не будет исчерпан.
Здесь действительно интересно то, что строки читаются, потребляются и отбрасываются по отдельности. Ни в коем случае не весь файл в памяти все сразу. Что произойдет, если этот файл журнала, скажем, терабайт? Это просто работает, потому что он читает только одну строку за раз.
Вывод
Это не полный обзор всех применений генераторов в Python. Примечательно, что я пропустил бесконечные генераторы, конечные автоматы, возвращение значений и их связь с сопрограммами.
Я верю, что этого достаточно, чтобы продемонстрировать, что у вас могут быть генераторы как чисто интегрированная, полезная языковая функция.
yield
это по сути государственный двигатель. Это не означает, что нужно возвращать один и тот же результат каждый раз. Что он будет делать с абсолютной уверенностью, так это возвращать следующий элемент в перечисляемом каждый раз, когда он вызывается. Темы не требуются; вам нужно закрытие (более или менее), чтобы поддерживать текущее состояние.