[python] 어휘 폐쇄는 어떻게 작동합니까?

Javascript 코드에서 어휘 폐쇄와 관련된 문제를 조사하는 동안 Python 에서이 문제가 발생했습니다.

flist = []

for i in xrange(3):
    def func(x): return x * i
    flist.append(func)

for f in flist:
    print f(2)

이 예제는주의해서 피한다 lambda. “4 4 4″를 인쇄하는데 이는 놀랍습니다. “0 2 4″를 기대합니다.

이 동등한 Perl 코드가 올바르게 수행합니다.

my @flist = ();

foreach my $i (0 .. 2)
{
    push(@flist, sub {$i * $_[0]});
}

foreach my $f (@flist)
{
    print $f->(2), "\n";
}

“0 2 4″가 인쇄됩니다.

차이점을 설명해 주시겠습니까?


최신 정보:

문제는 되지 않습니다 와 함께 i글로벌 인. 동일한 동작이 표시됩니다.

flist = []

def outer():
    for i in xrange(3):
        def inner(x): return x * i
        flist.append(inner)

outer()
#~ print i   # commented because it causes an error

for f in flist:
    print f(2)

주석 처리 된 행이 표시하는 것처럼 i해당 시점에서 알 수 없습니다. 여전히 “4 4 4″를 인쇄합니다.



답변

파이썬은 실제로 정의 된대로 동작합니다. 세 개의 개별 함수 가 작성되지만 각각 정의 된 환경 (이 경우 글로벌 환경 (또는 루프가 다른 함수 내에 배치 된 경우 외부 함수의 환경))이 닫힙니다. 이것은 정확히 문제이지만,이 환경에서는 i가 변이 되고 클로저는 모두 동일한 i를 참조합니다 .

함수 creater를 만들고 호출 – 여기에 내가 가지고 올 수있는 최고의 솔루션입니다 대신. 이렇게하면 생성 된 각 기능 에 대해 서로 다른 i 를 사용하여 서로 다른 환경 을 만들 수 있습니다.

flist = []

for i in xrange(3):
    def funcC(j):
        def func(x): return x * j
        return func
    flist.append(funcC(i))

for f in flist:
    print f(2)

부작용과 기능 프로그래밍을 혼합 할 때 발생하는 현상입니다.


답변

루프에 정의 된 함수 i는 값이 변경되는 동안 동일한 변수에 계속 액세스합니다 . 루프의 끝에서 모든 함수는 동일한 변수를 가리키며,이 변수는 루프의 마지막 값을 유지합니다. 효과는 예제에서보고 된 것입니다.

i값 을 평가 하고 사용 하기 위해 공통 패턴은이를 매개 변수 기본값으로 설정하는 것입니다. 매개 변수 기본값은 def명령문이 실행될 때 평가 되므로 루프 변수의 값이 고정됩니다.

다음은 예상대로 작동합니다.

flist = []

for i in xrange(3):
    def func(x, i=i): # the *value* of i is copied in func() environment
        return x * i
    flist.append(func)

for f in flist:
    print f(2)


답변

다음은 functools라이브러리를 사용하여 수행하는 방법입니다 (질문이 제기 될 당시 확실하지 않았습니다).

from functools import partial

flist = []

def func(i, x): return x * i

for i in xrange(3):
    flist.append(partial(func, i))

for f in flist:
    print f(2)

예상대로 0 2 4를 출력합니다.


답변

이거 봐요:

for f in flist:
    print f.func_closure


(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)

이는 모두 동일한 i 변수 인스턴스를 가리키며 루프가 끝나면 2의 값을 갖습니다.

읽을 수있는 솔루션 :

for i in xrange(3):
        def ffunc(i):
            def func(x): return x * i
            return func
        flist.append(ffunc(i))


답변

일어나고있는 일은 변수 i가 캡처되고 함수가 호출 될 때 바인딩 된 값을 반환한다는 것입니다. 기능적 언어에서는 이러한 상황이 결코 발생하지 않습니다. 그러나 파이썬과 함께 lisp에서 본 것처럼 이것은 더 이상 사실이 아닙니다.

스키마 예제와의 차이점은 do 루프의 의미와 관련이 있습니다. 체계는 다른 언어와 마찬가지로 기존 i 바인딩을 재사용하는 대신 루프를 통해 매번 새로운 i 변수를 효과적으로 작성합니다. 루프 외부에서 생성 된 다른 변수를 사용하고 변경하면 구성표에서 동일한 동작이 나타납니다. 루프를 다음과 같이 바꾸십시오.

(let ((ii 1)) (
  (do ((i 1 (+ 1 i)))
      ((>= i 4))
    (set! flist
      (cons (lambda (x) (* ii x)) flist))
    (set! ii i))
))

이에 대한 추가 논의는 여기 를 보십시오 .

[편집] 아마도 이것을 설명하는 더 좋은 방법은 do 루프를 다음 단계를 수행하는 매크로로 생각하는 것입니다.

  1. 루프의 본문으로 정의 된 본문을 사용하여 단일 매개 변수 (i)를 사용하는 람다를 정의합니다.
  2. 적절한 i 값을 매개 변수로 사용하여 해당 람다를 즉시 호출합니다.

즉. 아래 파이썬과 같습니다.

flist = []

def loop_body(i):      # extract body of the for loop to function
    def func(x): return x*i
    flist.append(func)

map(loop_body, xrange(3))  # for i in xrange(3): body

i는 더 이상 부모 범위의 것이 아니라 자체 범위의 새로운 변수 (예 : 람다에 대한 매개 변수)이므로 관찰 한 동작을 얻습니다. 파이썬에는이 암시적인 새로운 범위가 없으므로 for 루프의 본문은 i 변수를 공유합니다.


답변

나는 왜 아직도 어떤 언어에서는 이것이 어떤 식 으로든 어떤 식 으로든 다른 방식으로 작동하는지 완전히 확신하지 못합니다. Common Lisp에서는 Python과 같습니다.

(defvar *flist* '())

(dotimes (i 3 t)
  (setf *flist*
    (cons (lambda (x) (* x i)) *flist*)))

(dolist (f *flist*)
  (format t "~a~%" (funcall f 2)))

“6 6 6″을 인쇄합니다 (여기서 목록은 1-3이며 반대 방향으로 작성 됨). Scheme에서는 Perl에서와 같이 작동합니다.

(define flist '())

(do ((i 1 (+ 1 i)))
    ((>= i 4))
  (set! flist
    (cons (lambda (x) (* i x)) flist)))

(map
  (lambda (f)
    (printf "~a~%" (f 2)))
  flist)

“6 4 2″인쇄

그리고 이미 언급했듯이 Javascript는 Python / CL 캠프에 있습니다. 여기에 다른 언어가 다른 방식으로 접근하는 구현 결정이있는 것으로 보입니다. 나는 결정이 무엇인지 정확하게 이해하고 싶습니다.


답변

문제는 모든 로컬 함수가 동일한 환경과 동일한 i변수에 바인딩된다는 것입니다 . 해결책 (해결 방법)은 각 함수 (또는 람다)에 대해 별도의 환경 (스택 프레임)을 만드는 것입니다.

t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]

>>> t[1](2)
2
>>> t[2](2)
4