Scope와 Closure

Jinhyeon Son·2020년 3월 28일
0

개념

목록 보기
7/26

Scope

scope는 변수의 생존 범위이다
파이썬 인터프리터는 각 scope의 순서에 따라서 변수를 탐색한다

파이썬에 존재하는 scope의 종류는 다음과 같다
  • local scope
  • nonlocal scope (enclosed scope)
  • global scope (module scope)
  • builtin scope
    z = 3

    def outer(x):
        y = 10
        def inner():
            x = 1000
            return x

        return inner()

    print(outer(10))

위 함수에서 각각의 스코프에 해당하는 변수는

  • local scope = outer(x)의 y, inner()의 x
  • nonlocal scope = inner()에서 본 y
  • global scope = outer(x)와 inner()에서 본 z 이다
  • builtin scope = 파이썬 내장함수 들이 위치하는 scope

각 스코프는 상위 스코프에 대해서 참조가 가능하나 쓰기는 제한적이다

    def count(x):
        def increment():
            x += 1
            print(x)

        return increment()

    count(5)
# 실행 결과  : UnboundLocalError: local variable 'x' referenced before assignment

이 점을 해결하기 위해서는 다음과 같이 참조하고자 하는 변수가 속한 스코프를 명시하면 된다

    def count(x):
        def increment():
            nonlocal x
            x += 1
            print(x)

        return increment()

    count(5)
    # 6

Closure

정의

프로그래밍 언어에서의 클로저란 퍼스트클래스 함수를 지원하는 언어의 네임 바인딩 기술이다. 클로저는 어떤 함수를 함수 자신이 가지고 있는 환경과 함께 저장한 레코드이다. 또한 함수가 가진 프리변수(free variable)를 클로저가 만들어지는 당시의 값과 레퍼런스에 맵핑하여 주는 역할을 한다. 클로저는 일반 함수와는 다르게, 자신의 영역 밖에서 호출된 함수의 변수값과 레퍼런스를 복사하고 저장한 뒤, 이 캡처한 값들에 액세스할 수 있게 도와준다.

파이썬에서 클로저는 ‘자신을 둘러싼 스코프(네임스페이스)의 상태값을 기억하는 함수’이며 다음의 세가지 조건을 만족해야 한다.
1. 해당 함수는 어떤 함수 내의 중첩된 함수여야 한다
2. 해당 함수는 자신을 둘러싼(enclose) 함수 내의 상태값을 반드시 참조해야 한다
3. 해당 함수를 둘러싼 함수는 이 함수를 반환해야 한다

동작

  def cache_test(func):
      cache = {}

      def wrapper(n):
          print(cache)
          if n in cache:
              return cache[n]
          else:
              cache[n] = func(n)
              print(f"cube of {n} = {func(n)}")
              return cache[n]

      return wrapper


  def cube(n):
      return n * 3


  cubing = cache_test(cube)

위 코드에서 wrapper(n)는

  1. cache_test의 중첩 함수이며
  2. nonlocal 객체 cache를 참조하며
  3. 부모 함수가 자신을 반환하고 있다

클로저이기 위한 조건을 모두 충족한다

이 때 클로저가 담겨있는 cubing을 함수로 호출하면 뭔가 특이한 동작을 하게 된다

  cubing(3)
  cubing(6)
  cubing(9)
  
  # 실행 결과
  {}
  cube of 3 = 9
  {3: 9}
  cube of 6 = 18
  {3: 9, 6: 18}
  cube of 9 = 27
  

실행결과에서 알 수 있듯이 cache는 cache_test의 local variable이지만
클로저가 실행됨에 따라 그 상태가 업데이트 되며 유지되고 있다

  cubing = cache_test(cube)
  cubing2 = cache_test(cube)


  cubing(3)
  cubing2(1)
  cubing(6)
  cubing2(2)
  cubing(9)
  cubing2(3)
  
  # 실행 결과
  {}
  cube of 3 = 9
  {}
  cube of 1 = 3
  {3: 9}
  cube of 6 = 18
  {1: 3}
  cube of 2 = 6
  {3: 9, 6: 18}
  cube of 9 = 27
  {1: 3, 2: 6}
  cube of 3 = 9

또한 위 코드를 통해 클로저는 자체 스코프를 갖고 있다는 것을 알 수 있고

이를 통해 클로저는 자신을 둘러싼 스코프의 상태를 기억하는 함수라는것을 확인 할 수 있다

특징

  • 클로저가 참조하는 부모 함수의 상태값은 부모 함수가 메모리에서 사라져도 값이 유지된다
  cubing = cache_test(cube)
  cubing2 = cache_test(cube)
  cubing(3)
  cubing2(1)
  del(cache_test)
  print("삭제 실행")
  cubing(6)
  cubing2(2)
  
  # 실행 결과
  {}
  cube of 3 = 9
  {}
  cube of 1 = 3
  삭제 실행
  {3: 9}
  cube of 6 = 18
  {1: 3}
  cube of 2 = 6
  • 멤버변수 __closure__ 를 통해 클로저가 참조하는 객체에 접근할 수 있다
  cubing = cache_test(cube)
  cubing(3)
  cubing(6)
  cubing(9)
  print(f"클로저가 참조하는 주소 : {cubing.__closure__}")
  print(f"클로저가 참조하는 상태값의 주소: {cubing.__closure__[0]}")
  print(f"클로저가 참조하는 상태값 : {cubing.__closure__[0].cell_contents}")
  
  # 실행 결과
  {}
  cube of 3 = 9
  {3: 9}
  cube of 6 = 18
  {3: 9, 6: 18}
  cube of 9 = 27
  클로저가 참조하는 주소 : (<cell at 0x7fb20ff36350: dict object at 0x7fb20fffc320>,
  <cell at 0x7fb20ff36590: function object at 0x7fb20ff2fb00>)
  클로저가 참조하는 상태값의 주소: <cell at 0x7fb20ff36350: dict object at 0x7fb20fffc320>
  클로저가 참조하는 상태값 : {3: 9, 6: 18, 9: 27}

목적

  • 관리와 책임을 명확히 할수 있다
  • 각 변수가 섞여 불필요한 충돌을 방지할 수 있다

다시 말하자면 함수를 generate하는 클로저의 특성상 만들어진 모든 함수가 전역 변수와 같은 변수를 참조할 수도 있지만
독립적인 scope가 필요할 경우가 있을 수 있고 이를 위해 함수 갯수 만큼의 변수를 선언하는것은 굉장히 비효율적이며
가독성 면에서도 좋지 않기 때문에 클로저마다 존재하는 고유의 scope를 이용한다

0개의 댓글