Коли не сприятливий час для використання генераторів пітонів?


83

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

Тож коли не сприятливий час використовувати генератор, або вираз генератора, або itertoolsфункцію?

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

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

Ми використовуємо генератори, щоб не призначати нові списки в пам’яті для проміжних даних. Це особливо має сенс для великих наборів даних. Чи є сенс також для невеликих наборів даних? Чи є помітний компроміс пам’яті / процесора?

Мені особливо цікаво, якщо хтось робив деякі профілі з цього приводу, у світлі привабливого обговорення продуктивності розуміння списку порівняно з map () та filter () . ( alt посилання )


2
Я поставив подібне питання тут і провів аналіз, щоб виявити, що в моєму конкретному прикладі списки швидші для ітерацій довжини<5 .
Олександр Макфарлейн

Це відповідає на ваше запитання? Вирази генератора проти розуміння списку
ggorlen

Відповіді:


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
Цікаво, що я дуже часто використовую генератори, щоб приєднувати srings. Але, цікаво, як це працює, якщо потрібно два проходи? наприклад''.join('%s' % i for i in xrange(10))
bgusach

4
@ ikaros45 Якщо вхід для приєднання не є списком, він повинен виконати додаткову роботу, щоб створити тимчасовий список для двох проходів. Приблизно це `` дані = дані, якщо isinstance (дані, список) ще список (дані); n = сума (карта (об'єктив, дані)); буфер = байтовий масив (n); ... <скопіювати фрагменти в буфер> ``.
Реймонд Хеттінгер,

40

Загалом, не використовуйте генератор, коли вам потрібні операції зі списком, такі як len (), reversed () тощо.

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


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

4
Це хороший момент. Дуже засмучує пройти половину шляху обробки генератора, лише щоб все вибухнуло. Це потенційно може бути небезпечним.
Девід Ейк,

26

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

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

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

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


1
Профіль, справді. Днями я спробую провести емпіричне порівняння. До того часу я просто сподівався, що хтось інший уже мав. :)
Девід Ейк,

Профіль, Профіль, Профіль. Я цілком згоден. Профіль, Профіль, Профіль.
Джеппе

17

Ви ніколи не повинні віддавати перевагу zipнад izip, rangeнад xrangeабо перераховувати розуміння, аніж генераторські. У Python 3.0 rangeє xrange-подібна семантика та zipмає izip-подібна семантика.

Розуміння списку насправді чіткіше, як list(frob(x) for x in foo)у ті часи, коли вам потрібен фактичний список.


3
@Steven Я не погоджуюсь, але мені цікаво, в чому полягає аргументація вашої відповіді. Чому розуміння zip, діапазону та списку ніколи не слід віддавати перевагу відповідній "ледачій" версії ??
mhawke

тому що, як він сказав, стара поведінка zip та діапазону скоро зникне.

@ Стівен: Гарна думка. Я забув про ці зміни в 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
Крім того, псико зараз майже не підтримується. Усі розробники проводять час на PyPy's JIT, який, наскільки мені відомо, оптимізує генератори.
Ноуфал Ібрагім

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

3

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

Наприклад: ви створюєте список, який ви кілька разів повторите у своїй програмі.

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


Якщо вам потрібен лише обмежений огляд уперед / назад за потоком, то, можливо, вам itertools.tee()можуть допомогти. Але загалом, якщо ви хочете більше одного проходу або довільного доступу до деяких проміжних даних, складіть їх список / набір / склад.
Бені Чернявський-Паскін
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.