Генераторні вирази та розуміння списку


411

Коли слід використовувати генераторні вирази і коли слід використовувати розуміння списку в Python?

# Generator expression
(x*2 for x in range(256))

# List comprehension
[x*2 for x in range(256)]

27
може [exp for x in iter]бути просто цукор list((exp for x in iter))? чи є різниця у виконанні?
b0fh

1
думаю, у мене виникло відповідне питання, тож при використанні урожайності ми можемо використовувати лише вираз генератора з функції, або ми повинні використовувати вихід для функції для повернення генераторного об'єкта?

28
@ b0fh Дуже пізня відповідь на ваш коментар: у Python2 є невелика різниця, змінна циклічність просочиться із розуміння списку, тоді як вираз генератора не просочиться. Порівняйте X = [x**2 for x in range(5)]; print xз Y = list(y**2 for y in range(5)); print y, друге видасть помилку. У Python3 розуміння списку справді є синтаксичним цукром для генераторного виразу, що подається, list()як ви очікували, тому змінна циклу більше не просочується .
Bas Swinckels

12
Я б запропонував прочитати PEP 0289 . Підсумований "Цей ПЕП представляє генераторні вирази як високу продуктивність, узагальнену пам'ять узагальнення списків та генераторів" . Тут також є корисні приклади, коли їх використовувати.
icc97

5
@ icc97 Я також спізнююсь на вісім років на вечірку, і посилання PEP було ідеальним. Дякуємо, що зробили це легко!
eenblam

Відповіді:


283

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

def gen():
    return (something for something in get_some_stuff())

print gen()[:2]     # generators don't support indexing or slicing
print [5,6] + gen() # generators can't be added to lists

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

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


70
Іноді доводиться використовувати генератори - наприклад, якщо ви пишете заходи з спільного планування з використанням урожайності. Але якщо ви це робите, ви, мабуть, не ставите цього питання;)
ephemient

12
Я знаю, що це старе, але я думаю, що варто зазначити, що генератори (і будь-які ітерабельні) можна додавати до списків із розширенням: a = [1, 2, 3] b = [4, 5, 6] a.extend(b)- a тепер буде [1, 2, 3, 4, 5, 6]. (Чи можете ви додавати нові коментарі у коментарі ??)
jarvisteve

12
@jarvisteve ваш приклад несе слова, які ви говорите. Тут також є прекрасний момент. Списки можна подовжувати генераторами, але тоді не було сенсу робити його генератором. Генератори не можуть бути розширені списками, і генератори не зовсім ітерабельні. a = (x for x in range(0,10)), b = [1,2,3]наприклад. a.extend(b)кидає виняток. b.extend(a)оцінюватиме все, і в цьому випадку немає сенсу робити його генератором в першу чергу.
Слейтер Вікторофв

4
@SlaterTyranus ви на 100% правильні, і я висловив за точність. тим не менше, я вважаю, що його коментар є корисною невідповіддю на питання ОП, тому що він допоможе тим, хто опинився тут, тому що вони набрали щось подібне до "поєднати генератор із розумінням списку" в пошукову систему.
rbp

1
Чи не буде причина використання генератора для повторного повторення (наприклад, моя стурбованість відсутністю пам'яті переосмислює мою стурбованість щодо "отримання" значень по одному ), ймовірно, все ще застосовується при повторному повторенні? Я б сказав, що це може зробити список більш корисним, але чи достатньо цього, щоб переважати проблеми з пам'яттю - це щось інше.
Роб Грант

181

Ітерація над виразом генератора чи розумінням списку зробить те саме. Однак розуміння списку спочатку створить весь список у пам'яті, тоді як вираз генератора створить елементи на льоту, тож ви зможете використовувати його для дуже великих (а також нескінченних!) Послідовностей.


39
+1 для нескінченності. Ви не можете зробити це зі списком, незалежно від того, наскільки мало ви дбаєте про продуктивність.
Пол Дрейпер

Чи можете ви створити нескінченні генератори, використовуючи метод розуміння?
AnnanFay

5
@Annan Тільки якщо ви вже маєте доступ до іншого нескінченного генератора. Наприклад, itertools.count(n)є нескінченна послідовність цілих чисел, починаючи з n, так (2 ** item for item in itertools.count(n))би була нескінченна послідовність повноважень 2починаючи з 2 ** n.
Кевін

2
Генератор видаляє елементи з пам'яті після їх повторення. Тому швидко, якщо у вас є великі дані, ви просто хочете їх відобразити, наприклад. Це не свиня для пам’яті. з генераторами елементи обробляються "за потребою". якщо ви хочете затриматися на списку або повторити його знову (тому зберігайте елементи), тоді використовуйте розуміння списку.
j2emanue

102

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

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


2
Це, мабуть, буде трохи поза темою, але, на жаль, "не переглянене" ... Що б в цьому контексті означало "першорядне значення"? Я не є носієм англійської мови ... :)
Гільєрмо Арес

6
@GuillermoAres - це прямий результат "гуглінгу" для першочергового значення: важливіший за все інше; вищий.
Snađошƒаӽ

1
Так listsшвидше, ніж generatorвирази? Прочитавши відповідь dF, зрозуміло, що це навпаки.
Хассан Байг

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

59

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

Уявіть, що у вас є файл журналу 2 ТБ під назвою "величезний файл.txt", і ви хочете, щоб вміст і довжина для всіх рядків, що починаються з слова "ВПІТИ".

Тож ви спробуйте почати, написавши список розуміння:

logfile = open("hugefile.txt","r")
entry_lines = [(line,len(line)) for line in logfile if line.startswith("ENTRY")]

Це розбиває весь файл, обробляє кожен рядок і зберігає відповідні рядки у вашому масиві. Отже, цей масив може містити до 2 ТБ вмісту. Це багато оперативної пам’яті і, мабуть, не практичне для ваших цілей.

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

logfile = open("hugefile.txt","r")
entry_lines = ((line,len(line)) for line in logfile if line.startswith("ENTRY"))

З нашого файлу ще не було прочитано жодного рядка. Насправді, скажімо, ми хочемо ще більше відфільтрувати результат:

long_entries = ((line,length) for (line,length) in entry_lines if length > 80)

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

Давайте випишемо наші відфільтровані рядки в інший файл:

outfile = open("filtered.txt","a")
for entry,length in long_entries:
    outfile.write(entry)

Тепер ми читаємо вхідний файл. Оскільки наш forцикл продовжує запитувати додаткові лінії, long_entriesгенератор вимагає від entry_linesгенератора рядків , повертаючи лише ті, довжина яких перевищує 80 символів. А в свою чергу entry_linesгенератор запитує рядки (відфільтровані так, як зазначено) від logfileітератора, який у свою чергу зчитує файл.

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


46

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

Наприклад:

sum(x*2 for x in xrange(256))

dict( (k, some_func(k)) for k in some_list_of_keys )

Перевага полягає в тому, що список не генерується повністю, і, таким чином, використовується мало пам'яті (і також має бути швидше)

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

Наприклад:

reversed( [x*2 for x in xrange(256)] )

9
Для мови прямо підказано, що вирази генератора призначені для використання таким чином. Втрачайте дужки! sum(x*2 for x in xrange(256))
u0b34a0f6ae

8
sortedі reversedдобре працювати над будь-якими ітерабельними включеними виразами генератора.
marr75

1
Якщо ви можете використовувати 2.7 і вище, то приклад dict () виглядає краще як розуміння диктату (PEP для цього старше, ніж генераторні вирази PEP, але потрібно більше часу для приземлення)
Юрген А. Ерхард

14

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

>>> mylist = ["a", "b", "c"]
>>> gen = (elem + "1" for elem in mylist)
>>> mylist.clear()
>>> for x in gen: print (x)
# nothing

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


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

4

Іноді ви можете піти від функції трійника з itertools , вона повертає кілька ітераторів для одного і того ж генератора, який можна використовувати самостійно.


4

Я використовую модуль Hadoop Mincemeat . Я думаю, що це чудовий приклад, щоб взяти до відома:

import mincemeat

def mapfn(k,v):
    for w in v:
        yield 'sum',w
        #yield 'count',1


def reducefn(k,v): 
    r1=sum(v)
    r2=len(v)
    print r2
    m=r1/r2
    std=0
    for i in range(r2):
       std+=pow(abs(v[i]-m),2)  
    res=pow((std/r2),0.5)
    return r1,r2,res

Тут генератор отримує числа з текстового файлу (розміром до 15 Гб) і застосовує просту математику до цих чисел, використовуючи зменшення карти Hadoop. Якби я не використовував функцію урожайності, а натомість розуміння списку, знадобилось би набагато більше часу для обчислення сум і середнього рівня (не кажучи вже про складність простору).

Hadoop - прекрасний приклад використання всіх переваг генераторів.

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