57
задан 4 February 2019 в 20:42

8 ответов

Выражения генератора Python являются поздним связыванием (см. PEP 289 - Выражения Генератора ) (что другой отвечает на "ленивый" звонок):

Раннее связывание по сравнению с Поздним связыванием

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

[...] Python проявляет подход позднего связывания к лямбда-выражениям и не имеет никакого прецедента для автоматического, раннего связывания. Чувствовалось, что представление новой парадигмы излишне представит сложность.

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

, Который означает это только [1 156] оценивают наиболее удаленное for при создании выражения генератора. Таким образом, это на самом деле связывает значение с именем array в "подвыражении" in array (на самом деле, это связывает эквивалент [1 110] в этой точке). Но когда Вы выполняете итерации по генератору эти if array.count, вызов на самом деле относится к тому, что в настоящее время называют array.

<час>

, Так как это на самом деле list не array, я изменил имена переменной в остальной части ответа, чтобы быть более точным.

В Вашем первом случае list Вы выполняете итерации, и list Вы включаете, будет отличаться. Это - как будто Вы использовали:

list1 = [1, 2, 2, 4, 5]
list2 = [5, 6, 1, 2, 9]
f = (x for x in list1 if list2.count(x) == 2)

, Таким образом, Вы проверяете на каждый элемент в [1 117], если его количество в [1 118] равняется двум.

можно легко проверить это путем изменения второго списка:

>>> lst = [1, 2, 2]
>>> f = (x for x in lst if lst.count(x) == 2)
>>> lst = [1, 1, 2]
>>> list(f)
[1]

, Если бы это выполнило итерации по первому списку и рассчитало в первом списке, это возвратилось бы [2, 2] (потому что первый список содержит два 2). Если бы это выполнило итерации и рассчитало во втором списке, то вывод должен быть [1, 1]. Но так как это выполняет итерации по первому списку (содержащий один 1), но проверяет второй список (который содержит два 1 с, вывод является просто синглом 1.

Решение с помощью функции генератора

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

def keep_only_duplicated_items(lst):
    for item in lst:
        if lst.count(item) == 2:
            yield item

И затем используют его как это:

lst = [1, 2, 2, 4, 5]
f = keep_only_duplicated_items(lst)
lst = [5, 6, 1, 2, 9]

>>> list(f)
[2, 2]

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

А лучшее решение с помощью генератора функционирует со Счетчиком

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

from collections import Counter

def keep_only_duplicated_items(lst):
    cnts = Counter(lst)
    for item in lst:
        if cnts[item] == 2:
            yield item

Приложение: Используя подкласс для "визуализирования", что происходит и когда это происходит

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

В этом случае я просто переопределяю методы __iter__ и count, потому что мне интересно, по которому списку выражение генератора выполняет итерации и в котором списке это рассчитывает. Тела метода на самом деле просто делегируют к суперклассу и печатают что-то (так как он использует super без аргументов и струн фа, он требует Python 3.6, но должно быть легко адаптироваться к другим версиям Python):

class MyList(list):
    def __iter__(self):
        print(f'__iter__() called on {self!r}')
        return super().__iter__()

    def count(self, item):
        cnt = super().count(item)
        print(f'count({item!r}) called on {self!r}, result: {cnt}')
        return cnt

Это - простой подкласс, просто печатающий, когда __iter__ и count метод называют:

>>> lst = MyList([1, 2, 2, 4, 5])

>>> f = (x for x in lst if lst.count(x) == 2)
__iter__() called on [1, 2, 2, 4, 5]

>>> lst = MyList([5, 6, 1, 2, 9])

>>> print(list(f))
count(1) called on [5, 6, 1, 2, 9], result: 1
count(2) called on [5, 6, 1, 2, 9], result: 1
count(2) called on [5, 6, 1, 2, 9], result: 1
count(4) called on [5, 6, 1, 2, 9], result: 0
count(5) called on [5, 6, 1, 2, 9], result: 1
[]
59
ответ дан 1 November 2019 в 16:27

Как другие упомянули , генераторы Python ленивы. Когда эта строка выполняется:

f = (x for x in array if array.count(x) == 2) # Filters original

ничего на самом деле еще не происходит. Вы только что объявили, как функция генератора f будет работать. На массив еще не смотрят. Затем Вы создаете новый массив, который заменяет первый, и наконец когда Вы звоните

print(list(f)) # Outputs filtered

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

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

f = [x for x in array if array.count(x) == 2] # Filters original
...
print(f)
18
ответ дан 1 November 2019 в 16:27

Другие уже объяснили первопричину проблемы - генератор связывает с названием array локальная переменная, а не ее значение.

большей частью pythonic решения является определенно понимание списка:

f = [x for x in array if array.count(x) == 2]
<час>

Однако , если существует некоторая причина, что Вы не хотите создавать список, Вы можете также , вызывают объем близко более чем array:

f = (lambda array=array: (x for x in array if array.count(x) == 2))()

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

Примечание, которое это все еще связывает с переменная (ссылка), не значение , таким образом, например, следующее распечатает [2, 2, 4, 4]:

array = [1, 2, 2, 4, 5] # Original array

f = (lambda array=array: (x for x in array if array.count(x) == 2))() # Close over array
array.append(4)  # This *will* be captured

array = [5, 6, 1, 2, 9] # Updates original to something else

print(list(f)) # Outputs [2, 2, 4, 4]
<час>

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

9
ответ дан 1 November 2019 в 16:27

Вы не используете генератор правильно, если это - основное использование этого кода. Используйте понимание списка вместо понимания генератора. Просто замените круглые скобки скобками. Это оценивает к списку, если Вы не знаете.

array = [1, 2, 2, 4, 5]
f = [x for x in array if array.count(x) == 2]
array = [5, 6, 1, 2, 9]

print(f)
#[2, 2]

Вы получаете этот ответ из-за природы генератора. Вы называете генератор, когда it't содержание оценит к []

7
ответ дан 1 November 2019 в 16:27

Генераторы ленивы, они не будут оценены, пока Вы не выполните итерации через них. В этом случае это в точке, которую Вы создаете list с генератором, как введено, в print.

5
ответ дан 1 November 2019 в 16:27

Первопричина проблемы состоит в том, что генераторы ленивы; переменные оценены каждый раз:

>>> l = [1, 2, 2, 4, 5, 5, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]

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

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

>>> l = [1, 2, 2, 4, 5]
>>> filtered = (x for x in l if l.count(x) == 2)
>>> l = [1, 2, 4, 4, 5, 6, 6]
>>> list(filtered)
[4]
>>> def f(original, new, count):
    current = original
    filtered = (x for x in current if current.count(x) == count)
    current = new
    return list(filtered)

>>> from dis import dis
>>> dis(f)
  2           0 LOAD_FAST                0 (original)
              3 STORE_DEREF              1 (current)

  3           6 LOAD_CLOSURE             0 (count)
              9 LOAD_CLOSURE             1 (current)
             12 BUILD_TUPLE              2
             15 LOAD_CONST               1 (<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>)
             18 LOAD_CONST               2 ('f.<locals>.<genexpr>')
             21 MAKE_CLOSURE             0
             24 LOAD_DEREF               1 (current)
             27 GET_ITER
             28 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             31 STORE_FAST               3 (filtered)

  4          34 LOAD_FAST                1 (new)
             37 STORE_DEREF              1 (current)

  5          40 LOAD_GLOBAL              0 (list)
             43 LOAD_FAST                3 (filtered)
             46 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             49 RETURN_VALUE
>>> f.__code__.co_varnames
('original', 'new', 'count', 'filtered')
>>> f.__code__.co_cellvars
('count', 'current')
>>> f.__code__.co_consts
(None, <code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>, 'f.<locals>.<genexpr>')
>>> f.__code__.co_consts[1]
<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>
>>> dis(f.__code__.co_consts[1])
  3           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                32 (to 38)
              6 STORE_FAST               1 (x)
              9 LOAD_DEREF               1 (current)  # This loads the current list every time, as opposed to loading a constant.
             12 LOAD_ATTR                0 (count)
             15 LOAD_FAST                1 (x)
             18 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             21 LOAD_DEREF               0 (count)
             24 COMPARE_OP               2 (==)
             27 POP_JUMP_IF_FALSE        3
             30 LOAD_FAST                1 (x)
             33 YIELD_VALUE
             34 POP_TOP
             35 JUMP_ABSOLUTE            3
        >>   38 LOAD_CONST               0 (None)
             41 RETURN_VALUE
>>> f.__code__.co_consts[1].co_consts
(None,)

Для повторения: список, который будет выполнен с помощью итераций, только загружается однажды. Любые закрытия в условии или выражение, однако, загружаются из объема включения каждое повторение. Они не хранятся в константе.

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

4
ответ дан 1 November 2019 в 16:27

Оценка генератора "ленива" - это не становится выполняемым, пока Вы не реализовываете его с надлежащей ссылкой. С Вашей строкой:

Взгляд снова на Ваш вывод с типом f: тот объект генератор , не последовательность. Это ожидает, чтобы использоваться, своего рода итератор.

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

<час>

Код, чтобы "заставить его работать"

, Который зависит от того, под чем Вы подразумеваете, "заставляют его работать". Если Вы хотите f быть фильтрованным списком, то используйте список, не генератор:

f = [x for x in array if array.count(x) == 2] # Filters original
2
ответ дан 1 November 2019 в 16:27

Генераторы ленивы , и Ваш недавно определенный array используется, когда Вы исчерпываете свой генератор после переопределения. Поэтому вывод корректен. Быстрое исправление должно использовать понимание списка путем замены круглых скобок () скобками [].

Хождение дальше к тому, как лучше записать, Ваша логика, считая значение в цикле имеет квадратичную сложность. Для алгоритма, который работает в линейное время, можно использовать collections.Counter для подсчета значений, и сохраняют копию исходного списка :

from collections import Counter

array = [1, 2, 2, 4, 5]   # original array
counts = Counter(array)   # count each value in array
old_array = array.copy()  # make copy
array = [5, 6, 1, 2, 9]   # updates array

# order relevant
res = [x for x in old_array if counts[x] >= 2]
print(res)
# [2, 2]

# order irrelevant
from itertools import chain
res = list(chain.from_iterable([x]*count for x, count in counts.items() if count >= 2))
print(res)
# [2, 2]

Уведомление вторая версия даже не требует old_array и полезна, если нет никакой потребности поддержать упорядочивание значений в Вашем исходном массиве.

2
ответ дан 1 November 2019 в 16:27

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

Похожие вопросы: