Хорошая идея - иметь возможность использовать язык генератора, такой как «yield»?

9

PHP, C #, Python и, вероятно, некоторые другие языки имеют yieldключевое слово, которое используется для создания функций генератора.

В PHP: http://php.net/manual/en/language.generators.syntax.php

В Python: https://www.pythoncentral.io/python-generators-and-yield-keyword/

В C #: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/yield

Я обеспокоен тем, что как языковая функция / средство yieldнарушает некоторые соглашения. Одним из них является то, что я бы назвал "уверенность". Это метод, который возвращает разные результаты каждый раз, когда вы вызываете его. Вы можете вызывать обычную функцию, не являющуюся генератором, и если ей будет дан тот же вход, он вернет тот же результат. С помощью yield он возвращает различный вывод в зависимости от своего внутреннего состояния. Таким образом, если вы случайно вызываете генерирующую функцию, не зная ее предыдущего состояния, вы не можете ожидать, что она вернет определенный результат.

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

Деннис
источник
4
yieldэто по сути государственный двигатель. Это не означает, что нужно возвращать один и тот же результат каждый раз. Что он будет делать с абсолютной уверенностью, так это возвращать следующий элемент в перечисляемом каждый раз, когда он вызывается. Темы не требуются; вам нужно закрытие (более или менее), чтобы поддерживать текущее состояние.
Роберт Харви
1
Что касается качества «определенности», учтите, что при одной и той же входной последовательности ряд вызовов итератора приведет к абсолютно одинаковым элементам в абсолютно одинаковом порядке.
Роберт Харви
4
Я не уверен, откуда большинство ваших вопросов, потому что в C ++ нет такого yield ключевого слова, как в Python. У него есть статический метод std::this_thread::yield(), но это не ключевое слово. Таким образом, к нему this_threadбудет добавлен практически любой вызов, что делает очевидным, что это библиотечная функция только для получения потоков, а не языковая функция для получения потока управления в целом.
Ixrec
ссылка обновлена ​​до C #, удалена одна для C ++
Деннис

Ответы:

16

Сначала предостережения - C # - это язык, который я знаю лучше всего, и хотя он имеет язык, yieldкоторый очень похож на другие языки » yield, могут быть тонкие различия, о которых я не знаю.

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

Туфта. Вы действительно ожидаете Random.Nextили Console.ReadLine возвращаете один и тот же результат каждый раз, когда звоните им? Как насчет отдыха звонки? Аутентификация? Получить товар из коллекции? Есть все виды (хорошие, полезные) функции, которые нечисты.

Как такая функция вписывается в языковую парадигму? Это на самом деле нарушает какие-либо соглашения?

Да, yieldочень плохо играет try/catch/finallyи не допускается ( https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ для больше информации).

Это хорошая идея, чтобы иметь и использовать эту функцию?

Это, безусловно, хорошая идея, чтобы иметь эту функцию. Такие вещи, как LINQ в C #, действительно хороши - ленивая оценка коллекций дает большое преимущество в производительности и yieldпозволяет делать такие вещи в части кода с частью ошибок, которые мог бы сделать ручной итератор.

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

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

Не совсем. Компилятор генерирует итератор конечного автомата, который отслеживает, где он остановился, чтобы он мог начать там снова при следующем вызове. Процесс генерации кода делает что-то похожее на стиль продолжения продолжения, когда код после yieldнего перетаскивается в свой собственный блок (и, если он имеет какие-либо yields, другой подблок и т. Д.). Это хорошо известный подход, который чаще используется в функциональном программировании, а также обнаруживается в асинхронной / ожидающей компиляции C #.

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

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

Telastyn
источник
Я никогда не использовал C # серьезно, но это yieldключевое слово похоже на сопрограммы, да, или что-то другое? Если так, то я хотел бы иметь один в C! Я могу вспомнить хотя бы несколько приличных участков кода, которые было бы намного проще написать с помощью такой языковой функции.
2
@DrunkCoder - похоже, но с некоторыми ограничениями, насколько я понимаю.
Теластин
1
Вы также не хотели бы, чтобы доходность использовалась неправильно. Чем больше возможностей в языке, тем больше вероятность того, что программа будет плохо написана на этом языке. Я не уверен, что правильный подход к написанию доступного языка - это бросить все это на вас и посмотреть, что прилипнет.
Нил
1
@DrunkCoder: это ограниченная версия полу-сопрограмм. На самом деле, он обрабатывается компилятором как синтаксический паттерн, который расширяется в серию вызовов методов, классов и объектов. (По сути, компилятор генерирует объект продолжения, который захватывает текущий контекст в полях.) Реализация по умолчанию для коллекций является полу-сопрограммой, но перегрузив «магические» методы, используемые компилятором, вы можете на самом деле настроить поведение. Например, до того, как async/ awaitбыл добавлен к языку, кто-то реализовал его, используя yield.
Йорг Миттаг
1
@Neil Как правило, можно использовать практически любую функцию языка программирования. Если то, что вы говорите, было правдой, то было бы гораздо сложнее плохо программировать на C, чем на Python или C #, но это не так, поскольку в этих языках есть много инструментов, которые защищают программистов от многих ошибок, которые очень легко сделать с C. В действительности, причина плохих программ - плохие программисты - это проблема, не зависящая от языка.
Бен Коттрелл
12

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. Примечательно, что я пропустил бесконечные генераторы, конечные автоматы, возвращение значений и их связь с сопрограммами.

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

Джоэл Хармон
источник
6

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

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

Вопрос в том, где захватить изменчивое состояние. В классическом ООП изменяемое состояние существует на уровне объекта. Но если языковая поддержка закрывается, у вас может быть изменяемое состояние на уровне функций. Например в JavaScript:

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

Короче говоря, yieldэто естественно в языке, который поддерживает замыкания, но неуместен в языке, подобном более старой версии Java, где изменяемое состояние существует только на уровне объекта.

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

На мой взгляд, это не очень хорошая особенность. Это плохая особенность, в первую очередь потому, что ее нужно учить очень осторожно, а все учат этому неправильно. Люди используют слово «генератор», разделяющее функцию генератора и объект генератора. Вопрос в следующем: кто или что делает на самом деле?

Это не просто мое мнение. Даже Гвидо в бюллетене PEP, в котором он руководствуется этим, признает, что функция генератора - это не генератор, а «фабрика генераторов».

Это важно, не правда ли? Но, прочитав 99% документации, у вас сложится впечатление, что функция генератора является фактическим генератором, и они склонны игнорировать тот факт, что вам также нужен объект генератора.

Гвидо подумал о замене «def» на «gen» для этих функций и сказал «Нет». Но я бы сказал, что этого было бы недостаточно. Это действительно должно быть:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
user320927
источник