[Python] Closure

1 minute read

python-version-3.7.1

Closure

자신 영역 밖에서 호출된 함수의 변수 및 레퍼런스를 따로 세이브해둔 뒤, 접근할 수 있도록 해준다.
(=Free variable(자유변수)를 맵핑해준다.)

Free variable (자유변수) : 해당 블럭안에서 정의되지 않았지만, 사용한 변수


Form

  • 중첩 함수를 가진다.

  • 중첩 함수는 자신의 부모함수의 변수를 참조하고 있다. (Free variable)

  • 부모함수는 중첩함수(자식함수)를 반환한다.



Example

def f():  # 부모함수
    k = 777
    def nested():  # 자식함수(중첩함수)
        print(k)
        
    return nested()

f()  # 함수 f 실행

>>> 777


Code Analysis

  1. f()에 의해 함수f가 실행된다.

  2. 지역변수 k = 777 가 정의된다.

  3. nested 함수를 호출과 동시에 반환시킨다. (return)

  4. 이때 호출된 nested는 함수 f(부모함수) 내부에서 정의된 중첩함수(자식함수)이며, k를 출력시킨다.


여기서 4번째 단계가 뭔가 의문이 생긴다. nested 함수는 함수f 내부에서 정의되었다.
지역변수k에 nested 함수에서 접근이 가능할까? 결론적으로는 가능하다.(제한적으로)
nested함수에서 변수k는 자유변수이다. k가 자유변수로 인지됨에 따라 부모함수의 k값은 따로 저장되어지고,
nested함수에서 k가 호출되면 메모리 어딘가에 따로 저장되어있던 k의 값에 접근하는 것이다.
이 과정을 closure 라고 할 수 있다.



Notice

위에서 지역변수k에 nested함수가 접근이 제한적으로 가능하다고 한 이유를 다루어보겠다.

def f():
    k = 777
    def nested():
        k = k + 3  # 자유변수 k에 새로운 값 할당
        print(k)

    return nested  # 위 코드와는 다르게 () 제거하였다.

f()

>>> UnboundLocalError: local variable 'k' referenced before assignment

에러가 발생한다.
위와 다른점은 중첩함수 nested함수에서 자유변수 k새로운 값을 할당하였다는 것이다.
이제 그 원인에 대해서 설명하겠다.

파이썬은 중첩함수에서 변수에 어떤값을 할당하려는 시도를 하면, 그 변수가 지역변수가 된다.
kk + 3 이라는 식을 할당하려고 시도하였기 때문에 k는 자유변수가 아닌, 지역변수가 되어 k + 3을 계산하는 과정에서 k의 값을 찾지못하는 것이다.



nonlocal

위 문제를 해결하는 방법은 nonlocal을 사용하는 것이다.

def f():
    k = 777
    def nested():
        nonlocal k
        k = k + 3
        print(k)

    return nested  # 위 코드와는 다르게 () 제거하였다.

f()

>>> <function __main__.f.<locals>.nested>
에러가 발생하지 않았다.

a = f()

a()

>>> 780

a()

>>> 783

a()

>>> 786

문제없이 작동함을 알 수 있다.



Practice

def character(class_, hp):
    def damaged(attack_damage):
        nonlocal hp
        hp -= attack_damage
        return class_, hp
    return damaged
first_ch = character("Warrior", 500)
second_ch = character("Wizard", 150)

Warrior는 500의 체력을, Wizard는 150의 체력을 가지도록 만들었다.

print(first_ch(30))  # Warrior 가 30의 데미지를 입었다.
print(second_ch(50))  # Wizard 가 50의 데미지를 입었다.

>>> ('Warrior', 470)
>>> ('Wizard', 100)

Leave a comment