ChangHyeon Nam's Blog notes and thoughts

__missing__을 사용해 키에 따라 다른 디폴트 값을 생성하는 방법을 알아두라

Comments

[Effective Python] Better way 18: __missing__ 을 사용해 키에 따라 다른 디폴트 값을 생성하는 방법을 알아두라

  • 디폴트 값을 만드는 계산 비용이 높거나 만드는 과정에서 예외가 발생할 수 있는 상황에서는 dict의 setdefault 메서드를 사용하지 말아라
  • defaultdict에 전달되는 함수는 인자를 받지 않는다. 따라서 접근에 사용한 키 값에 맞는 디폴트 값을 생성하는 것은 불가능하다.
  • 디폴트 키를 만들때 어떤 키를 사용했는지 반드시 알아야 하는 상황이라면 직접 dict의 하위클래스와 __missing__ 메서드를 정의하면 된다.

  • 앞선 챕터에서 setdefault 메서드와 defaultdict 타입에 대해 배웠습니다. 이번 챕터에서 두개의 방법 모두 사용하기 적절하지 않을 경우에 대해 사용하는 방법에 대해 배웁니다.
  • 다음 예제 코드는 파일 시스템에 있는 SNS 프로필 사진을 관리하는 프로그램입니다. 필요할 때, 파일을 읽고 쓰기 위해 프로필 사진의 경로와 열린 파일 핸들을 연관시켜주는 딕셔너리가 필요합니다. 먼저 get을 이용하여 구현한 코드입니다.

      pictures ={}
      path = 'profile_1234.png'
    
      if(handle :=pictures.get(path)) is None:
          try:
              handle = open(path,'a+b')
          except OSError:
              print(f'경로를 열 수 없습니다: {path}')
              raise
          else:
              pictures[path] = handle
      handle.seek(0)
      image_data = handle.read()
    

    파일 핸들이 이미 딕셔너리에 있으면 딕셔너리를 한번만 읽습니다. 파일핸들이 없으면 try/else 구문의 else에서 핸들을 딕셔너리에 대입합니다. read 메서드를 호출하는 코드와 open 호출하고 예외를 처리하는 코드가 잘 분리되어 있습니다.

    open(path,'a+b'): append + binary mode를 의미하고, handle.seek(0): cursor를 0으로 설정해주는 것을 의미합니다. (linux에서의 open, lseek과 매우 유사함)

  • 같은 코드를 setdefault로 구현해보겠습니다.

      try:
          handle = pictures.setdefault(path,open(path,'a+b'))
      except OSError:
          print(f'경로를 열 수 없습니다: {path}')
          raise
      else:
          handle.seek(0)
          image_data = handle.read()
    

    위의 코드는 문제가 많습니다. 파일 핸들을 만드는 내장함수인 open이 딕셔너리에 경로의 존재 유무에 상관없이 항상 호출됩니다. 이로 인해 프로그램상에 존재하던 열린 파일 핸들과 혼동될 수 있는 새로운 파일 핸들이 생길수도 있습니다.

  • 다음은 defulatdict 타입을 이용해 구현한 코드입니다.

      from collections import  defaultdict
      def open_picture(profile_path):
          try:
              return open(profile_path,'a+b')
          except OSError:
              print(f'경로를 열 수 없습니다: {path}')
              raise
      pictures = defaultdict(open_picture())
      handle = pictures[path]
      handle.seek(0)
      # TypeError: open_picture() missing 1 required
      # positional argument: 'profile_path'
    

    위 코드의 문제는 defaultdict 생성자에 전달한 함수는 인자를 받을 수 없습니다. 이로 인해 파일 경로를 사용해 open을 호출할 방법이 없습니다.

  • 그래서 __missing__ 특별 메서드를 구현하면 키가 없는 경우를 처리하는 로직을 커스텀화하여 구현할 수 있습니다.

      def open_picture(profile_path):
          try:
              return open(profile_path,'a+b')
          except OSError:
              print(f'경로를 열 수 없습니다: {path}')
              raise
    
      class Pictures(dict):
          def __missing__(self,key):
              value = open_picture(key)
              self[key] = value
              return  value
    
      pictures = Pictures()
      handle = pictures[path]
      handle.seek(0)
      image_data = handle.read()
    

    dict 타입의 하위 클래스를 따로 만들어, 특별 메서드 __missing__를 구현하면 됩니다. pictures[path] 라는 딕셔너리 접근에서 path가 딕셔너리에 없으면 __missing__ 메서드가 호출됩니다. 이 메서드는 키에 해당하는 디폴트 값을 생성해 딕셔너리에 넣어준 다음에 호출한 쪽에 그 값을 반환합니다.