(lambda) 함수 클로저는 무엇을 캡처합니까?


249

최근에 나는 파이썬으로 놀기 시작했고 클로저가 작동하는 방식에 독특한 것을 발견했습니다. 다음 코드를 고려하십시오.

adders=[0,1,2,3]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

단일 입력을 사용하여 숫자로 추가 된 해당 입력을 반환하는 간단한 함수 배열을 작성합니다. 함수는 for반복자가에서 i실행되는 루프로 구성 0됩니다 3. 이러한 각 숫자에 대해 lambda함수를 작성 i하여 함수의 입력에 추가 하는 함수가 작성 됩니다. 마지막 줄 은 매개 변수로 두 번째 lambda함수를 호출합니다 3. 놀랍게도 출력은이었습니다 6.

나는을 기대했다 4. 내 추론은 : 파이썬에서는 모든 것이 객체이므로 모든 변수는 그것에 대한 포인터입니다. 에 대한 lambda클로저를 만들 때 i현재 가리키는 정수 객체에 대한 포인터를 저장할 것으로 예상했습니다 i. 이는 i새로운 정수 객체 가 할당 될 때 이전에 생성 된 클로저에 영향을 미치지 않아야 함을 의미합니다 . 안타깝게도 adders디버거 내 에서 배열을 검사하면 배열이 작동하는 것으로 나타납니다. 모든 lambda함수는 i, 의 마지막 값을 참조하여 3결과를 adders[1](3)반환 6합니다.

다음에 대해 궁금해합니다.

  • 클로저는 정확히 무엇을 포착합니까?
  • lambda값을 변경할 i때 영향을받지 않는 방식으로 현재 값을 캡처 하도록 함수 를 설득하는 가장 우아한 방법은 무엇입니까 i?

35
UI 코드 에서이 문제가 발생했습니다. 날 미치게 했어 요령은 루프가 새로운 범위를 생성하지 않는다는 것을 기억하는 것입니다.
detly

3
@TimMB i네임 스페이스 는 어떻게 남겨 집니까 ?
detly

3
@detly 글쎄 print i, 루프 후에는 작동하지 않을 것이라고 말했습니다 . 그러나 나는 그것을 직접 테스트했으며 이제는 당신이 무엇을 의미하는지 알았습니다. 파이썬에서 루프 변수 뒤에 루프 변수가 남아 있다는 것을 몰랐습니다.
Tim MB

1
@TimMB-그래, 그게 내 뜻이야 에 대한 동일 if, with, try
detly

13
이것은 공식 Python FAQ에 있습니다. 왜 다른 값으로 루프에 정의 된 람다는 모두 동일한 결과를 반환합니까? , 설명과 일반적인 해결 방법이 모두 있습니다.
abarnert

답변:


161

두 번째 질문에 대한 답변이 있지만 첫 번째 질문은 다음과 같습니다.

클로저는 정확히 무엇을 포착합니까?

파이썬에서 범위는 동적이고 어휘입니다. 클로저는 변수가 가리키는 객체가 아닌 변수의 이름과 범위를 항상 기억합니다. 예제의 모든 함수는 동일한 범위에서 작성되고 동일한 변수 이름을 사용하므로 항상 동일한 변수를 참조합니다.

편집 : 이것을 극복하는 방법에 대한 다른 질문과 관련하여 두 가지 방법이 있습니다.

  1. Adrien Plisson이 권장 하는 방법은 가장 간결하지만 엄격하게 동등한 방법은 아닙니다 . 추가 인수로 람다를 만들고 추가 인수의 기본값을 유지하려는 객체로 설정하십시오.

  2. 람다를 생성 할 때마다 조금 더 장황하지만 덜 해킹되는 것은 새로운 범위를 만드는 것입니다.

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5

    여기서 범위는 인수를 바인딩하고 인수로 바인딩하려는 값을 전달하는 새 함수 (간결함을위한 람다)를 사용하여 작성됩니다. 그러나 실제 코드에서는 새로운 범위를 만들기 위해 람다 대신 일반 함수를 사용합니다.

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]

1
Max, 다른 질문에 대한 답변을 추가하면 더 간단한 답변으로 표시 할 수 있습니다. 고마워!
보아스

3
파이썬에는 동적 범위가 아닌 정적 범위가 있습니다. 모든 변수가 참조 일 뿐이므로 변수를 새 객체로 설정하면 변수 자체 (참조)의 위치는 동일하지만 다른 것을 가리 킵니다. Scheme에서도 같은 일이 발생합니다 set!. 동적 범위가 실제로 무엇인지 보려면 voidspace.org.uk/python/articles/code_blocks.shtml을 참조하십시오 .
Claudiu

6
옵션 2는 "Curried function"이라고 부르는 기능 언어와 유사합니다.
Crashworks

205

기본값을 가진 인수를 사용하여 변수를 강제로 캡처 할 수 있습니다.

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

아이디어는 매개 변수를 선언하고 (명명하게 i) 캡처하려는 변수의 기본값 (의 값 i)을 제공하는 것입니다.


7
기본값을 사용하는 경우 +1 람다가 정의 될 때 평가되면 이러한 용도에 적합합니다.
quornian

21
+1도 공식 FAQ에서 승인 한 솔루션이기 때문 입니다.
abarnert

23
이것은 놀랍다. 그러나 기본 파이썬 동작은 그렇지 않습니다.
세실 카레

1
이것은 좋은 해결책처럼 보이지는 않습니다 ... 실제로 변수의 사본을 캡처하기 위해 함수 서명을 변경하고 있습니다. 또한 함수를 호출하는 사람들은 i 변수를 망칠 수 있습니다.
David Callanan

@DavidCallanan 우리는 람다에 대해 이야기하고 있습니다. 전체 SDK를 통해 공유하는 것이 아니라 구멍을 뚫기 위해 일반적으로 자신의 코드에서 정의하는 임시 함수 유형입니다. 더 강력한 서명이 필요한 경우 실제 기능을 사용해야합니다.
Adrien Plisson

33

완전성을 위해 두 번째 질문에 대한 또 다른 대답 : functools 모듈 에서 partial 을 사용할 수 있습니다 .

Chris Lutz가 제안한대로 연산자에서 add를 가져 오면 예제는 다음과 같습니다.

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
   # store callable object with first argument given as (current) i
   adders[i] = partial(add, i) 

print adders[1](3)

24

다음 코드를 고려하십시오.

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

나는 대부분의 사람들이 혼란스럽지 않다고 생각합니다. 예상되는 동작입니다.

그렇다면 사람들은 왜 루프에서 수행 될 때 그것이 달라질 것이라고 생각합니까? 나는 그 실수를 저질렀다는 것을 알고 있지만 이유를 모르겠습니다. 루프입니까? 아니면 람다?

결국 루프는 짧은 버전입니다.

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a

11
많은 다른 언어에서 루프는 새로운 범위를 만들 수 있기 때문에 루프입니다.
detly

1
이 답변은 왜 i각 람다 함수에 대해 동일한 변수에 액세스 하는지 설명하기 때문에 좋습니다 .
David Callanan

3

두 번째 질문에 대한 답으로 가장 우아한 방법은 배열 대신 두 개의 매개 변수를 취하는 함수를 사용하는 것입니다.

add = lambda a, b: a + b
add(1, 3)

그러나 람다를 사용하는 것은 약간 바보입니다. 파이썬은 우리에게 operator기본 연산자들에게 기능적인 인터페이스를 제공하는 모듈을 제공합니다. 위의 람다는 추가 연산자를 호출하는 데 불필요한 오버 헤드가 있습니다.

from operator import add
add(1, 3)

나는 당신이 놀고, 언어를 탐구하려고 노력한다는 것을 이해하지만, 파이썬의 범위가 이상한 것을 방해하는 함수 배열을 사용하는 상황을 상상할 수 없습니다.

원한다면 배열 인덱싱 구문을 사용하는 작은 클래스를 작성할 수 있습니다.

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)

2
물론 위의 코드는 원래 문제와 관련이 없습니다. 그것은 내 요지를 간단한 방식으로 설명하기 위해 만들어졌습니다. 물론 무의미하고 바보입니다.
보아스

3

다음은 엔 클로징 컨텍스트가 "저장된"시점을 명확히하기 위해 클로저의 데이터 구조 및 컨텐츠를 강조 표시하는 새로운 예제입니다.

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

폐쇄는 무엇입니까?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

특히, my_str은 f1에서 닫히지 않았습니다.

f2의 폐쇄에는 무엇이 있습니까?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

메모리 주소에서 두 클로저에 동일한 객체가 포함되어 있습니다. 그래서, 당신은 할 수 있습니다 시작 범위에 대한 참조를 필요로 람다 함수의 생각. 그러나 my_str은 f_1 또는 f_2의 클로저에 없으며 i는 f_3 (미도시)의 클로저에 없습니다. 이는 클로저 객체 자체가 고유 한 객체임을 나타냅니다.

클로저 객체 자체는 동일한 객체입니까?

>>> print f_1.func_closure is f_2.func_closure
False

NB 결과 int object at [address X]>는 클로저가 [주소 X] AKA를 참조로 저장하고 있다고 생각했습니다. 그러나 람다 문 다음에 변수를 다시 할당하면 [주소 X]가 변경됩니다.
Jeff
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.