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

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

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

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

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

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

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

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

Alexander McFarlane 28.06.2016 05:28

Отвечает ли это на ваш вопрос? Выражения генератора против понимания списков

ggorlen 12.07.2020 23:05
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
83
2
17 510
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

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

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

@ Стивен: Я не возражаю, но мне интересно, какова причина вашего ответа. Почему понимание zip, range и list никогда не должно быть предпочтительнее соответствующей "ленивой" версии ??

mhawke 29.10.2008 09:05

потому что, как он сказал, старое поведение zip и range скоро исчезнет.

user3850 29.10.2008 13:01

@ Стивен: Хороший момент. Я забыл об этих изменениях в версии 3.0, а это, вероятно, означает, что кто-то наверху убежден в своем общем превосходстве. Re: Составление списков, они часто более понятны (и быстрее, чем расширенные циклы for!), Но можно легко написать непонятные понимания списков.

David Eyk 29.10.2008 18:54

Я имел в виду, что список (frob (x) for x in foo) более описательный, чем [frob (x) for x in foo] - то есть понимание списка [] «сахар» бесполезно.

Steven Huwig 29.10.2008 20:45

Я понимаю, что вы имеете в виду, но считаю, что форма [] достаточно описательна (и в целом более лаконична и менее загромождена). Но это дело вкуса.

David Eyk 30.10.2008 18:57

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

David Eyk 30.10.2008 18:59

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

Ryan Ginstrom 01.11.2008 09:34

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

Ryan C. Thompson 06.03.2011 23:12

Это довольно слабое место, я не могу вспомнить случай, когда вы не можете получить ленивую версию, играя с try/except NameError или ImportError.

bgusach 03.11.2014 12:29

Использование timeit в Python 3.8 дает [frob(x) for x in foo] на 50% быстрее, чем list(frob(x) for x in foo). Последнее не является пониманием списка, как говорится в сообщении, - это генератор выражения.

ggorlen 12.07.2020 22:04

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

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

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

Ryan C. Thompson 06.03.2011 23:06

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

David Eyk 03.11.2011 21:03

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

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

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

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

u0b34a0f6ae 13.10.2009 22:38

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

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

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

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

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

David Eyk 29.10.2008 19:01

Профиль, Профиль, Профиль. Я полностью согласен. Профиль, Профиль, Профиль.

Jeppe 07.04.2020 23:59

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

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

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

Например:

sorted(xrange(5))

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

sorted(range(5))

Ни один из них не предлагает никаких улучшений по сравнению с range(5), поскольку результирующий список уже отсортирован.

dan04 06.01.2014 23:19

Что касается производительности: при использовании 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

Это потому, что psyco вообще не ускоряет генераторы, поэтому это скорее недостаток psyco, чем генераторов. Но хороший ответ.

Steven Huwig 03.11.2008 23:10

Кроме того, psyco сейчас практически не обслуживается. Все разработчики тратят время на JIT PyPy, который, насколько мне известно, оптимизирует генераторы.

Noufal Ibrahim 31.12.2009 19:55

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

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

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

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

Beni Cherniavsky-Paskin 31.12.2009 16:33
Ответ принят как подходящий

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

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 29.10.2014 19:48

Спасибо! Я не знал о поведении соединения строк. Можете ли вы предоставить или дать ссылку на объяснение, почему для этого требуется два прохода?

David Eyk 29.10.2014 21:47

@DavidEyk str.join делает один проход для суммирования длин всех фрагментов строки, чтобы он знал много памяти, которую нужно выделить для комбинированного конечного результата. Второй проход копирует фрагменты строки в новый буфер для создания одной новой строки. См. hg.python.org/cpython/file/82fd95c2851b/Objects/stringlib/…

Raymond Hettinger 30.10.2014 08:56

Интересно, что я очень часто использую генераторы для объединения строк. Но, интересно, как это работает, если нужно два прохода? например ''.join('%s' % i for i in xrange(10))

bgusach 03.11.2014 12:26

@ ikaros45 Если вход в присоединиться не является списком, он должен проделать дополнительную работу для создания временного списка для двух проходов. Примерно это `` данные = данные if isinstance (data, list) else list (data); n = сумма (карта (len, данные)); буфер = массив байтов (n); ... <копировать фрагменты в буфер> ``.

Raymond Hettinger 03.11.2014 18:03

Другие вопросы по теме