[Effective Python] 1. bytes와 str의 차이를 알자

파이썬 코딩의 기술 개정 2판 BETTER WAY-3


파이썬 코딩의 기술이라는 책을 공부해가면서 포스팅을 이어갈 예정입니다. 제가 이해하지 못한 챕터는 이해할때까지 반복 후 게시할려고 합니다.

지금부터 설명할 내용을 이해하려면, 인코딩의 개념에 대해 조금이나마 알고 있어야합니다. 그래서 인코딩/디코딩 개념에 대해 조금이나마 짚고 넘어가도록 하겠습니다.

전통적으로 우리가 사용하는 컴퓨터는 0과 1로 구성된 이진수만 이해할 수 있습니다. 그래서, 우리가 일상에서 사용하는 텍스트의 경우, 컴퓨터가 이해하기 위해 이진수로 변환할 필요가 있었습니다. 즉, 텍스트를 컴퓨터가 이해할 수 있는 바이너리 데이터로 변환해주는 것을 인코딩이라고 하고, 그 반대의 개념을 디코딩이라고 합니다.

어찌됐든, 바이너리 데이터만 인식할 수 있는 컴퓨터로 인해 텍스트를 바이너리로 변환할 필요성이 있었고 그로인해 ASCII 코드, 유니코드(UTF-8)와 같은 문자 코드가 생겨났습니다.

하지만 컴퓨터 시스템마다 사용하고 있는 디폴트 인코딩 포맷이 모두 동일하지 않을 것입니다. 컴퓨터 시스템마다 사용하고 있는 인코딩 포맷이 다르다는 것은 파이썬 코드를 작성할 때 크고 작은 불편을 야기시킵니다. 아마 파이썬을 이용하여 프로그램을 만들어보신 분들이라면 한글이 아닌 외계어가 표시되는 상황을 자주 마주하고 또 수정하기 위해 꽤나 노력을 쏟았을 것입니다.

python에는 문자열을 표시하는 2가지 타입이 존재합니다. bytes와 str이 바로 그것입니다. 몇가지 간단한 예를 통해 bytes와 str에 대해 알아봅시다.

보통 파이썬에서 문자열을 다루다 보면 다음과 같은 상황이 자주 발생합니다.

  • UTF-8(또는 다른 인코딩 방식)로 인코딩된 8비트 시퀀스를 그대로 사용하고 싶다.
  • 특정 인코딩을 지원하지 않는 유니코드 문자열을 사용하고 싶다.

파이썬 코딩의 기술에서는 이런 경우 입력 값이 원하는 인코딩 포맷과 일치하는지 확신하기 위해, 아래와 같은 도우미 함수를 이용하는 것을 권장합니다.

1
2
3
4
5
6
7
8
9
10
11
def to_str(bytes_or_str):
  if isinstance(bytes_or_str, bytes):
    value = bytes_or_str.decode('utf-8')
  else:
    value = bytes_or_str

def to_bytes(bytes_or_str):
  if isinstance(bytes_or_str, str):
    value = bytes_or_str.encode('utf-8')
  else:
    value = bytes_or_str

위의 간단한 도우미 함수를 사용하여, 원하는 텍스트의 인코딩 포맷을 고정시킬 수 있으며, 추가적으로 유니코드 샌드위치라는 방법을 사용하는 것 또한 권장합니다.

유니코드 샌드위치
데이터를 인코딩/디코딩하는 부분은 인터페이스의 가장 먼 경계 지점에 위치시키고, 프로그램의 핵심 코드 부분은 항상 str을 사용하도록 구현하는 것.

다음으로는 bytes와 str을 사용할 때 문제점에 대해 알아보도록 하겠습니다.

책에서는 2가지 문제점을 꼭 기억하라고 말합니다. 첫번째 문제점은 bytes와 str은 서로 호환되지 않기 대문에 전달 중인 문자 시퀀스가 어떤 타입인지 항상 잘 알고 있어야 한다는 것입니다. (이 부분은 도우미 함수와 유니코드 샌드위치를 사용하면 될 것 같습니다만 책에서는 따로 위에서 언급한 것을 사용하라고 되어있진 않군요). 또한 책에서는 인스턴스간 호환되지 않는 경우의 예를 많이 보여주고 있지만 여기서는 생략하도록 하겠습니다.

두번째 문제점은 open 호출을 통해 얻은 파일 핸들과 관련한 연산들이 디폴트로 유니코드 문자열을 요구하고 바이너리 문자열을 요구하지 않는다는 것 입니다. 다음의 예제 코드를 봅시다.

1
2
with open('data.bin', 'w') as f:
  f.write(b'\xf1\xf2\xf3\xf4\xf5')

위의 예제 코드는 오류가 발생합니다. 오류가 발생한 이유는 파일을 열 때, 이진 쓰기 모드(‘wb’)가 아닌 텍스트 쓰기 모드(‘w’)로 열었기 때문입니다. 그렇다면 바이너리 파일을 read하는 경우는 어떨까요?

아래의 코드 또한 바이너리 데이터를 일반 텍스트 모드로 read 열었기 때문에 에러가 발생합니다. 즉 바이너리 데이터의 read/write 시에는 반드시 바이너리 모드를 사용해야 합니다.

1
2
with open('data.bin', 'r') as f:
  data = f.read()


다음은 에러를 발생시켰던 코드를 정상 동작하도록 수정한 코드 입니다. 단순히 ‘w’와 ‘r’을 ‘wb’, ‘rb’로 수정한 것 입니다.

1
2
3
4
5
with open('data.bin', 'wb') as f:
  f.write(b'\xf1\xf2\xf3\xf4\xf5')

with open('data.bin', 'rb') as f:
  data = f.read()


위의 방법 외에 다른 방법도 있습니다. 다음 코드와 같이 open시에 encoding 파라미터를 명시하는 것 입니다. (명시하지 않을 경우 시스템 디폴트 인코딩을 사용합니다) 또한 이 방법은 특수한 상황에서 사용자가 정말로 원하는 출력 방식일수 도 있습니다. (바이너리 값이 그대로 출력되는 것이 아니라 인코딩 포맷에 맞는 텍스트를 출력해주기 때문)

1
2
with open('data.bin', 'r', encoding='cp1252') as f:
  data = f.read()


즉, 사용자는 시스템의 디폴트 인코딩이 무엇인지 시스템 인코딩을 검사해야할 필요가 있습니다. 시스템 디폴트 인코딩은 아래의 코드로 확인이 가능합니다.

1
2
3
4
import locale

if __name__ == '__main__':
    print(locale.getpreferredencoding())


자, 포스팅을 통해 bytes와 str 사용시 유의할 점과 사용 스킬들에 대해 알아보았습니다. 현재 일하는 회사에서 여러 국가의 언어를 사용하는 경우가 많고, 제가 직접 작성한 코드가 아닌 독일이나 중국에서 작성된 레거시 코드를 수정하는 경우도 종종 있었습니다. 그 때 수정했던 기억으로는 의도했던 텍스트가 외계어로 표시되어 인코딩을 수정하는데 꽤 힘들었던 기억이 있습니다. 약간은 ‘사’의 기운이 느껴지는 방법으로 문제를 해결 했었는대, 이제 동일한 문제가 발생한다면 유니코드 샌드위치 방법으로 코드를 리팩토링하고, 좀 더 정확한 방법으로 문제를 해결할 수 있을 것 같네요 ^_^

Updated:

Leave a comment