python

이펙티브 파이썬 2nd 정리 #2

기기디 2023. 8. 5. 00:05

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

✅  setdefault, defaultdict를 사용하기 적당하지 않은 경우 __missing__을 사용하라

  • 디폴트 값을 만드는 계산 비용이 높거나 만드는 과정에서 예외가 발생할 수 있는 상황에서는 dict의 setdefault 메서드를 사용하지 않는것을 권장한다.
  • defaultdict에 전달되는 함수는 인자를 받지 않는다. 따라서 접근에 사용한 키 값에 맞는 디폴트 값을 생성하는 것이 불가능하다.
  • 디폴트 키를 만들때 어떤 키를 사용했는지 반드시 알아야 하는 상황이라면 직접 dict의 하위클래스와 __missing__ 메서드를 정의하여 해결할 수 있다.
# setdefault, defaultdict를 사용하기 적당하지 않은 경우
# 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()
# setdefault를 사용한 경우
	...
	try:
      handle = pictures.setdefault(path,open(path,'a+b'))
  except OSError:
      print(f'경로를 열 수 없습니다: {path}')
      raise
  else:
      handle.seek(0)
      image_data = handle.read()
	...
# defaultdcit를 사용한 경우
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'
  • setdefault를 사용하는 경우의 문제는 파일 핸들을 만드는 내장함수인 open이 딕셔너리에 경로의 존재 유무에 상관없이 항상 호출됩니다. 이로 인해 프로그램상에 존재하던 열린 파일 핸들과 혼동될 수 있는 새로운 파일 핸들이 생길수도 있습니다. open()이 예외를 던질수도 있기 때문에 예외를 처리해야하지만 setdefault가 던지는 예외와 구분하기 어려울 수 있습니다.
  • defaultdict를 사용하는 경우의 문제는 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__ 메서드가 호출됩니다. 이 메서드는 키에 해당하는 디폴트 값을 생성해 딕셔너리에 넣어준 다음에 호출한 쪽에 그 값을 반환합니다. 이후 딕셔너리에서 같은 경로에 저근하면 이미 해당 원소가 딕셔너리에 들어있으므로 __missing__은 호출되지 않습니다.

20. None을 반환하기보다는 예외를 발생시켜라

특별한 의미를 표시하는 None을 반환하는 함수를 사용하면 None과 다른 값이 조건문에서 False로 평가될 수 있기 때문에 실수할 가능성이 있다. 특별한 상황을 표현하기 위해 None을 반환하는 대신 예외를 발생시키는 것이 좋습니다.

  • ZeroDivisionError에 대해 None을 반환하는 코드입니다.위의 조건문에서 None인지 검사하는 대신 False인지 검사하는 코드가 있다고 해봅시다.False와 동등한 반환값을 잘못 해석하는 경우는 None이 특별한 의미를 가지는 파이썬 코드에서 흔히 저지르는 실수입니다.
  • x,y = 0,5 result = careful_divdie(x,y) if not result: print('input error') # input error
  • def careful_divdie(a,b): try: return a/b except ZeroDivisionError: return None x,y = 1,0 result = careful_divdie(x,y) if result is None : print('input error') # input error
  • 이런 문제에 대해 가독성을 높이고, 실수를 줄이는 첫번째 방법은 에러 유무에 대한 값과 계산값을 tuple로 반환하는 방법입니다.더 나은 두번째 방법은 None을 반환하지 않고, ZeroDivisionError가 발생한 경우 이를 ValueError로 바꿔 호출한 쪽에 입력값이 잘못됬음을 알리는 방법입니다.
  • def careful_divide(a,b): try: return a/b except ZeroDivisionError as e: raise ValueError('input error') x,y =5,2 try: result = careful_divide(x,y) except ValueError: print('input error') # 결과 2.5
  • def careful_divide(a,b): try: return True,a/b except ZeroDivisionError: return False,None x,y = 0,5 success,result = careful_divide(x,y) if not success: print('input error')
  • 위 접근방법을 type annotation을 사용하는 코드에도 적용할 수 있습니다. 파이썬의 점진적 타입 지정(gradual typing)에서는 호출자가 어떤 Exception을 잡아내야 할지 결정할 때 문서를 참조할것을 예상하고, 발생시키는 예외를 문서에 명시하는 것이 해야합니다.
  • def careful_divide(a: float, b:float)->float: """ :param a:numerator :param b:denominator :return: a/b Raise : ValueError : ZeroDivisionError """ try: return a/b except ZeroDivisionError as e: raise ValueError('input error')

'python' 카테고리의 다른 글

이펙티브 파이썬 2nd 정리 #5  (0) 2023.09.08
이펙티브 파이썬 2nd 정리 #4  (0) 2023.09.01
이펙티브 파이썬 2nd 정리 #3  (0) 2023.08.18
이펙티브 파이썬 2nd 정리 #1  (1) 2023.07.28