Когда не самое подходящее время для использования генераторов Python?

83

Это скорее обратное тому, для чего вы можете использовать функции генератора Python? : генераторы python, выражения генератора и itertoolsмодуль - одни из моих любимых функций python в наши дни. Они особенно полезны при настройке цепочек операций для работы с большим объемом данных - я часто использую их при обработке файлов DSV.

Так когда же не самое подходящее время использовать генератор, выражение генератора или itertoolsфункцию?

  • Когда я должен предпочесть zip()больше itertools.izip(), или
  • range()над xrange(), или
  • [x for x in foo]закончился (x for x in foo)?

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

Мы используем генераторы, чтобы не назначать новые списки в память для промежуточных данных. Это особенно важно для больших наборов данных. Имеет ли это смысл и для небольших наборов данных? Есть ли заметный компромисс между памятью и процессором?

Мне особенно интересно, проводил ли кто-нибудь какое-то профилирование по этому поводу, в свете открывающего глаза обсуждения производительности понимания списка по сравнению с map () и filter () . ( альтернативная ссылка )

Дэвид Эйк
источник
2
Я задал здесь аналогичный вопрос и провел некоторый анализ, чтобы обнаружить, что в моем конкретном примере списки быстрее для итераций длины<5 .
Alexander McFarlane
Отвечает ли это на ваш вопрос? Генераторные выражения против понимания
списков

Ответы:

57

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

1) Вам необходимо получить доступ к данным несколько раз (т.е. кешировать результаты, а не пересчитывать их):

for i in outer:           # used once, okay to be a generator or return a list
    for j in inner:       # used multiple times, reusing a list is better
         ...

2) Вам нужен произвольный доступ (или любой доступ, кроме прямого последовательного порядка):

for i in reversed(data): ...     # generators aren't reversible

s[i], s[j] = s[j], s[i]          # generators aren't indexable

3) Вам необходимо соединить строки (для чего требуется два прохода данных):

s = ''.join(data)                # lists are faster than generators in this use case

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

Раймонд Хеттингер
источник
Для №3 нельзя ли избежать двух проходов, используя ireduceдля репликации соединения?
Platinum Azure
Благодаря! Я не знал о поведении соединения строк. Можете ли вы предоставить или дать ссылку на объяснение того, почему для этого требуется два прохода?
Дэвид Эйк
5
@DavidEyk str.join выполняет один проход для суммирования длин всех фрагментов строки, поэтому он знает много памяти, которую нужно выделить для объединенного конечного результата. Второй проход копирует фрагменты строки в новый буфер для создания единой новой строки. См. Hg.python.org/cpython/file/82fd95c2851b/Objects/stringlib/…
Раймонд Хеттингер,
1
Интересно, что я очень часто использую генераторы для объединения строк. Но, интересно, как это работает, если нужно два прохода? например''.join('%s' % i for i in xrange(10))
bgusach
4
@ ikaros45 Если входные данные для присоединения не являются списком, он должен проделать дополнительную работу, чтобы создать временный список для двух проходов. Примерно это `` данные = данные if isinstance (data, list) else list (data); n = сумма (карта (len, данные)); буфер = массив байтов (n); ... <копировать фрагменты в буфер> ``.
Raymond Hettinger
40

В общем, не используйте генератор, когда вам нужны операции со списком, такие как len (), reversed () и т. Д.

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

Райан Гинстром
источник
25
Кроме того, предварительное выполнение всех вычислений гарантирует, что если при вычислении элементов списка возникнет исключение, оно будет сгенерировано в точке, где создается список , а не в цикле, который впоследствии проходит через него. Если вам нужно обеспечить безошибочную обработку всего списка, прежде чем продолжить, генераторы бесполезны.
Райан С. Томпсон
4
Неплохо подмечено. Очень неприятно пройти половину обработки генератора, только чтобы все взорвалось. Это может быть потенциально опасно.
Дэвид Эйк
26

Профиль, Профиль, Профиль.

Профилирование кода - единственный способ узнать, имеет ли вообще то, что вы делаете, какой-либо эффект.

Большинство случаев использования xrange, генераторов и т. Д. Превышают статический размер, небольшие наборы данных. Только когда вы добираетесь до больших наборов данных, это действительно имеет значение. range () vs. xrange () в основном заключается в том, чтобы сделать код немного более уродливым, и ничего не потерять, а, возможно, что-то получить.

Профиль, Профиль, Профиль.

Джеруб
источник
1
Профиль, действительно. На днях я попробую провести эмпирическое сравнение. До этого я просто надеялся, что кто-то другой уже это сделал. :)
Дэвид Эйк
Профиль, Профиль, Профиль. Я полностью согласен. Профиль, Профиль, Профиль.
Jeppe
17

Вы никогда не должны способствовать zipболее izip, rangeболее xrangeили списочным более постижениям генератора. В Python 3.0 rangeимеет xrange-как семантики и zipимеет izip-как семантики.

Понимание списков на самом деле более ясное, как list(frob(x) for x in foo)в тех случаях, когда вам нужен реальный список.

Стивен Хьювиг
источник
3
@ Стивен: Я не согласен, но мне интересно, какова причина вашего ответа. Почему понимание zip, range и list никогда не должно быть предпочтительнее соответствующей "ленивой" версии ??
mhawke
потому что, как он сказал, старое поведение zip и range скоро исчезнет.
@ Стивен: Хороший момент. Я забыл об этих изменениях в версии 3.0, что, вероятно, означает, что кто-то там убежден в своем общем превосходстве. Re: Составление списков, они часто четче (и быстрее, чем расширенные forциклы!), Но можно легко написать непонятные понимания списков.
Дэвид Эйк,
9
Я понимаю, что вы имеете в виду, но считаю, что []форма достаточно описательна (и в целом более лаконична и менее загромождена). Но это дело вкуса.
Дэвид Эйк,
4
Операции со списками выполняются быстрее для данных небольшого размера, но все происходит быстро при небольшом размере данных, поэтому вы всегда должны предпочесть генераторы, если у вас нет особой причины использовать списки (по таким причинам см. Ответ Райана Гинстрома).
Райан С. Томпсон
7

Как вы упомянули: «Это особенно важно для больших наборов данных», я думаю, это отвечает на ваш вопрос.

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

Однако, как упоминалось @ u0b34a0f6ae в комментариях, использование генераторов в начале может облегчить вам масштабирование до более крупных наборов данных.

монкут
источник
5
Генераторы +1 делают ваш код более готовым к работе с большими наборами данных, даже если вы этого не ожидаете.
u0b34a0f6ae
6

Что касается производительности: при использовании psyco списки могут быть немного быстрее, чем генераторы. В приведенном ниже примере списки почти на 50% быстрее при использовании psyco.full ().

import psyco
import time
import cStringIO

def time_func(func):
    """The amount of time it requires func to run"""
    start = time.clock()
    func()
    return time.clock() - start

def fizzbuzz(num):
    """That algorithm we all know and love"""
    if not num % 3 and not num % 5:
        return "%d fizz buzz" % num
    elif not num % 3:
        return "%d fizz" % num
    elif not num % 5:
        return "%d buzz" % num
    return None

def with_list(num):
    """Try getting fizzbuzz with a list comprehension and range"""
    out = cStringIO.StringIO()
    for fibby in [fizzbuzz(x) for x in range(1, num) if fizzbuzz(x)]:
        print >> out, fibby
    return out.getvalue()

def with_genx(num):
    """Try getting fizzbuzz with generator expression and xrange"""
    out = cStringIO.StringIO()
    for fibby in (fizzbuzz(x) for x in xrange(1, num) if fizzbuzz(x)):
        print >> out, fibby
    return out.getvalue()

def main():
    """
    Test speed of generator expressions versus list comprehensions,
    with and without psyco.
    """

    #our variables
    nums = [10000, 100000]
    funcs = [with_list, with_genx]

    #  try without psyco 1st
    print "without psyco"
    for num in nums:
        print "  number:", num
        for func in funcs:
            print func.__name__, time_func(lambda : func(num)), "seconds"
        print

    #  now with psyco
    print "with psyco"
    psyco.full()
    for num in nums:
        print "  number:", num
        for func in funcs:
            print func.__name__, time_func(lambda : func(num)), "seconds"
        print

if __name__ == "__main__":
    main()

Полученные результаты:

without psyco
  number: 10000
with_list 0.0519102208309 seconds
with_genx 0.0535933367509 seconds

  number: 100000
with_list 0.542204280744 seconds
with_genx 0.557837353115 seconds

with psyco
  number: 10000
with_list 0.0286369007033 seconds
with_genx 0.0513424889137 seconds

  number: 100000
with_list 0.335414877839 seconds
with_genx 0.580363490491 seconds
Райан Гинстром
источник
1
Это потому, что psyco вообще не ускоряет генераторы, так что это скорее недостаток psyco, чем генераторов. Хотя ответ хороший.
Steven Huwig
4
Кроме того, psyco сейчас практически не поддерживается. Все разработчики тратят время на JIT PyPy, который, насколько мне известно, оптимизирует генераторы.
Нуфал Ибрагим
3

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

Джейсон Бейкер
источник
all(True for _ in range(10 ** 8))медленнее, чем all([True for _ in range(10 ** 8)])в Python 3.8. Я бы предпочел здесь список, а не генератор
ggorlen
3

Я никогда не встречал ситуации, когда генераторы мешали бы тому, что вы пытаетесь сделать. Однако есть множество случаев, когда использование генераторов не поможет вам больше, чем их неиспользование.

Например:

sorted(xrange(5))

Не предлагает никаких улучшений по сравнению с:

sorted(range(5))
Джереми Кантрелл
источник
4
Ни один из них не предлагает никаких улучшений range(5), так как результирующий список уже отсортирован.
dan04 06
3

Вы должны предпочесть понимание списков, если вам нужно сохранить значения для чего-то еще позже, и размер вашего набора не слишком велик.

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

В некоторой степени вы можете думать о генераторах как о замене итераций (циклов), а не понимания списков как о типе инициализации структуры данных. Если вы хотите сохранить структуру данных, используйте списки.

мятный
источник
Если вам нужен только ограниченный просмотр вперед / назад в потоке, возможно, это itertools.tee()может вам помочь. Но, как правило, если вам нужно более одного прохода или произвольный доступ к некоторым промежуточным данным, сделайте их список / набор / определение.
Бени Чернявский-Паскин