Go의 defer

A Tour of Go를 통해 처음 Go언어를 배우면서 재밌었던 부분은 defer였다. defer [statement] 형태로 사용하며, 여기에 정의해 둔 statement는 함수의 call stack이 사라지는 시점에(쉽게 말하면, 함수가 종료되기 직전에) 평가된다. 아래 코드는 텍스트 파일을 열고, 이 파일을 닫는 함수 호출을 defer하고, 파일의 내용을 print한다.

package main

import "os"

func main() {
    f, err := os.Open("sample.txt")
    if err != nil {
        panic(err)
    }

    // main 마지막에 파일 close 실행
    defer f.Close()

    // 파일 읽기
    bytes := make([]byte, 1024)
    f.Read(bytes)
    println(len(bytes))
}

deferf.Close를 그냥 함수의 맨 마지막 라인에 둬도 상관 없지 않을까 싶겠지만, 이는 마치 예외 처리를 위해 보통 쓰이는 tryfinally절과 비슷한 역할을 한다. defer절 자체에 문제가 없다면, 그 아래에서 에러가 나더라도 이는 실행된다. defer는 위의 경우처럼 자원을 해제하는 코드의 실행을 안정적이게 만드는 데에 유용하다.

Python의 defer

Python에서도 defer를 사용할 수 있다. 가장 유사한 것은 contextlib.ExitStack이라고 생각하는데, 여기까지의 흐름을 부드럽게 하기 위해 with문의 구조를 이용해 defer를 표현해 보기도 할 것이다.

유사한 것 : with문

with문은 해당 블럭에 들어갈 때 전달된 객체의 __enter__()를 호출하며 해당 함수의 리턴을 as [name] 부분에 명시된 이름에 바인딩하고, 블럭이 종료될 때 종료의 사유가 어떻든 __exit__()을 호출한다. 이러한 magic method들이 정의되어 있어 with문이 정상적으로 흐름을 관리할 수 있는 객체를 파이썬 세계에서 context manager라고 부른다. 무의미하지만 간단한 코드를 통해, context manager를 대충 살펴보자.

class ContextManager:
  def __enter__(self):
    print('enter')

  def __exit__(self, e_type, e_value, tb):
    print('exit')

with ContextManager():
  print('hi')
  raise Exception()

위 코드를 실행하면, 'enter', 'hi', 'exit' 순서로 출력된다. 만약 ContextManager 클래스가 인스턴스 변수로 콜백들을 관리하고, __exit__()에서 실행하게 한다면 대충 defer같은 구조를 만들어볼 수 있다.

class ContextManager:
  def __init__(self):
    self.callbacks = list()

  def __enter__(self):
    return self

  def __exit__(self, e_type, e_value, tb):
    print('exit called')

    for callback in self.callbacks:
      callback()

  def callback(self, callable):
    self.callbacks.append(callable)

with ContextManager() as c:
  c.callback(lambda: print(1))
  c.callback(lambda: print(2))

  print('before exit')

'before exit', 'exit called', 1, 2가 순서대로 출력된다.

contextlib.ExitStack

contextlib.ExitStack은 context manager이며, 위에서 시도해 본 callback같은 구조를 자체적으로, 그리고 안정적으로 구현해 두었다. 이런저런 기능들이 많지만, 여기서는 callback에 관한 예제만 첨부한다.

from contextlib import ExitStack
from functools import partial

with ExitStack() as stack:
    for i in range(10):
        stack.callback(partial(print, i))
    print('done')

'done', 9, 8, 7, 6, 5, 4, 3, 2, 1 순서로 출력이 진행된다.