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

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

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?


답변

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

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

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

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

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

답변

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

>>> 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)을 제공하는 것입니다.


답변

완전성을 위해 두 번째 질문에 대한 또 다른 대답 : 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)


답변

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

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


답변

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

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)


답변

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

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


답변