Розуміння генераторів в Python


218

Я зараз читаю кулінарну книгу Python і зараз переглядаю генератори. Мені важко обвести голову.

Як я походжу з тла Java, чи існує еквівалент Java? Книга говорила про "Виробник / Споживач", проте коли я чую, що я думаю про нарізку.

Що таке генератор і для чого ви його використовуєте? Без цитування жодної книги, очевидно (якщо тільки ви не зможете знайти гідну, спрощену відповідь прямо з книги). Можливо, з прикладами, якщо ви відчуваєте себе щедрими!

Відповіді:


402

Примітка: ця публікація передбачає синтаксис Python 3.x.

Генератор просто функція , яка повертає об'єкт , на якому ви можете зателефонувати next, наприклад , що для кожного виклику повертає яке - то значення, поки воно не викликає StopIterationвиключення, сигналізуючи , що всі значення були отримані. Такий об’єкт називається ітератором .

Звичайні функції повертають одне значення за допомогою return, як і в Java. Однак у Python існує альтернатива, що називається yield. Використання в yieldбудь-якому місці функції робить його генератором. Дотримуйтесь цього коду:

>>> def myGen(n):
...     yield n
...     yield n + 1
... 
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Як бачите, myGen(n)це функція, яка приносить nі n + 1. Кожен виклик nextотримує одне значення, поки всі значення не отримані. forпетлі викликають nextна задньому плані, таким чином:

>>> for n in myGen(6):
...     print(n)
... 
6
7

Так само є генераторні вирази , які забезпечують спосіб коротко описати певні поширені типи генераторів:

>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Зауважте, що вирази генератора так само схожі на розуміння списку :

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

Зауважте, що об’єкт генератора генерується один раз , але його код не запускається всі відразу. Тільки дзвінки nextфактично виконувати (частина) коду. Виконання коду в генераторі зупиняється, коли yieldбуло досягнуто оператора, після якого він повертає значення. Наступний виклик до nextцього приводить до продовження виконання в стані, в якому генератор залишився після останнього yield. Це принципова відмінність від регулярних функцій: ті завжди починають виконання у верхній частині та відкидають стан після повернення значення.

На цю тему слід сказати більше речей. Наприклад, можна sendповернути дані в генератор ( посилання ). Але це те, що я пропоную вам не заглядати, поки ви не зрозумієте основну концепцію генератора.

Тепер ви можете запитати: навіщо використовувати генератори? Є кілька вагомих причин:

  • Деякі поняття можна описати набагато коротше, використовуючи генератори.
  • Замість створення функції, яка повертає список значень, можна написати генератор, який генерує значення на льоту. Це означає, що ніякого списку не потрібно складати, це означає, що отриманий код є більш ефективним у пам’яті. Таким чином можна навіть описати потоки даних, які просто були б занадто великими, щоб вміститися в пам'яті.
  • Генератори дозволяють природним способом описати нескінченні потоки. Розглянемо для прикладу числа Фібоначчі :

    >>> def fib():
    ...     a, b = 0, 1
    ...     while True:
    ...         yield a
    ...         a, b = b, a + b
    ... 
    >>> import itertools
    >>> list(itertools.islice(fib(), 10))
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

    Цей код використовує itertools.isliceдля отримання нескінченної кількості елементів з нескінченного потоку. Вам рекомендується добре ознайомитися з функціями в itertoolsмодулі, оскільки вони є важливими інструментами для запису сучасних генераторів з великою легкістю.


   Про Python <= 2.6: у наведених вище прикладах nextє функція, яка викликає метод __next__на даному об'єкті. У Python <= 2,6 використовується дещо інша техніка, а не o.next()замість next(o). Python 2.7 має next()виклик, .nextтому вам не потрібно використовувати наступне в 2.7:

>>> g = (n for n in range(3, 5))
>>> g.next()
3

9
Ви згадуєте, що можна sendпередавати дані в генератор. Як тільки ви це зробите, у вас є "співавтор". Реалізувати такі моделі, як згаданий споживач / виробник, дуже просто за допомогою процедур, оскільки вони не потребують Locks, а тому не можуть зайти в тупик. Важко описати співпрограми без розбиття ниток, тому я просто скажу, що супроводи є дуже елегантною альтернативою нитці.
Йохен Рітцель

Чи генератори Python є в основному машинами Тьюрінга з точки зору їх функціонування?
Вогненний Фенікс

48

Генератор - це фактично функція, яка повертає (дані) до її закінчення, але вона зупиняється в цій точці, і ви можете відновити функцію в цій точці.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

і так далі. (Або одна) перевага генераторів полягає в тому, що, оскільки вони обробляють дані по одному фрагменту, можна обробляти велику кількість даних; зі списками, надмірні потреби в пам'яті можуть стати проблемою. Генератори, як і списки, є ітерабельними, тому їх можна використовувати однаково:

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

Зауважте, що генератори надають інший спосіб боротьби з нескінченністю, наприклад

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

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


30

Перш за все, термін генератор спочатку був дещо неправильно визначений в Python, що призводило до великої плутанини. Ви, мабуть, маєте на увазі ітератори та ітерабелі (див. Тут ). Потім в Python є також функції генератора (які повертають об'єкт генератора), об'єкти генератора (які є ітераторами) і вирази генератора (які оцінюються об'єктом генератора).

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

Можливо, все-таки буде хорошою ідеєю бути точним і уникати терміна "генератор" без додаткових уточнення.


2
Хм, я думаю, ти маєш рацію, принаймні згідно тесту на кілька рядків у Python 2.6. Вираз генератора повертає ітератор (він же "об’єкт генератора"), а не генератор.
Крейг МакКуїн

22

Генератори можна вважати скороченими для створення ітератора. Вони поводяться як ітератор Java. Приклад:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Сподіваюся, що це допомагає / це те, що ви шукаєте.

Оновлення:

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

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...

1
Тепер у Java є Streams, які набагато більше схожі на генератори, за винятком того, що ви, очевидно, не можете просто отримати наступний елемент без дивної кількості клопоту.
Фонд позову Моніки

12

Немає еквівалента Java.

Ось трохи надуманий приклад:

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

У генераторі є цикл, який працює від 0 до n, і якщо змінна циклу кратна 3, вона дає змінну.

Під час кожної ітерації forциклу генератор виконується. Якщо генератор вперше виконується, він запускається на початку, інакше він продовжується з попереднього часу, коли він дав результат.


2
Останній абзац є дуже важливим: стан генераторної функції 'заморожується' кожного разу, коли він видає sth, і продовжується в точно такому ж стані, коли його викликають наступного разу.
Йоганнес Шарра

У Java немає синтаксичного еквівалента "генераторному вираженню", але генератори - коли ви їх отримали - по суті є лише ітератором (ті ж основні характеристики, що ітератор Java).
перемогти

@overthink: Добре, що у генераторів можуть бути й інші побічні ефекти, яких ітератори Java не можуть мати. Якби я ставлю print "hello"після цього x=x+1в своєму прикладі, "привіт" було б надруковано 100 разів, тоді як тіло циклу для виконання було б виконане лише 33 рази.
Wernsey

@iWerner: Досить впевнений, що такий же ефект міг би мати і в Java. Реалізація next () в еквівалентному ітераторі Java все ще повинна шукати від 0 до 99 (використовуючи приклад mygen (100)), щоб ви могли System.out.println () кожен раз, якщо хочете. Ви повернетесь лише 33 рази з наступного (). Що не вистачає Java - це дуже зручний синтаксис урожайності, який значно простіше читати (і записувати).
переосмислити

Я любив читати і пам’ятати цей один рядок: якщо генератор це вперше виконується, він запускається на початку, інакше він продовжується з попереднього часу, коли він вийшов.
Ікра.

8

Мені подобається описувати генераторів, тих, хто має пристойний досвід роботи в мовах програмування та обчислювальних технологій, з точки зору фрейму стека.

У багатьох мовах є стек, поверх якого знаходиться поточний стек "кадр". Кадр стека включає простір, виділений для змінних, локальних для функції, включаючи аргументи, передані цій функції.

Коли ви викликаєте функцію, на стек висувається поточна точка виконання ("лічильник програми" або еквівалент) і створюється новий кадр стека. Потім виконання виконується на початку виклику функції.

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

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

До Python 2.5 це робили всі генератори. Python 2.5 також додав можливість передавати значення в генератор. При цьому значення, що передається, доступне у вигляді вираження, що є результатом заяви про вихід, який тимчасово повернув контроль (і значення) від генератора.

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


6

Єдине, що я можу додати до відповіді Stephan202, - це рекомендація, щоб ви подивилися на презентацію Девіда Бізлі «PyCon '08» «Трюкові генератори для системних програмістів», що є найкращим єдиним поясненням того, як і чому генераторів, які я бачив де завгодно. Це те, що перевело мене від "Python виглядає дуже весело" до "Це те, що я шукав". Це за адресою http://www.dabeaz.com/generators/ .


6

Це допомагає чітко розрізняти функцію foo та генератор foo (n):

def foo(n):
    yield n
    yield n+1

foo - це функція. foo (6) - це генераторний об'єкт.

Типовий спосіб використання об'єкта генератора - це цикл:

for n in foo(6):
    print(n)

Петля друкує

# 6
# 7

Розгляньте генератор як функцію відновлення.

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

Коли ви називаєте bar=foo(6)панель об'єктів генератора, ви знаєте, що ви маєте nextатрибут.

Ви можете викликати його самостійно, щоб отримати значення, отримані з foo:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

Коли foo закінчується (і більше немає next(bar)вихідних значень), виклик кидає помилку StopInteration.


5

У цій публікації будуть використані числа Фібоначчі як інструмент для пояснення корисності генераторів Python .

У цій публікації будуть представлені як C ++, так і Python-код.

Числа Фібоначчі визначаються як послідовність: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....

Або взагалі:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

Це можна перенести у функцію C ++ надзвичайно легко:

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

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

Наприклад:, Fib(3) = Fib(2) + Fib(1)але Fib(2)також здійснює перерахунок Fib(1). Чим вище значення ви хочете обчислити, тим гірше ви будете.

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

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

Але це дуже негарно, і це ускладнює нашу логіку main. Краще б не турбуватися про стан у нашомуmain функції.

Ми могли б повернути vectorзначення значень і використовувати iteratorітерацію над цим набором значень, але для цього потрібно багато пам'яті відразу для великої кількості повернених значень.

Тож повертаємось до нашого старого підходу, що трапиться, якщо ми хотіли зробити щось інше, крім того, щоб надрукувати цифри? Нам доведеться скопіювати і вставити весь блок коду mainі змінити вихідні оператори на все, що ми хотіли зробити. А якщо ви копіюєте та вставляєте код, то вас слід зняти. Ви не хочете, щоб вас застрелили, чи не так?

Щоб вирішити ці проблеми та уникнути пострілу, ми можемо переписати цей блок коду за допомогою функції зворотного виклику. Щоразу, коли виникає нове число Фібоначчі, ми б викликали функцію зворотного виклику.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

Це очевидно вдосконалення, ваша логіка mainне така захаращена, і ви можете робити все, що завгодно, з числами Фібоначчі, просто визначити нові зворотні дзвінки.

Але це все ще не ідеально. Що робити, якщо ви хотіли отримати лише перші два числа Фібоначчі, а потім щось зробити, потім ще трохи, а потім зробити щось інше?

Ну, ми могли би продовжуватись так, як ми були, і ми можемо знову почати додавати стан main, дозволяючи GetFibNumbers починати з довільної точки. Але це ще більше розширить наш код, і він вже виглядає занадто великим для такої простої задачі, як друк цифр Фібоначчі.

Ми могли реалізувати модель виробника та споживача через пару ниток. Але це ще більше ускладнює код.

Замість цього давайте поговоримо про генератори.

У Python є дуже приємна мовна функція, яка вирішує такі проблеми, як названі генератори.

Генератор дозволяє виконувати функцію, зупинятися в довільній точці, а потім продовжувати знову там, де ви зупинилися. Кожен раз повертаючи значення.

Розглянемо наступний код, який використовує генератор:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

Що дає нам результати:

0 1 1 2 3 5

yieldОператор використовується в зв'язці з генераторами на Python. Це зберігає стан функції та повертає зведене значення. Наступного разу, коли ви зателефонуєте на наступну () функцію на генераторі, вона продовжиться там, де вихід відключений.

Це набагато більш чисто, ніж код функції зворотного дзвінка. У нас є чистіший код, менший код, не кажучи вже про набагато більш функціональний код (Python дозволяє довільно великі цілі числа).

Джерело


3

Я вважаю, що перша поява ітераторів та генераторів була мовою програмування Icon, приблизно 20 років тому.

Вам може сподобатися огляд Icon , який дозволяє обмотати голову навколо них, не концентруючись на синтаксисі (оскільки Ікона - це мова, яку ви, мабуть, не знаєте, і Грісволд пояснював переваги своєї мови людям, які походять з інших мов).

Прочитавши лише кілька абзаців, корисність генераторів та ітераторів може стати більш очевидною.


2

Досвід розуміння списків показав їх широку корисність на всій Python. Однак у багатьох випадках використання не потрібно мати повний список, створений у пам'яті. Натомість їм потрібно лише перебирати елементи по одному.

Наприклад, наступний код підсумовування створить повний перелік квадратів у пам'яті, повторить ці значення і, коли посилання більше не потрібне, видалить список:

sum([x*x for x in range(10)])

Пам'ять зберігається, використовуючи генераторний вираз:

sum(x*x for x in range(10))

Подібні переваги надаються конструкторам для контейнерних об'єктів:

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

Вирази генератора особливо корисні з такими функціями, як sum (), min () та max (), які зводять ітерабельний вхід до одного значення:

max(len(line)  for line in file  if line.strip())

більше


1

Я виставив цей фрагмент коду, який пояснює 3 ключові поняття про генератори:

def numbers():
    for i in range(10):
            yield i

gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers

for i in gen: #we iterate over the generator and the values are printed
    print(i)

#the generator is now empty

for i in gen: #so this for block does not print anything
    print(i)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.