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


149

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"를 인쇄합니다.



3
이 문제에 대한 꽤 좋은 기사가 있습니다. me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu

답변:


151

파이썬은 실제로 정의 된대로 동작합니다. 세 개의 개별 함수 가 작성되지만 각각 정의 된 환경 (이 경우 글로벌 환경 (또는 루프가 다른 함수 내에 배치 된 경우 외부 함수의 환경))이 닫힙니다. 이것은 정확히 문제이지만,이 환경에서는 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)

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


5
귀하의 솔루션도 Javascript에서 사용되는 솔루션입니다.
Eli Bendersky

9
이것은 오작동이 아닙니다. 정의 된대로 정확하게 동작합니다.
Alex Coventry

6
IMO piro는 더 나은 솔루션을 가지고 있습니다 stackoverflow.com/questions/233673/…
jfs

2
명확성을 위해 가장 안쪽의 'i'를 'j'로 변경했을 것입니다.
eggsyntax

7
다음과 같이 정의하는 것은 def inner(x, i=i): return x * i
어떻습니까?

152

루프에 정의 된 함수 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)

7
s / 컴파일 타임 / def문이 실행될 때 /
jfs

23
이것은 독창적 인 솔루션이므로 끔찍합니다.
Stavros Korokithakis

이 솔루션에는 한 가지 문제점이 있습니다. func에는 이제 두 개의 매개 변수가 있습니다. 즉, 가변적 인 양의 매개 변수에서는 작동하지 않습니다. 더 나쁜 것은 두 번째 매개 변수로 func을 호출 i하면 정의 에서 원본 을 덮어 씁니다 . :-(
Pascal

34

다음은 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를 출력합니다.


나는 이것을 정말로 사용하고 싶었지만 내 함수는 실제로 클래스 메소드이며 전달 된 첫 번째 값은 self입니다. 어쨌든 주위에 있습니까?
마이클 데이비드 왓슨

1
물론. add (self, a, b) 메소드가있는 Math 클래스가 있고 'increment'메소드를 작성하기 위해 a = 1을 설정하려고한다고 가정하십시오. 그런 다음 클래스 'my_math'의 인스턴스를 만들고 증가 방법은 'increment = partial (my_math.add, 1)'입니다.
Luca Invernizzi

2
당신은 또한 사용할 수있는 방법에이 기술을 적용하려면 functools.partialmethod()파이썬 3.4로
매트 대한 수정 사항

13

이거 봐요:

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

1
내 질문은 더 "일반"입니다. 왜 파이썬에 결함이 있습니까? 나는 Perl과 Lisp 왕조와 같은 어휘 폐쇄를 지원하는 언어가 이것을 올바르게 작동시킬 것으로 기대합니다.
Eli Bendersky

2
결함이있는 이유를 묻는 것은 결함이 아니라고 가정하는 것입니다.
Null303

7

일어나고있는 일은 변수 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 변수를 공유합니다.


흥미 롭군 do 루프의 의미론의 차이점을 알지 못했습니다. 감사합니다
Eli Bendersky

4

나는 왜 아직도 어떤 언어에서는 이것이 어떤 식 으로든 어떤 식 으로든 다른 방식으로 작동하는지 완전히 확신하지 못합니다. 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 캠프에 있습니다. 여기에 다른 언어가 다른 방식으로 접근하는 구현 결정이있는 것으로 보입니다. 나는 결정이 무엇인지 정확하게 이해하고 싶습니다.


8
차이점은 범위 지정 규칙이 아닌 (do ...)에 있습니다. 구성표에서는 루프를 통과 할 때마다 새 변수를 작성하지만 다른 언어는 기존 바인딩을 재사용합니다. lisp / python과 비슷한 동작을 가진 구성표 버전의 예와 자세한 내용은 내 대답을 참조하십시오.
Brian

2

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

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

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

1

변수 i는 전역이며, 함수 f가 호출 될 때마다 값이 2 입니다.

다음과 같이 수행 한 동작을 구현하는 경향이 있습니다.

>>> class f:
...  def __init__(self, multiplier): self.multiplier = multiplier
...  def __call__(self, multiplicand): return self.multiplier*multiplicand
... 
>>> flist = [f(i) for i in range(3)]
>>> [g(2) for g in flist]
[0, 2, 4]

업데이트에 대한 응답 : 이 동작을 유발하는 것은 i 그 자체 의 세계 성이 아니며, f가 호출되는 시간에 걸쳐 고정 된 값을 갖는 둘러싸는 범위의 변수라는 사실입니다. 두 번째 예에서의 값은 함수 i의 범위에서 가져 오며 kkkon 함수를 호출 할 때 아무것도 변경되지 않습니다 flist.


0

이 동작의 추론은 이미 설명되었고 여러 솔루션이 게시되었지만 이것이 가장 파이썬 적이라고 생각합니다 (Python의 모든 것은 객체입니다!).

flist = []

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

for f in flist:
    print f(2)

Claudiu의 대답은 함수 발생기를 사용하면 꽤 좋지만, piro의 대답은 내가 기본값으로 "숨겨진"논리를 만들 때 정직하게 말하면 해킹입니다 (잘 작동하지만 "pythonic"은 아닙니다) .


나는 그것이 당신의 파이썬 버전에 달려 있다고 생각합니다. 이제 저는 더 경험이 많으며 더 이상 이런 방식으로 제안하지 않습니다. Claudiu는 Python에서 클로저를 만드는 적절한 방법입니다.
darkfeline

1
파이썬 2 나 3에서는 작동하지 않습니다 (둘 다 "4 4 4"출력). func에서는 x * func.i항상 정의의 마지막 함수를 참조한다. 따라서 각 함수에는 개별적으로 올바른 숫자가 붙어 있지만 결국 마지막 함수에서 읽습니다.
Lambda Fairy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.