파이썬 2.5의 또 다른 주요 변화로 소개해 드릴 만한 것으로 incremental codec이 있습니다. 파이썬 유니코드 코덱의 기존 스펙에 대한 확장인데, 논의되는 과정 중에 여러가지 난관이 있었습니다. 확장이 쉬운 디자인이 어떤 것인가, 확장이 어려운 디자인을 택했을 때 나중에 확장을 어떻게 할 것인가에 대한 좋은 사례가 될 듯 합니다.
파이썬 2.0에서 처음 들어온 PEP-100 유니코드 통합에서는 4가지 코덱 방식을 정의하고 있습니다. 인코더, 디코더, 스트림 인코더, 스트림 디코더 입니다. 인코더와 디코더는 상태가 없는 (stateless) 단순 문자열 변환을 담당하고, 스트림 인코더와 스트림 디코더는 파일같은 스트림들에 대해서 상태가 있는 (stateful) 변환을 담당합니다.
얼핏 보면 상태가 있는 것도 있고, 없는 것도 있으니 괜찮은 디자인이 아닌가 하고 생각을 할 수 있는데, 파이썬 2.0이 릴리스 되고 나서, 그 이후에 JapaneseCodecs나 KoreanCodecs가 나오면서 문제가 발견되게 됩니다. 바로, 상태가 있는 문자열 변환을 할 수가 없다는 것입니다. 상태가 있는 변환을 하기 위해서는 스트림을 거쳐야하기 때문에, 엉뚱하게 계속 StringIO같은 것을 끼고 들어가야 하게 되어서, 번거롭기도 하고 느리기도 하고 아주 기분 나쁜 상황이 되어 버립니다. 게다가, 스트림은 끝이 단 하나만 존재하기 때문에, 현재 버퍼에 있는 미완성 부분을 완성하려고 파일 끝으로 표시해 버리면 더 이상 쓸 수도 없고 상태도 잃어버리는 문제도 있었습니다.
여기서 알 수 있는 디자인의 교훈은, 다른 언어에 있는 걸 무작정 따라하기 보다는, 해당 도메인에서 할 수 있는 작업들을 나열한 다음에, 각 작업들이 공통으로 가지는 최소한의 요소들을 뽑아서 그것들로 다른 작업들을 구성해 보면 좋겠다는 생각입니다. 간혹 어떤 라이브러리를 쓰다 보면, 한 함수의 극히 부분적인 기능이 필요한데, 더 작은 함수가 없어서 결국은 함수의 쓸데없는 다른 부분까지도 모두 에뮬레이트 해야 하는 경우가 있습니다. 그럴 때는 답답하고 억울해서 산에 가서 임금님 귀는~~ 이라도 하고 싶은 경험을 할 때가 있는데, 그 때의 경험을 마음 깊이 새겨서 그런 라이브러리를 안 만들도록 해야겠네요. 🙂
(얘기가 딴 데로 빠져서 다시 원래대로 오자면~) 그래서, 그동안 파이썬에서 여러모로 문제가 많았던 UTF-8 스트림 디코딩이 결국은 내부적인 상태 제어 함수를 만들어서 해결이 되었고, 애플리케이션들도 쉽게 이런 것을 쓸 수 있게 유니코드 스펙을 확장해서 기존의 4개에 2개를 더해 incremental decoder, incremental encoder가 추가되었습니다.
그런데 여기서 발생하는 또 하나의 디자인 문제! 코덱을 찾아 주는 codecs.lookup라는 함수는 위에서 언급한 4개를 tuple로 리턴해 주는 방식으로 되어 있다는 것! 그래서 결국은 이번에 새로 추가된 2개를 더하면 tuple의 크기가 바뀌어서 사용자 코드들이 하위호환성이 없어진다는 치명적인 문제가 생깁니다. codecs.lookup을 왜 public API로 공개해서 이런 문제를 만드냐~ 하고 이런 저런 서로 원망을 하다가 결국은 os.stat에서 쓴 트릭으로 처리가 되었습니다. 즉, 객체를 따로 하나 만들어서(codecs.CodecInfo) tuple 쓰듯이 접근하면 옛날처럼 4개를 돌려주고, 하위 속성을 접근하면 이름으로도 접근할 수 있게 하는 것입니다. 예를 들어, c = codecs.lookup(‘cp949’) 일 때, c[0]부터 c[3], len(c) 하면 옛날 tuple 쓰듯이 흉내를 내고, c.incrementalcodec 이나 c.encoder, c.streamwriter 같은 방법으로 새로운 API를 쓸 수 있게 하였습니다.
그래서, 어제 Walter가 작성한 기본 패치에 맞게 CJKCodecs 패치도 만들었는데, 이제 본격적으로 incremental codec을 쓰는 세션을 하나 보겠습니다. 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
>>> import codecs >>> dec = codecs.lookup('cp949').incrementaldecoder() # cp949 상태있는 디코더를 만듦 >>> dec.decode('한글') # 그냥 상태 없는 디코딩이나 마찬가지 u'\ud55c\uae00' >>> dec.decode('한글'[:3]) # 마지막 1바이트 빼고 넣음 u'\ud55c' >>> dec.decode('글'[1:]) # 남은 1바이트를 마저 넣어서 완성 u'\uae00' >>> dec.decode('한글'[:3]) # 다시 첫 3바이트를 넣음 u'\ud55c' >>> dec.decode('', True) # 버퍼에 '글' 앞쪽 바이트가 있는 것을 flush Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeDecodeError: 'cp949' codec can't decode byte 0xb1 in position 0: incomplete multibyte sequence >>> dec.reset() # 버퍼를 초기화 >>> dec.decode('', True) # 버퍼를 초기화했기 때문에 비어있음 u'' |
cp949는 사실 좀 밋밋하긴 한데, 살 떨리게 재미있는 ISO-2022로 한 번 해 보면..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> dec = codecs.getincrementaldecoder('iso2022-jp')() # 이게 더 좋은 방법 >>> ESC = '\x1b' >>> dec.decode(ESC+'(') # G1 캐릭터셋 할당 시퀀스 앞부분 u'' >>> dec.decode('J~~') # 0201로 할당 u'\u203e\u203e' >>> dec.decode('J~~') # 이제 앞의 J는 이스케이프가 아님 u'J\u203e\u203e' >>> dec.decode(ESC+'$B$@') # G1을 0208로 바꾸고 1글자 u'\u3060' >>> dec.decode('$@$@$') # G1에서 2글자 더 진행하고 반은 남김 u'\u3060\u3060' >>> dec.decode('@'+ESC+'(BABC') # 0208에서 마지막 완성하고 ascii에서 3글자 u'\u3060ABC' |
처음에는 이런 것을 지원해 주기 위해서 incremental codec이 아니라 feed style codec이라는 것이 나왔었는데, 뭔가 부자연스러워 보이는 것이 관련있는 사람 대부분이 답장도 안 올리고 바쁜 척을 했었습니다. 그런데, 새롭게 incremental codec이 나오고 나니까 다들 훨씬 깔끔하다고 칭찬을! 으흐흐. 아무래도 문제 해결을 억지로 라도 한 번 해 보고 나면, 훨씬 더 좋은 다른 방법을 발견하기도 쉽다는 것도 있겠고.. 다른 사람들이 바쁜 척하는 것 같으면 잘 눈치를 채야 한다는 뭔가가… -o-
다음 시간에는 뜨거운 감자였던 조건적 표현식(C에서 보통 삼항 연산자라고 부르는 그것)에 대해서~