Чи хороша ідея створення мови для генератора, такого як "урожай"?


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 порушує деякі умовності. Один з них - це те, про що я б посилався, - «визначеність». Це метод, який повертає інший результат щоразу, коли ви його називаєте. За допомогою звичайної негенераторної функції ви можете викликати її, і якщо їй подати один і той же вхід, він поверне той самий вихід. З врожайністю він повертає різний вихід, виходячи з внутрішнього стану. Таким чином, якщо ви випадково викликаєте функцію, що генерує, не знаючи її попереднього стану, ви не можете очікувати, що вона поверне певний результат.

Як така функція вписується в мовну парадигму? Чи насправді це порушує якісь умовності? Це гарна ідея мати та використовувати цю функцію? (наводити приклад того, що добре, а що погано, gotoколись було властивістю багатьох мов і досі є, але це вважається шкідливим і як таке було вилучено з деяких мов, наприклад, Java). Чи повинні компілятори / інтерпретатори мови програмування порушувати будь-які умови, щоб реалізувати таку функцію, наприклад, чи має мова реалізувати багатопотоковість, щоб ця функція працювала, чи це можна зробити без технології нарізки?


4
yieldпо суті є двигуном держави. Це не означає щоразу повертати один і той же результат. Що це буде робити з абсолютною впевненістю, це повернути наступний товар у безлічі кожного разу, коли він буде викликаний. Нитки не потрібні; вам потрібно закрити (більш-менш), щоб зберегти поточний стан.
Роберт Харві

1
Щодо якості "визначеності", врахуйте, що, враховуючи однакову послідовність введення, серія викликів ітератору дасть абсолютно ті ж самі елементи в точно такому ж порядку.
Роберт Харві

4
Я не впевнений, звідки береться більшість ваших запитань, оскільки у C ++ немає такого yield ключового слова, як у Python. Він має статичний метод std::this_thread::yield(), але це не ключове слово. Таким чином, він this_threadби передбачив майже будь-який виклик до нього, що робить його досить очевидним, що це бібліотечна функція лише для отримання потоків, а не мовна функція щодо отримання потоку управління загалом.
Іксрек

посилання оновлено до C #, одне для C ++ видалено
Денніс

Відповіді:


16

Перші застереження - C # - це мова, яку я найкраще знаю, і хоча вона 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/ для більше інформації).

Це гарна ідея мати та використовувати цю функцію?

Це, звичайно, гарна ідея. Такі речі, як C # 's LINQ, справді приємні - ліниво оцінюючи колекції надає велику користь від продуктивності, і yieldдозволяє подібне робити в частині коду з часткою помилок, які б передавав вручну ітератор.

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

Чи повинні компілятори / інтерпретатори мови програмування порушувати будь-які умови, щоб реалізувати таку функцію, наприклад, чи має мова реалізувати багатопотоковість, щоб ця функція працювала, чи це можна зробити без технології нарізки?

Не зовсім. Компілятор генерує ітератор стану машини, який відслідковує, де він зупинився, щоб він міг запуститися туди наступного разу, коли він буде викликаний. Процес генерації коду чимось схожий на стиль продовження передачі, де код після yieldвтягується у власний блок (і якщо він маєyield s, інший підблок тощо). Це добре відомий підхід, який частіше відключається у функціональному програмуванні, а також з'являється в компіляції асинхронізації / очікування C #.

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

Однак, загалом, yieldце відносно низький вплив, який справді допомагає у вирішенні певного набору проблем.


Я ніколи не використовував C # серйозно, але це yieldключове слово схоже на розробки, так, чи щось інше? Якщо так, я хотів би, щоб я мав його в C! Я можу придумати хоча б якісь пристойні розділи коду, які було б набагато простіше написати з такою мовною особливістю.

2
@DrunkCoder - схожий, але з деякими обмеженнями, наскільки я це розумію.
Теластин

1
Ви також не хочете, щоб врожайність не використовувалася. Чим більше можливостей має мова, тим більше шансів на те, що ви знайдете програму, написану погано на цій мові. Я не впевнений, чи правильний підхід до написання доступної мови - це кинути все на вас і подивитися, що палички.
Ніл

1
@DrunkCoder: це обмежена версія напівпрограм. Насправді компілятор трактує його як синтаксичний зразок, який розширюється на низку викликів методів, класів та об'єктів. (В основному, компілятор генерує об’єкт продовження, який фіксує поточний контекст у полях.) За замовчуванням реалізація колекцій є напівпрограмою, але, перевантажуючи «магічні» методи, якими користується компілятор, ви можете фактично налаштувати поведінку. Наприклад, до async/ awaitдодавали до мови, хтось реалізував її за допомогою yield.
Йорг W Міттаг

1
@Neil Зазвичай можна зловживати практично будь-якою функцією мови програмування. Якщо те, що ви говорите, було правдою, тоді програмувати з використанням C, ніж Python або C #, набагато складніше, але це не так, оскільки в цих мовах є багато інструментів, які захищають програмістів від багатьох помилок, які дуже прості. робити з C. Насправді причиною поганих програм є погані програмісти - це цілком мовно-агностична проблема.
Бен Коттрелл

12

Чи yieldє хороша ідея з мовою для генератора, наприклад ?

Я хотів би відповісти на це з точки зору Python з виразним так, це чудова ідея .

Почну з розгляду деяких питань та припущень у вашому запитанні, а потім продемонструю розповсюдженість генераторів та їх необґрунтовану корисність у Python пізніше.

За допомогою звичайної негенераторної функції ви можете викликати її, і якщо їй подати один і той же вхід, він поверне той самий вихід. З врожайністю він повертає різний вихід, виходячи з внутрішнього стану.

Це помилково. Методи на об'єктах можна розглядати як самі функції, із власним внутрішнім станом. У 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))

Перший, другий та третій рядки просто визначають генератор кожен, але не виконують жодної реальної роботи. Коли викликається останній рядок, сума запитує числовий стовпець для значення, числовий стовпець потребує значення з останнього стовпця, останній стовпець запитує значення з logfile, яке потім фактично зчитує рядок з файлу. Цей стек розгортається, поки сума не отримає перше ціле число. Потім процес повторюється для другого рядка. На даний момент сума має два цілі числа, і вона додає їх разом. Зауважте, що третій рядок ще не було прочитано з файлу. Потім сума надходить на запит значень з числового стовпчика (повністю не звертаючи уваги на решту ланцюга) та додавання їх, поки числовий стовпець не вичерпається.

Дійсно цікава частина тут полягає в тому, що рядки читаються, споживаються та відкидаються окремо. У жодному разі весь файл не запам'ятовується відразу. Що станеться, якщо цей файл журналу є, скажімо, терабайт? Це просто працює, тому що читає лише один рядок.

Висновок

Це не повний огляд усіх застосувань генераторів в Python. Помітно, я пропустив нескінченні генератори, стан машини, передаючи значення назад і їх взаємозв'язок з супровідними програмами.

Я вважаю, що достатньо продемонструвати, що ви можете мати генератори як чітко інтегровану корисну мову.


6

Якщо ви звикли до класичних мов OOP, генератори і yieldможуть здатися нездужаючими, оскільки стан, що змінюється, зберігається на рівні функції, а не на рівні об'єкта.

Питання "визначеності" - це червона оселедець. Зазвичай це називається референтною прозорістю , і в основному означає, що функція завжди повертає той самий результат для тих же аргументів. Як тільки у вас змінюється стан, ви втрачаєте еталонну прозорість. В OOP об'єкти часто мають стан, що змінюється, що означає, що результат виклику методу залежить не тільки від аргументів, але і від внутрішнього стану об'єкта.

Питання в тому, де захопити стан, що змінюється. У класичному ООП змінне стан існує на рівні об'єкта. Але якщо мовна підтримка закривається, у вас може виникнути стан змін на рівні функції. Наприклад у JavaScript:

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

Коротше кажучи, yieldце природно в мові, яка підтримує закриття, але вона буде поза місцем у такій мові, як старіша версія Java, де змінюваний стан існує лише на об'єктному рівні.


Я думаю, якби мовні особливості мали спектр, вихід був би настільки далеко від функціонального, як міг би бути. Це не обов'язково погано. OOP колись був дуже модним, і знову пізніше функціональним програмуванням. Я вважаю, що небезпека цього насправді становить змішування та узгодження таких функцій, як урожай з функціональним дизайном, який змушує вашу програму вести себе несподівано.
Ніл

0

На мою думку, це не є гарною особливістю. Це погана риса, насамперед тому, що її потрібно дуже уважно навчати, і всі вчать її неправильно. Люди використовують слово "генератор", позначаючи функцію генератора та об'єкт генератора. Питання: тільки хто чи що робить фактичний урожай?

Це не просто моя думка. Навіть Гвідо в бюлетені PEP, в якому він керується цим питанням, визнає, що функція генератора - це не генератор, а "фабрика генераторів".

Це наче важливо, ви не думаєте? Але прочитавши 99% документації там, ви складете враження, що функція генератора - це власне генератор, і вони, як правило, ігнорують той факт, що вам також потрібен об’єкт генератора.

Гвідо подумав замінити "def" на "gen" для цих функцій і сказав "Ні". Але я б стверджував, що цього б не було достатньо. Це дійсно повинно бути:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.