루프에서 함수 만들기


102

루프 내부에 함수를 만들려고합니다.

functions = []

for i in range(3):
    def f():
        return i

    # alternatively: f = lambda: i

    functions.append(f)

문제는 모든 기능이 동일하게된다는 것입니다. 0, 1, 2를 반환하는 대신 세 함수 모두 2를 반환합니다.

print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output:   [2, 2, 2]

왜 이런 일이 발생하고 각각 0, 1, 2를 출력하는 3 개의 다른 함수를 얻으려면 어떻게해야합니까?


답변:


167

지연 바인딩에 문제가 있습니다. 각 함수는 i가능한 한 늦게 조회됩니다 (따라서 루프가 끝난 후 호출 i되면로 설정됩니다 2).

초기 바인딩을 강제하여 쉽게 수정 : 다음 def f():def f(i=i):같이 변경 합니다 .

def f(i=i):
    return i

기본값 (오른쪽 i에서 i=i인수 이름에 대한 기본값입니다 i왼쪽이다, i에서가 i=i)에서 조회됩니다 def하지에 시간 call시간, 그래서 본질적으로 그들은 특히 초기 바인딩을 찾고있는 방법이야.

f추가 인수 를 받는 것이 걱정된다면 (따라서 잠재적으로 잘못 호출 될 수 있음) 클로저를 "함수 팩토리"로 사용하는 더 정교한 방법이 있습니다.

def make_f(i):
    def f():
        return i
    return f

루프 f = make_f(i)에서 def문 대신 사용하십시오 .


7
이 문제를 해결하는 방법을 어떻게 압니까?
alwbtc

3
@alwbtc 대부분은 경험 일 뿐이며, 대부분의 사람들은 어느 시점에서 이러한 일을 스스로 직면했습니다.
ruohola 2019 년

왜 작동하는지 설명해 주시겠습니까? (당신은 루프에서 생성 된 콜백에 저를 저장합니다. 인수는 항상 루프의 마지막 부분 이었으므로 감사합니다!)
Vincent Bénet

20

설명

여기서 문제 i는 함수 f가 생성 될 때의 값이 저장되지 않는다는 것입니다. 오히려, f의 값 조회 i가 될 때 호출을 .

생각해 보면이 동작이 완벽하게 이해됩니다. 사실 함수가 작동 할 수있는 유일한 합리적인 방법입니다. 다음과 같이 전역 변수에 액세스하는 함수가 있다고 가정 해보십시오.

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

이 코드를 읽으면 global_var함수가 선언 된 후의 값 이 변경 되었기 때문에 "foo"가 아닌 "bar"가 인쇄 될 것으로 예상 할 수 있습니다 . 자체 코드에서도 똑같은 일이 발생합니다.를 호출 할 때 f의 값 i이 변경되고로 설정되었습니다 2.

해결책

실제로이 문제를 해결하는 방법에는 여러 가지가 있습니다. 다음은 몇 가지 옵션입니다.

  • i기본 인수로 사용하여 초기 바인딩 강제

    클로저 변수 (예 :)와 달리 i기본 인수는 함수가 정의 될 때 즉시 평가됩니다.

    for i in range(3):
        def f(i=i):  # <- right here is the important bit
            return i
    
        functions.append(f)

    이것이 작동하는 방법 / 이유에 대한 약간의 통찰력을 제공하기 위해 : 함수의 기본 인수는 함수의 속성으로 저장됩니다. 따라서의 현재i이 스냅 샷되고 저장됩니다.

    >>> i = 0
    >>> def f(i=i):
    ...     pass
    >>> f.__defaults__  # this is where the current value of i is stored
    (0,)
    >>> # assigning a new value to i has no effect on the function's default arguments
    >>> i = 5
    >>> f.__defaults__
    (0,)
  • 함수 팩토리를 사용하여 i클로저 의 현재 값을 캡처합니다.

    문제의 근원은 i변할 수있는 변수라는 것입니다. 절대 변경되지 않는 다른 변수를 만들어이 문제를 해결할 수 있습니다 . 가장 쉬운 방법은 클로저입니다 .

    def f_factory(i):
        def f():
            return i  # i is now a *local* variable of f_factory and can't ever change
        return f
    
    for i in range(3):           
        f = f_factory(i)
        functions.append(f)
  • functools.partial의 현재 값을에 바인딩하는 데 사용 i합니다.f

    functools.partial기존 함수에 인수를 연결할 수 있습니다. 어떤면에서 그것도 일종의 함수 팩토리입니다.

    import functools
    
    def f(i):
        return i
    
    for i in range(3):    
        f_with_i = functools.partial(f, i)  # important: use a different variable than "f"
        functions.append(f_with_i)

주의 사항 : 이러한 솔루션 은 변수에 새 값을 할당 하는 경우에만 작동 합니다. 변수에 저장된 객체 를 수정 하면 동일한 문제가 다시 발생합니다.

>>> i = []  # instead of an int, i is now a *mutable* object
>>> def f(i=i):
...     print('i =', i)
...
>>> i.append(5)  # instead of *assigning* a new value to i, we're *mutating* it
>>> f()
i = [5]

i기본 인수로 변경했지만 여전히 어떻게 변경 되었는지 확인하십시오 ! 코드가으로 변경 i 되면 다음 과 같이 의 복사본i함수에 바인딩해야합니다 .

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.