Неожиданное поведение с условным выражением генератор

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

Я тестировал этот простой код:

array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
array = [5, 6, 1, 2, 9] # Updates original to something else

print(list(f)) # Outputs filtered

А выход был:

>>> []

Да, ничего. Я ожидал понимания Filter, чтобы найти элементы в массив со счетчиком 2 и выходной, но я не'т получить, что:

# Expected output
>>> [2, 2]

Когда я закомментировал третьей строке, чтобы проверить его еще раз:

array = [1, 2, 2, 4, 5] # Original array
f = (x for x in array if array.count(x) == 2) # Filters original
### array = [5, 6, 1, 2, 9] # Ignore line

print(list(f)) # Outputs filtered

Вывод был правильным (Вы можете проверить это сами):

>>> [2, 2]

В один момент я вывести тип переменной Ф:

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

print(type(f))
print(list(f)) # Outputs filtered

И я получил:

>>> <class 'generator'>
>>> []

Почему обновление списка в Python изменение выходной переменной генератор? Это кажется мне очень странным.

Комментарии к вопросу (3)
Решение

В Python'выражений генератор с позднего связывания (см. ОПТОСОЗ 289 -- выражения генератор) (что других ответов называют "ленивый") смотрите:

раннее связывание против позднего связывания

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

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

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

Это означает, что он только оценивает крайними " за " при создании генератора выражений. Так оно, собственно, *персонализация стоимость с именем массив в "выражения" в в массив (на самом деле она&#39;с обязательным эквивалентомИТЭР(массив)на данный момент). Но когда вы перебирать генераторесли в массиве.количество вызововна самом деле относится к тому, что в настоящее время называетсямассив`.


Поскольку она'ов на самом деле список не массив я изменил имена переменных в остальном ответ будет более точный.

В первом случае "список" вы перебрать и список считать будут разные. Это's, как если бы вы использовали:

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

Так что вы проверьте каждый элемент в "список 1", если ее рассчитывать в список2-это два.

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

>>> 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]

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

Лучшим решением, используя функциональный генератор с счетчиком

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

from collections import Counter

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

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

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

В этом случае я просто переопределить методИТЭРиcount` потому что я'м интересно за какой список выражений генератора проходит и в каком списке он рассчитывает. Метод тела на самом деле просто делегировать суперкласс и что-то напечатать (так как он использует "супер" без аргументов и F-строки, у меня 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

Это простой подкласс только печати, когда __ИТЭР__ и метод граф называются:

>>> 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
[]
Комментарии (10)

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

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

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

print(list(f)) # Outputs filtered

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

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

f = [x for x in array if array.count(x) == 2] # Filters original
...
print(f)
Комментарии (1)

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

Наиболее подходящие для Python решение, безусловно, понимание списка:

f = [x for x in array if array.count(x) == 2]

** Однако, если есть какая-то причина, что вы Don'т хотите, чтобы создать список, вы можете же силу охвата закрыть за массив:

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

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

Обратите внимание, что это все-таки привязывает к переменная (ссылка), а не значение**, так, например, следующий будет печатать[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]

Это обычная картина в некоторых языках, но это's не очень подходящие для Python, так что только действительно имеет смысл, если там's очень веские причины не использовать список понимание (например, если массив очень долго, или используется во вложенном генератора понимание, и вы'вновь обеспокоен памяти).

Комментарии (1)

Вы не используете генератор правильно, если это основное применение этого кодекса. Использовать список понимание вместо понимания генератора. Просто замените скобок со скобками. Он возвращает список, если вы не'т знаю.

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]

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

Комментарии (5)

Генераторы ленивы, они выиграли'т быть оценено, пока вы перебираете их. В этом случае, что's в момент создания список с генератором в качестве входных, в "печать".

Комментарии (6)

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

``

л = [1, 2, 2, 4, 5, 5, 5] процеживают = (X для X в L, если L.граф(х) == 2) л = [1, 2, 4, 4, 5, 6, 6] Список(фильтрованный) [4] ``

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

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

``

л = [1, 2, 2, 4, 5] процеживают = (X для X в L, если L.граф(х) == 2) л = [1, 2, 4, 4, 5, 6, 6] Список(фильтрованный) [4] деф Ф(оригинальный, новый, граф): ток = оригинал отфильтровано = (X для X в ток ток.граф(х) == посчитай) ток = новый список возврата(фильтрованный)

от Дис импорт Дис рас(Ф) 2 0 0 LOAD_FAST (оригинал) 3 STORE_DEREF 1 (текущие)

3 6 LOAD_CLOSURE 0 (число) 9 LOAD_CLOSURE 1 (текущие) 12 BUILD_TUPLE 2 15 LOAD_CONST 1 (<код объекта В 0x02DD36B0, файл "<pyshell#17>”, в строке 3>) 18 LOAD_CONST 2 ('е.<местные жители и GT;.') 21 MAKE_CLOSURE 0 24 LOAD_DEREF 1 (текущие) 27 GET_ITER 28 CALL_FUNCTION 1 (1 позиционная, 0 пара ключевых слов) 31 STORE_FAST 3 (фильтрованное)

4 34 LOAD_FAST 1 (новый) 37 STORE_DEREF 1 (текущие)

5 40 LOAD_GLOBAL 0 (список) 43 LOAD_FAST 3 (фильтрованное) 46 CALL_FUNCTION 1 (1 позиционная, 0 пара ключевых слов) 49 RETURN_VALUE

Ф.код.co_varnames ('оригинал', 'новый', 'граф', 'фильтруют') Ф.код.co_cellvars ('граф', 'настоящих') Ф.код.co_consts (Нет, <объектный код В 0x02DD36B0, файл "<pyshell#17>”, в строке 3>, 'е.<местные жители и GT;.') Ф.код.co_consts[1] <код объекта В 0x02DD36B0, файл "<pyshell#17>”, в строке 3> рас(Ф.код.co_consts[1]) 3 0 LOAD_FAST 0 (.0) 3 FOR_ITER 32 (до 38) 6 STORE_FAST 1 (х) 9 LOAD_DEREF 1 (текущая) # загружает текущий список в любое время, в отличие от нагрузки постоянной. 12 LOAD_ATTR 0 (число) 15 LOAD_FAST 1 (х) 18 CALL_FUNCTION 1 (1 позиционная, 0 пара ключевых слов) 21 LOAD_DEREF 0 (число) 24 COMPARE_OP 2 (==) 27 POP_JUMP_IF_FALSE 3 30 LOAD_FAST 1 (х) 33 YIELD_VALUE 34 POP_TOP 35 JUMP_ABSOLUTE 3 38 LOAD_CONST 0 (Нет) 41 RETURN_VALUE Ф.код.co_consts[1].co_consts (Нет,) ``

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

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

Комментарии (0)

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

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

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 и полезно, если нет необходимости поддерживать порядок следования значений в исходном массиве.

Комментарии (0)

Оценка генератор есть "ленивый" и ... это не'т получить выполняется, пока вы реализовать его с соответствующей ссылкой. С вашей линии:

Еще раз взгляните на свой выход с типом "е": что объект является генератор, а не последовательность. Это'ы ждут, чтобы быть использованы, итератор сортов.

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


Код "и заставить его работать на"

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

f = [x for x in array if array.count(x) == 2] # Filters original
Комментарии (1)