Effective Python C1. Pythonic(3) - bytes와 str 차이를 알아두라

4 분 소요

Introduction

파이썬에서는 문자열 시퀀스를 표현하는 방법으로 bytes와 str이 있다. 이 둘의 차이를 알아본다.

Best WAY 3 : bytes와 str의 차이를 알아두라

bytes 타입은 부호가 없는 8바이트 데이터가 그대로 들어가며, str 인스턴스는 유니코드 코드 포인터가 들어있다.

hello_bytes = b'h\x65llo'
hello_str = 'a\u0300 propos'
print(hello_bytes, hello_str)
print(list(hello_bytes), list(hello_str))
>>>
b'hello' à propos
[104, 101, 108, 108, 111] ['a', '̀', ' ', 'p', 'r', 'o', 'p', 'o', 's']

유니코드 샌드위치

str에 대응하는 이진 인코딩이 없고, bytes에 대응하는 텍스트 인코딩이 없기 떄문에, bytes와 str을 서로 변환하기 위해서는 str의 encode 또는 bytes의 decode 메서드가 필요하다.
저자가 추천하는 방식은 코드의 핵심 부분은 str을 이용해 처리하고, encode와 decode되는 부분은 최대한 프로그램 바깥(인터페이스 경계 지점)에서 사용해야 한다는 것이다.
이를 유니코드 샌드위치 방식이라고 한다.

도우미 함수의 사용

문자를 표현하는 방식이 두 가지 이므로, 특정 인코딩 방식을 정하고 하나로 통일해 사용하고 싶을 때가 있다.
다음과 같은 도우미 함수를 쓸 수 있다.

def to_str(byte_or_str):
    if isinstance(byte_or_str, bytes):
        value = byte_or_str.decode('utf-8')
    else:
        value = byte_or_str
    return value

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

print(repr(to_str(b'foo')))
print(repr(to_str('foo')))
print(repr(to_str(b'\xed\x95\x9c\xea\xb8\x80')))
print(repr(to_bytes(b'foo')))
print(repr(to_bytes('foo')))
print(repr(to_bytes('한글')))
>>>
'foo'
'foo'
'한글'
b'foo'
b'foo'
b'\xed\x95\x9c\xea\xb8\x80'

str과 bytes의 연산자 혼용 금지

str 인스턴스와 bytes 인스턴스는 서로 연산자 호환이 안된다.
이를테면, str 또는 bytes 인스턴스 끼리는 +, ==, < 와 같은 연산자를 사용하나, str과 byte 인스턴스 간에 이러한 연산자를 적용하면 type-error가 나오거나 이상한 결과가 출력된다.

b'one' + 'two'

>>>
Trackback...
TypeError : can't concat str to bytes

이진 쓰기 모드와 텍스트 쓰기 모드

내장함수인 open을 호출해 얻은 파일 핸들과 관련된 연산은 기본적으로 유니코드 문자열을 요구한다.
따라서 다음과 같이 이진 바이트 문자열을 쓰면 오류가 생긴다. ‘r’로 읽어도 동일한 문제가 생긴다.

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

>>>
TypeError.. .

이를 방지하려면 b를 붙여 이진 쓰기 모드로 읽어야 한다.

with open('data.bin', 'wb') as f:
    f.write(b'\xf1\xf2\xf3')

또는 bin파일 등을 읽을 때 인코딩 차이로 인해 결과값이 이상하게 나올 수 있는 경우, encoding 파라미터를 명시한다.

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

Discussion

bytes가 왜 필요한가?

보통 문자열을 다룰 때는 str를 자주 사용하고, bytes는 사용하지 않는다. 저자도 문자열을 다루는 핵심 코드에서는 str을 사용하라고 권장했다.
그럼에도 불구하고, bytes를 사용할 때가 있는데, 바로 컴퓨터 메모리에 직접적으로 데이터를 저장하거나 불러오는 코드를 작성할 때이다. 유니코드 코드 포인트는 문자열을 사람이 편하게 읽도록 만들어진 추상적 개념이며, 실제로는 byte 형태로만 메모리에 저장이 가능하다.
따라서 텍스트 파일을 PC에 저장할 때도 utf-8 등의 인코딩 방식으로 유니코드 코드 포인터를 bytes로 인코딩해 저장하게 된다. 개념을 더 확장하면, 이미지의 경우에는 png, jpeg와 같은 다양한 인코딩 포멧이 있지만 실제 데이터는 bytes로 구성되어 있는 것이다.
이에 대한 설명은 stack overflow에 나와있는 글을 참고하면 좋다. 링크

Summary

  • bytes에는 8비트의 시퀀스가 들어있고, str에는 유니코드 코드 포인트 시퀀스가 들어있다.
  • 처리할 입력을 정해진 문자 시퀀스로 처리하려면 도우미 함수(예제의 to_bytes, to_str 같은)를 사용하라.
  • 이진데이터 파일을 읽거나 쓰려면 이진 모드(‘rb’나 ‘rw’)로 파일을 연다.
  • 유니코드 데이터를 읽을 때는 시스템 디폴트 인코딩에 주의하고, 인코딩 차이를 방지하기 위해서는 open에 encoding 파라미터를 명시하라.

Reference

파이썬 코딩의 기술 제 2판. - 브렛 슬리킨 지음 / 오현석 옮김