Для чего вы можете использовать функции генератора Python? - PullRequest
201 голосов
/ 19 сентября 2008

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

Ответы [ 16 ]

225 голосов
/ 19 сентября 2008

Генераторы дают вам ленивую оценку. Вы используете их, перебирая их, либо явно с помощью for, либо неявно, передавая их любой функции или конструкции, которая выполняет итерацию. Вы можете думать о генераторах как о возвращении нескольких элементов, как будто они возвращают список, но вместо того, чтобы возвращать их все сразу, они возвращают их один за другим, и функция генератора приостанавливается до тех пор, пока не будет запрошен следующий элемент.

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

Другое использование для генераторов (это действительно то же самое) - замена обратных вызовов итерацией. В некоторых ситуациях вы хотите, чтобы функция выполняла большую работу и иногда сообщала об этом вызывающей стороне. Традиционно вы использовали бы функцию обратного вызова для этого. Вы передаете этот обратный вызов рабочей функции, и он будет периодически вызывать этот обратный вызов. Генераторный подход заключается в том, что рабочая функция (теперь генератор) ничего не знает о обратном вызове и просто возвращает всякий раз, когда она хочет сообщить что-то. Вызывающий объект вместо написания отдельного обратного вызова и передачи его рабочей функции выполняет всю работу по отчетности в небольшом цикле for вокруг генератора.

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

Если вы хотите увидеть пример двух последних подходов, смотрите os.path.walk () (старая функция обхода файловой системы с обратным вызовом) и os.walk () (новый генератор обхода файловой системы.) Конечно, если вы действительно хотите собрать все результаты в списке, подход с использованием генератора является тривиальным для преобразования в подход с большим списком:

big_list = list(the_generator)
86 голосов
/ 19 сентября 2008

Одна из причин использования генератора - сделать решение более понятным для некоторых решений.

Другой - обрабатывать результаты по одному, избегая создания огромных списков результатов, которые вы в любом случае будете обрабатывать отдельно.

Если у вас есть функция Фибоначчи-до-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 элементов не будет создан вообще, только одно значение за раз. Это не будет иметь место при использовании версии списка, где список будет создан первым.

41 голосов
/ 19 сентября 2008

См. Раздел «Мотивация» в PEP 255 .

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

36 голосов
/ 18 января 2013

Я нахожу это объяснение, которое очищает мои сомнения. Потому что есть вероятность, что человек, который не знает Generators, также не знает о yield

Возвращение

Оператор return - это место, где все локальные переменные уничтожаются, а полученное значение возвращается (возвращается) вызывающей стороне. Если через некоторое время эта же функция будет вызвана, она получит новый набор переменных.

Выход

Но что, если локальные переменные не выбрасываются при выходе из функции? Это означает, что мы можем 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 (), генератор возобновляет работу с того места, где он остановился (он запоминает все значения данных и какой оператор был выполнен в последний раз).

29 голосов
/ 08 мая 2014

Пример из реального мира

Предположим, у вас есть 100 миллионов доменов в таблице MySQL, и вы хотите обновить 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()
26 голосов
/ 19 сентября 2008

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

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

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

21 голосов
/ 12 апреля 2009

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

В качестве абстрактного примера можно привести генератор чисел Фибоначчи, который не существует в цикле и при его вызове из любого места всегда будет возвращать следующий номер в последовательности:

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 Generators - что это такое? и с небольшим воображением, вы можете придумать множество других ситуаций, в которых генераторы делают большие альтернатива for циклам и другим традиционным итерационным конструкциям.

18 голосов
/ 19 сентября 2008

простое объяснение: Рассмотрим заявление 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

С генераторами вы можете также выполнять итерации по бесконечным последовательностям, что, конечно, невозможно при итерации по контейнерам.

12 голосов
/ 19 сентября 2008

Мое любимое использование - операции «фильтр» и «уменьшение».

Допустим, мы читаем файл и хотим только строки, начинающиеся с "##".

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

Опять же, мы можем использовать этот генератор в правильном цикле for.

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

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

8 голосов
/ 27 сентября 2014

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

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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...