__missing__을 사용해 키에 따라 다른 디폴트 값을 생성하는 방법을 알아두라
15 Nov 2021
[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__
메서드가 호출됩니다. 이 메서드는 키에 해당하는 디폴트 값을 생성해 딕셔너리에 넣어준 다음에 호출한 쪽에 그 값을 반환합니다.