Programing

조건부 생성기 표현식의 예상치 못한 동작

lottogame 2020. 12. 24. 23:21
반응형

조건부 생성기 표현식의 예상치 못한 동작


이 질문에 이미 답변이 있습니다.

프로그램의 한 부분에서 예기치 않게 논리 오류가 발생한 코드를 실행하고있었습니다. 섹션을 조사 할 때 실행중인 명령문 세트를 테스트하기위한 테스트 파일을 만들었고 매우 이상해 보이는 비정상적인 버그를 발견했습니다.

이 간단한 코드를 테스트했습니다.

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

결과는 다음과 같습니다.

>>> []

예, 없습니다. 필터 이해가 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]

어느 시점에서 나는 변수의 유형을 출력했습니다 f.

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에서 목록을 업데이트하면 다른 생성기 변수의 출력이 변경되는 이유는 무엇입니까? 이것은 나에게 매우 이상하게 보입니다.


Python의 생성기 표현식은 후기 바인딩입니다 ( PEP 289-Generator Expressions 참조 ) (다른 답변은 "lazy"라고 부르는 것).

초기 바인딩 대 후기 바인딩

많은 논의 끝에 [생성기 표현식의] 첫 번째 (가장 바깥 쪽) for-expression을 즉시 평가하고 나머지 표현식은 생성기가 실행될 때 평가되도록 결정했습니다.

[...] 파이썬은 람다 식에 대한 후기 바인딩 접근 방식을 취하며 자동 조기 바인딩에 대한 전례가 없습니다. 새로운 패러다임을 도입하면 불필요하게 복잡성이 도입 될 것이라고 생각했습니다.

많은 가능성을 탐색 한 후 바인딩 문제를 이해하기 어렵고 사용자가 인수를 즉시 사용하는 함수 내에서 생성기 표현식을 사용하도록 강력히 권장해야한다는 합의가 나타났습니다. 더 복잡한 응용 프로그램의 경우 전체 생성기 정의는 범위, 수명 및 바인딩에 대한 명확한 측면에서 항상 우수합니다.

, 생성기 표현식을 생성 할 때 가장 바깥 쪽 평가합니다 for. 따라서 실제로 는 "하위 표현식" 의 이름 으로 값을 바인딩 합니다 (사실 이 시점에서에 해당하는 것을 바인딩합니다 ). 그러나 생성기를 반복 할 때 호출은 실제로 현재 이름이 지정된 것을 참조합니다 .arrayin arrayiter(array)if array.countarray


실제로는 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)

따라서 list1카운트 인 list2이 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]

PEP (위 링크 참조)는 또한 더 복잡한 경우에는 전체 생성기 정의가 선호된다고 명시합니다.

카운터와 함께 생성기 함수를 사용하는 더 나은 솔루션

더 나은 솔루션 (배열의 각 요소에 대해 전체 배열을 반복하기 때문에 2 차 런타임 동작을 피하는 것 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인수와 f- 문자열없이 사용하기 때문에 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
[]

다른 사람들이 언급했듯이 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)

다른 사람들은 이미 문제의 근본 원인을 설명했습니다. 생성기는 array값이 아닌 로컬 변수 의 이름에 바인딩됩니다 .

가장 비단뱀적인 해결책은 확실히 목록 이해입니다.

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나중에 변수가 재정의 된 경우에도 생성자가 예상 한 변수를 볼 수 있도록하는 것입니다.

이것은 여전히 value가 아닌 변수 (참조)에 바인딩 되므로 예를 들어 다음과 같이 인쇄됩니다 .[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]

이것은 일부 언어에서 일반적인 패턴이지만 그다지 비단뱀 적이 지 않습니다. 따라서 목록 이해력을 사용하지 않는 아주 좋은 이유가있는 경우에만 의미가 있습니다 (예 : array매우 길거나 중첩 된 생성기 이해에서 사용되는 경우, 그리고 당신은 기억에 대해 걱정합니다).


이것이이 코드의 주요 용도 인 경우 생성기를 올바르게 사용하고 있지 않습니다. 생성기 이해력 대신 목록 이해력을 사용하십시오. 괄호를 대괄호로 바꾸십시오. 모르는 경우 목록으로 평가됩니다.

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]

생성기의 특성 때문에이 응답을 받고 있습니다. 내용이 평가되지 않을 때 생성기를 호출합니다.[]


생성기는 게으 르기 때문에 반복 할 때까지 평가되지 않습니다. 이 경우 list생성기를 입력으로 사용하여 print.


문제의 근본 원인은 발전기가 게으르다는 것입니다. 변수는 매번 평가됩니다.

>>> 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,)

To reiterate: The list to be iterated is only loaded once. Any closures in the condition or expression, however, are loaded from the enclosing scope each iteration. They are not stored in a constant.

The best solution for your problem would be to create a new variable referencing the original list and use that in your generator expression,.


Generator evaluation is "lazy" -- it doesn't get executed until you actualize it with a proper reference. With your line:

Look again at your output with the type of f: that object is a generator, not a sequence. It's waiting to be used, an iterator of sorts.

Your generator isn't evaluated until you start requiring values from it. At that point, it uses the available values at that point, not the point at which it was defined.


Code to "make it work"

That depends on what you mean by "make it work". If you want f to be a filtered list, then use a list, not a generator:

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

Generators are lazy and your newly defined array is used when you exhaust your generator after redefining. Therefore, the output is correct. A quick fix is to use a list comprehension by replacing parentheses () by brackets [].

Moving on to how better to write your logic, counting a value in a loop has quadratic complexity. For an algorithm that works in linear time, you can use collections.Counter to count values, and keep a copy of your original list:

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]

Notice the second version doesn't even require old_array and is useful if there is no need to maintain ordering of values in your original array.

ReferenceURL : https://stackoverflow.com/questions/54245618/unexpected-behaviour-with-a-conditional-generator-expression

반응형