중첩 함수의 지역 변수


105

좋아요, 이것에 대해 참아주세요. 끔찍하게 복잡해 보일 것이라는 것을 압니다.하지만 무슨 일이 일어나고 있는지 이해하도록 도와주세요.

from functools import partial

class Cage(object):
    def __init__(self, animal):
        self.animal = animal

def gotimes(do_the_petting):
    do_the_petting()

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        cage = Cage(animal)

        def pet_function():
            print "Mary pets the " + cage.animal + "."

        yield (animal, partial(gotimes, pet_function))

funs = list(get_petters())

for name, f in funs:
    print name + ":", 
    f()

제공 :

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

그래서 기본적으로 왜 세 가지 다른 동물을 얻지 못합니까? cage중첩 함수의 로컬 범위에 '패키징' 되지 않습니까? 그렇지 않은 경우 중첩 함수에 대한 호출은 지역 변수를 어떻게 조회합니까?

이런 종류의 문제가 발생한다는 것은 일반적으로 '잘못하는 것'을 의미한다는 것을 알고 있지만 어떤 일이 발생하는지 이해하고 싶습니다.


1
시도해보십시오 for animal in ['cat', 'dog', 'cow']... 누군가가 와서 이것을 설명 할 것이라고 확신합니다-그것은 Python gotcha 중 하나입니다 :)
Jon Clements

답변:


114

중첩 함수는 정의 될 때가 아니라 실행될 때 상위 범위에서 변수를 찾습니다.

함수 본문이 컴파일되고 '자유'변수 (할당에 의해 함수 자체에 정의되지 않음)가 확인 된 다음 코드가 각 셀을 참조하는 인덱스를 사용하여 함수에 클로저 셀로 바인딩됩니다. pet_function따라서 하나의 자유 변수 ( cage)가 있으며,이 변수 는 클로저 셀, 인덱스 0을 통해 참조됩니다. 클로저 자체 cageget_petters함수 의 지역 변수 를 가리 킵니다 .

실제로 함수를 호출 할 때, 그 폐쇄는 다음의 값을보고하는 데 사용되는 cage주변 범위에서 함수를 호출 할 때 . 여기에 문제가 있습니다. 함수를 호출 할 때까지 get_petters함수는 이미 결과 계산을 완료했습니다. cage이 실행되는 동안 어떤 시점에서 로컬 변수가 각각 할당 된 'cow', 'dog'그리고 'cat'문자열을하지만 함수의 끝에서, cage마지막 값을 포함한다 'cat'. 따라서 동적으로 반환되는 각 함수를 호출하면 값이 'cat'인쇄됩니다.

해결 방법은 클로저에 의존하지 않는 것입니다. 당신은 사용할 수있는 일부 기능을 대신 만드는 새로운 기능 범위를 A와 변수를, 또는 바인드 키워드 매개 변수에 대한 디폴트 값 .

  • 다음을 사용하는 부분 함수 예제 functools.partial():

    from functools import partial
    
    def pet_function(cage=None):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, partial(pet_function, cage=cage)))
  • 새 범위 예제 만들기 :

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
  • 변수를 키워드 매개 변수의 기본값으로 바인딩 :

    def pet_function(cage=cage):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, pet_function))

scoped_cage루프 에서 함수 를 정의 할 필요가 없습니다 . 컴파일은 루프의 각 반복이 아니라 한 번만 발생합니다.


1
나는 오늘 3 시간 동안 작업 대본에이 벽에 머리를 부딪혔다. 마지막 요점은 매우 중요하며이 문제가 발생한 주된 이유입니다. 내 코드 전체에 걸쳐 클로저가있는 콜백이 있지만 루프에서 동일한 기술을 시도하는 것이 저를 얻었습니다.
DrEsperanto 2018 년

12

내 이해는 산출 된 pet_function이 실제로 호출 될 때 부모 함수 네임 스페이스에서 케이지가 이전이 아니라 호출된다는 것입니다.

그래서 당신이 할 때

funs = list(get_petters())

마지막으로 생성 된 케이지를 찾는 3 개의 함수를 생성합니다.

마지막 루프를 다음으로 바꾸면 :

for name, f in get_petters():
    print name + ":", 
    f()

당신은 실제로 얻을 것입니다 :

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

6

이것은 다음에서 비롯됩니다.

for i in range(2): 
    pass

print(i)  # prints 1

반복 후의 값은 i최종 값으로 느리게 저장됩니다.

제너레이터로서 함수는 작동하지만 (즉, 각 값을 차례로 인쇄) 목록으로 변환 할 때 제너레이터를 통해 실행 되므로 cage( cage.animal)에 대한 모든 호출은 cat을 반환합니다.


0

질문을 단순화합시다. 밝히다:

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        def pet_function():
            return "Mary pets the " + animal + "."

        yield (animal, pet_function)

그런 다음 질문에서와 같이 다음을 얻습니다.

>>> for name, f in list(get_petters()):
...     print(name + ":", f())

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

그러나 list()첫 번째 생성을 피한다면 :

>>> for name, f in get_petters():
...     print(name + ":", f())

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

무슨 일이야? 이 미묘한 차이가 결과를 완전히 바꾸는 이유는 무엇입니까?


를 살펴보면 list(get_petters())변경되는 메모리 주소에서 우리가 실제로 수행하는 세 가지 다른 기능이 있음이 분명합니다.

>>> list(get_petters())

[('cow', <function get_petters.<locals>.pet_function at 0x7ff2b988d790>),
 ('dog', <function get_petters.<locals>.pet_function at 0x7ff2c18f51f0>),
 ('cat', <function get_petters.<locals>.pet_function at 0x7ff2c14a9f70>)]

그러나 cell이러한 함수가 바인딩되는에 대해 살펴보십시오 .

>>> for _, f in list(get_petters()):
...     print(f(), f.__closure__)

Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)

>>> for _, f in get_petters():
...     print(f(), f.__closure__)

Mary pets the cow. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a95670>,)
Mary pets the dog. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a952f0>,)
Mary pets the cat. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c3f437f0>,)

두 루프 모두 cell객체는 반복 동안 동일하게 유지됩니다. 그러나 예상대로 str참조 하는 특정 사항 은 두 번째 루프에서 다릅니다. cell객체를 지칭 animal할 때 생성되는, get_petters()호출된다. 그러나 생성기 함수가 실행될 때 참조 animal하는 str객체를 변경 합니다 .

첫 번째 루프에서 각 반복 동안 모든 fs를 생성 하지만 생성기 get_petters()가 완전히 고갈되고 list함수가 이미 생성 된 후에 만 호출합니다 .

두 번째 루프에서 각 반복 동안 get_petters()생성기를 일시 중지하고 f각 일시 중지 후에 호출 합니다. 따라서 animal생성기 기능이 일시 중지 된 시점에 의 값을 검색하게 됩니다.

@Claudiu가 비슷한 질문 에 대한 답변을 넣었 듯이 :

세 개의 개별 함수가 생성되지만 각각 정의 된 환경 (이 경우에는 전역 환경 (또는 루프가 다른 함수 내부에 배치 된 경우 외부 함수의 환경))이 종료됩니다. 하지만 이것은 정확히 문제입니다.이 환경에서는 animal변이되고, 클로저는 모두 같은 것을 참조합니다 animal.

[편집자 주 : i가로 변경되었습니다 animal.]

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.