Для чого можна використовувати функції генератора Python?


213

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


6
може бути, краще питання, коли ми не повинні використовувати 'em
cregox

1
Приклад реального світу тут
Гірі

Відповіді:


239

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

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

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

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

Якщо ви хочете побачити приклад двох останніх підходів, див. Os.path.walk () (стара функція ходіння файлової системи із зворотним викликом) та os.walk () (новий генератор ходіння файлової системи.) Звичайно, якщо ви дуже хотіли зібрати всі результати у списку, підхід генератора тривіальний, щоб перетворитись на підхід у великому списку:

big_list = list(the_generator)

Чи виконує такий генератор, як той, який створює списки файлової системи, паралельно коду, який запускає цей генератор у циклі? В ідеалі комп'ютер би виконував тіло циклу (обробляючи останній результат), одночасно роблячи все, що повинен зробити генератор, щоб отримати наступне значення.
Стівен Лу

@StevenLu: Якщо не виникає проблем із ручним запуском ниток перед yieldта joinпісля них, щоб отримати наступний результат, він не виконується паралельно (і це не робить жоден стандартний генератор бібліотек; таємно запускаються потоки нахмурені). Генератор робить паузу при кожному, yieldпоки не буде запропоновано наступне значення. Якщо генератор завершує введення / виведення, ОС може проактивно кешувати дані з файлу за умови, що він буде запитаний незабаром, але це ОС, Python не задіяний.
ShadowRanger

90

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

Інша - обробляти результати поодинці, уникаючи складання величезних списків результатів, які ви б так чи інакше обробляли.

Якщо у вас є така функція підвищення рівня n:

# function version
def fibon(n):
    a = b = 1
    result = []
    for i in xrange(n):
        result.append(a)
        a, b = b, a + b
    return result

Ви можете легше записати функцію так:

# generator version
def fibon(n):
    a = b = 1
    for i in xrange(n):
        yield a
        a, b = b, a + b

Функція чіткіша. І якщо ви використовуєте цю функцію:

for x in fibon(1000000):
    print x,

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


18
а якщо вам потрібен список, ви завжди можете це зробитиlist(fibon(5))
endolith

41

Дивіться розділ "Мотивація" в PEP 255 .

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


1
Розділ Мотивація хороший тим, що він має конкретний приклад: "Коли функція виробника має достатньо важку роботу, що вимагає підтримання стану між виробленими значеннями, більшість мов програмування не пропонують приємного та ефективного рішення, крім додавання функції зворотного виклику до аргументу виробника список ... Наприклад, tokenize.py у стандартній бібліотеці застосовує такий підхід "
Бен Крізі

38

Я вважаю це пояснення, яке знімає сумніви. Тому що існує можливість, про яку не знає Generatorsі людина, яка не знаєyield

Повернення

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

Вихід

Але що робити, якщо локальні змінні не викидаються, коли ми виходимо з функції? Це означає, що ми можемо resume the functionтам, де зупинилися. Саме generatorsтут вводиться концепція , і yieldзаява поновлюється там, де functionзупинено.

  def generate_integers(N):
    for i in xrange(N):
    yield i

    In [1]: gen = generate_integers(3)
    In [2]: gen
    <generator object at 0x8117f90>
    In [3]: gen.next()
    0
    In [4]: gen.next()
    1
    In [5]: gen.next()

Отже, в цьому різниця між returnі yieldтвердженнями в Python.

Оперативність виходу - це те, що робить функцію функцією генератора.

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


33

Приклад реального світу

Скажімо, у вашій таблиці MySQL є 100 мільйонів доменів, і ви хочете оновити рейтинг Alexa для кожного домену.

Перше, що вам потрібно - це вибрати доменні імена з бази даних.

Скажімо, назва вашої таблиці - це ім'я domainsта стовпець domain.

Якщо ви використовуєте, SELECT domain FROM domainsце поверне 100 мільйонів рядків, що забирає багато пам'яті. Так ваш сервер може вийти з ладу.

Тож ви вирішили запускати програму партіями. Скажімо, наш розмір партії - 1000.

У нашій першій партії ми запитаємо перші 1000 рядків, перевіримо рейтинг Alexa для кожного домену та оновимо рядок бази даних.

У нашій другій партії ми будемо працювати над наступними 1000 рядками. У нашій третій партії це буде з 2001 по 3000 і так далі.

Тепер нам потрібна функція генератора, яка генерує наші партії.

Ось наша функція генератора:

def ResultGenerator(cursor, batchsize=1000):
    while True:
        results = cursor.fetchmany(batchsize)
        if not results:
            break
        for result in results:
            yield result

Як бачите, наша функція зберігає yieldрезультати. Якщо ви використовували ключове слово returnзамість yield, тоді вся функція буде закінчена, як тільки вона досягне повернення.

return - returns only once
yield - returns multiple times

Якщо функція використовує ключове слово, yieldто це генератор.

Тепер ви можете повторити так:

db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
    doSomethingWith(result)
db.close()

було б практичніше, якби врожайність могла бути пояснена з точки зору рекурсивного / динамічного програмування!
ігаурав

27

Буферизація. Коли дані можуть отримувати дані великими фрагментами, але обробляти їх невеликими шматками, то генератор може допомогти:

def bufferedFetch():
  while True:
     buffer = getBigChunkOfData()
     # insert some code to break on 'end of data'
     for i in buffer:    
          yield i

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


3
Якщо getBigChuckOfData не лінивий, то я не розумію, яка тут користь. Який випадок використання цієї функції?
Шон Джеффрі Піц

1
Але справа в тому, що IIUC, buferiFetch ліниво закликає отриматиBigChunkOfData. Якщо getBigChunkOfData вже лінивий, тоді buferiFetch виявиться марним. Кожен виклик bufterFetch () поверне один буферний елемент, навіть якщо BigChunk вже був прочитаний. І вам не потрібно чітко тримати підрахунок наступного елемента, щоб повернутись, оскільки механіка урожайності робить це явно.
hmijail сумує у відставці

21

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

Абстрактним прикладом може бути генератор чисел Фібоначчі, який не живе в циклі, і коли він викликається з будь-якого місця, завжди повертає наступне число у послідовності:

def fib():
    first = 0
    second = 1
    yield first
    yield second

    while 1:
        next = first + second
        yield next
        first = second
        second = next

fibgen1 = fib()
fibgen2 = fib()

Тепер у вас є два об'єкти генератора чисел Фібоначчі, на які можна зателефонувати з будь-якого місця вашого коду, і вони завжди повертатимуть все більші числа Фібоначчі в послідовності наступним чином:

>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5

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

Я отримав приклад Фібоначчі від генераторів Python - Що вони? і, трохи уявивши, ви можете придумати безліч інших ситуацій, коли генератори створюють чудову альтернативу forпетлям та іншим традиційним ітераційним конструкціям.


19

Просте пояснення: Розгляньте forтвердження

for item in iterable:
   do_stuff()

Багато часу, всі предмети в них iterableне повинні бути там з самого початку, але можуть бути створені на льоту, як потрібно. Це може бути набагато ефективніше в обох

  • місця (вам ніколи не потрібно зберігати всі предмети одночасно) та
  • час (ітерація може закінчитися, перш ніж всі елементи потрібні).

В іншому випадку ви навіть не знаєте всіх предметів достроково. Наприклад:

for command in user_input():
   do_stuff_with(command)

Ви не можете заздалегідь знати всі команди користувача, але ви можете використовувати приємний цикл, як цей, якщо у вас генератор передає команди:

def user_input():
    while True:
        wait_for_command()
        cmd = get_command()
        yield cmd

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


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

@mataap: Є itertoolдля цього - див cycles.
мартіно

12

Мої улюблені використання - це операції "фільтрація" та "зменшення".

Скажімо, ми читаємо файл і хочемо лише рядки, які починаються з "##".

def filter2sharps( aSequence ):
    for l in aSequence:
        if l.startswith("##"):
            yield l

Потім ми можемо використовувати функцію генератора у відповідному циклі

source= file( ... )
for line in filter2sharps( source.readlines() ):
    print line
source.close()

Приклад зменшення подібний. Скажімо, у нас є файл, де нам потрібно знаходити блоки <Location>...</Location>рядків. [Не теги HTML, а рядки, схожі на теги.]

def reduceLocation( aSequence ):
    keep= False
    block= None
    for line in aSequence:
        if line.startswith("</Location"):
            block.append( line )
            yield block
            block= None
            keep= False
        elif line.startsWith("<Location"):
            block= [ line ]
            keep= True
        elif keep:
            block.append( line )
        else:
            pass
    if block is not None:
        yield block # A partial block, icky

Знову ж таки, ми можемо використовувати цей генератор у відповідному циклі.

source = file( ... )
for b in reduceLocation( source.readlines() ):
    print b
source.close()

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


8
fileobj.readlines()буде читати весь файл до списку в пам'яті, перемагаючи мету використання генераторів. Оскільки файлові об’єкти вже ітерабельні, ви можете використовувати for b in your_generator(fileobject):замість цього. Таким чином ваш файл буде читатися один рядок, щоб уникнути читання цілого файлу.
nosklo

reduLocation - це досить дивний підсумок виходу списку, чому б не просто дати кожен рядок? Також фільтруйте і зменшуйте вбудовані елементи з очікуваною поведінкою (див. Довідку в ipython тощо), ваше використання "зменшити" те саме, що і фільтр.
Джеймс Антілл

Хороший момент у рядках (). Я зазвичай розумію, що файли є першокласними ітераторами рядків під час тестування одиниць.
S.Lott

Власне, "зменшення" - це об'єднання декількох окремих ліній у складений об'єкт. Гаразд, це список, але це все-таки скорочення, взяте з джерела.
S.Lott

9

Практичний приклад, де ви могли б скористатися генератором, це якщо у вас є якась форма і ви хочете перебрати його кути, краї та будь-яку іншу. Для мого власного проекту (вихідний код тут ) у мене був прямокутник:

class Rect():

    def __init__(self, x, y, width, height):
        self.l_top  = (x, y)
        self.r_top  = (x+width, y)
        self.r_bot  = (x+width, y+height)
        self.l_bot  = (x, y+height)

    def __iter__(self):
        yield self.l_top
        yield self.r_top
        yield self.r_bot
        yield self.l_bot

Тепер я можу створити прямокутник і петлю за його кутами:

myrect=Rect(50, 50, 100, 100)
for corner in myrect:
    print(corner)

Замість цього __iter__ви можете мати метод iter_cornersі викликати це за допомогою for corner in myrect.iter_corners(). Це просто більш елегантно використовувати __iter__з того часу, ми можемо використовувати ім'я екземпляра класу безпосередньо у forвиразі.


Я обожнював ідею передачі подібних класових полів як генератор
eusoubrasileiro

7

В основному уникайте функцій зворотного дзвінка під час ітерації над підтримкою вхідного стану.

Дивіться тут і тут для огляду того, що можна зробити за допомогою генераторів.


4

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


3

Оскільки спосіб відправлення генератора не згадувався, ось приклад:

def test():
    for i in xrange(5):
        val = yield
        print(val)

t = test()

# Proceed to 'yield' statement
next(t)

# Send value to yield
t.send(1)
t.send('2')
t.send([3])

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

Девід Бізлі про генератори на PyCon 2014


2

Я використовую генератори, коли наш веб-сервер виконує функції проксі:

  1. Клієнт вимагає проксі-сервера з сервера
  2. Сервер починає завантажувати цільовий URL
  3. Сервер дає змогу повернути клієнту результати, як тільки він їх отримує

1

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

def primes():
    primes_found = set()
    primes_found.add(2)
    yield 2
    for i in itertools.count(1):
        candidate = i * 2 + 1
        if not all(candidate % prime for prime in primes_found):
            primes_found.add(candidate)
            yield candidate

Потім ви можете використовувати це для створення продуктів наступних праймерів:

def prime_products():
    primeiter = primes()
    prev = primeiter.next()
    for prime in primeiter:
        yield prime * prev
        prev = prime

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


якщо ні, то будь-який (кандидат% прем'єр для прем'єр у primes_found) має бути, якщо всі (кандидат% прем'єр для прем'єр у primes_found)
rjmunro

Так, я мав намір написати "якщо не будь-який (кандидат% prime == 0 за прем'єр у primes_found). Хоча твій трохи акуратніше. :)
Нік Джонсон,

Я думаю, ви забули видалити "не" з, якщо не всі (кандидат% prime для прем'єр-міністра в primes_found)
Thava

0

Також добре для друку простих чисел до n:

def genprime(n=10):
    for num in range(3, n+1):
        for factor in range(2, num):
            if num%factor == 0:
                break
        else:
            yield(num)

for prime_num in genprime(100):
    print(prime_num)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.