bytes와 str의 차이는?
파이썬에는 문자열 데이터의 시퀀스를 표현하는 두 가지 타입이 있다. 바로 bytes와 str이다.
아래 코드처럼 bytes 타입의 인스턴스에는 부호가 없는 8바이트 데이터가 그대로 들어간다.
(종종 아스키 인코딩을 사용해 내부 문자를 표시한다.)
a = b'h\x65llo'
print(list(a))
print(a)
>>>
[104, 101, 108, 108, 111]
b'hello'
위 코드에서 \x65는 e를 의미한다. 그리고 b 뒤에 ' '(작은따옴표)나 " "(큰따옴표)를 붙이면 바이트(bytes) 객체가 된다. 그래서 리스트로 변환 시 bytes가 출력된다.
str 인스턴스에는 사람이 사용하는 언어의 문자를 표현하는 유니코드 코드 포인트(code point)가 들어 있다.
a = 'a\u0300 propos'
print(list(a))
print(a)
>>>
['a', '̀', ' ', 'p', 'r', 'o', 'p', 'o', 's']
à propos
중요한 사실은 str 인스턴스에는 직접 대응하는 이진 인코딩이 없고 bytes에는 직접 대응하는 텍스트 인코딩이 없다는 점이다. 유니코드 데이터를 이진 데이터로 변환하려면 str의 encode 메서드를 호출해야 하고, 이진 데이터를 유니코드 데이터로 변환하려면 bytes의 decode 메서드를 호출해야 한다. 두 메서드를 호출할 때 우리가 원하는 인코딩 방식을 명시적으로 지정할 수도 있고, 시스템의 디폴트 인코딩을 받아들일 수도 있다.
일반적으로는 UTF-8이 디폴트 인코딩 방식이다.
파이썬 프로그램을 작성할 때 유니코드 데이터를 인코딩하거나 디코딩하는 부분을 인터페이스의 가장 먼 경계 지점에 위치시키는 방식을 유니코드 샌드위치라고 한다. 프로그래밍의 핵심 부분은 유니코드 데이터가 들어 있는 str을 사용해야 하고, 문자 인코딩에 대해 어떠한 가정도 해서는 안 된다. 이런 접근 방식을 사용하면 다양한 텍스트 인코딩으로 입력 데이터를 받아들일 수 있고, 출력 텍스트 인코딩은 한 가지로 엄격히 제한할 수 있다.
UTF-8과 유니코드를 간략히 설명하자면
Unicode : 전 세계의 글자와 코드가 일대일 매핑되어 있는 코드표
UTF-8 : 존재하는 코드표를 컴퓨터가 이해할 수 있도록 변환하는 방식, 즉 인코딩 방식.
-> UTF-8은 유니코드를 인코딩(encoding)하는 방식이다. 세상에는 UTF-8 말고도 수많은 인코딩 방식이 있다.
문자를 표현하는 타입이 둘로 나뉘어져 있기 때문에 파이썬에서는 다음과 같은 두 가지 상황이 자주 발생한다.
- UTF-8(혹은 다른 인코딩)으로 인코드된 문자인 8비트 값을 처리해야 하는 상황
- 특정 인코딩을 지정하지 않은 유니코드 문자열을 사용하는 상황
두 가지 경우를 변환해주고 입력 값이 코드가 원하는 값과 일치하는지 확신하기 위해 종종 두 가지 도우미 함수가 필요하다.
첫 번째 함수는 bytes나 str 인스턴스를 받아 항상 str을 반환한다.
def to_str(bytes_or_str):
if isinstance(bytes_or_str, bytes):
value = bytes_or_str.decode('utf-8')
else:
value = bytes_or_str
return value # str 인스턴스 (유니코드 문자 저장)
print(repr(to_str(b'foo')))
print(repr(to_str('bar')))
print(repr(to_str(b'\xed\x95\x9c'))) # UTF-8에서 한글은 3바이트
>>>
'foo'
'bar'
'한'
두 번째 함수는 bytes나 str 인스턴스를 받아 항상 bytes를 반환한다.
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 # bytes 인스턴스 (8비트 저장)
print(repr(to_bytes(b'foo')))
print(repr(to_bytes('bar')))
print(repr(to_bytes('한글')))
>>>
b'foo'
b'bar'
b'\xed\x95\x9c\xea\xb8\x80'
둘을 사용할 때 기억할 점
이진 8비트 값과 유니코드 문자열을 파이썬에서 다룰 때 기억해야 할 두 가지 문제점이 있다.
첫 번째 문제점은 bytes와 str이 똑같이 작동하는 것처럼 보이지만 각각의 인스턴스는 서로 호환되지 않기 때문에 현재 전달 중인 문자 시퀀스가 어떤 타입인지를 잘 알고 있어야 한다는 것이다.
다양한 예
예를 들어, + 연산자를 사용하면 bytes끼리 더하거나 str끼리 더할 수 있지만 str을 bytes에 더할 수는 없고 그 역도 불가능하다.
print(b'one' + b'two')
print('one' + 'two')
>>>
b'onetwo'
onetwo
print(b'one' + 'two')
>>>
Traceback (most recent call last):
TypeError: can't concat str to bytes
이항 연산자를 사용하면 bytes와 bytes를 비교하거나 str을 str과 비교할 수 있다.
assert b'red' > b'blue' # assert는 https://wikidocs.net/21050 참고
assert 'red' > 'blue'
허나 str와 bytes는 비교할 수 없다.
assert b'red' > 'blue'
>>>
Traceback (most recent call last):
TypeError: '>' not supported between instances of 'bytes' and 'str
내부에 똑같은 문자들이 들어 있더라도 bytes와 str이 같은지 비교하면 항상 False가 나온다.
print(b'foo' == 'foo')
>>>
False
형식화 문자열에서의 문제점
% 연산자는 각 타입의 형식화 문자열(format string)에 대해 작동한다.
print(b'red %s' % b'blue')
print('red %s' % 'blue')
>>>
b'red blue'
red blue
그러나 파이썬이 어떤 이진 텍스트 인코딩을 사용할 지 알 수 없으므로 str 인스턴스를 bytes 형식화 문자열에 넘길 수는 없다.
print(b'red %s' % 'blue')
>>>
Traceback (most recent call last):
TypeError: %b requires a bytes-like object, or an object that implements __bytes__, not 'str'
str 형식화 문자열에 bytes 인스턴스를 넘길 수는 있다! 그러나 이 경우에는 아마 예상과는 다르게 동작할 것이다.
print('red %s' % b'blue')
>>>
red b'blue'
이 코드는 실제로 bytes 인스턴스의 __repr__ 메서드를 호출한 결과로 %s를 대신한다. 따라서 b'blue'가 출력에도 그대로 남는다.
두 번째 문제점은 (내장 함수인 open을 호출해서 얻은)파일 핸들과 관련된 연산들이 디폴트로 유니코드 문자열을 요구하고 이진 바이트 문자열을 요구하지 않는다는 것이다. 이로 인해 코드가 실행되지 않을 수도 있다. 특히 파이썬 2에 익숙하다면 더더욱 그렇다.
예를 들어 이진 데이터를 파일에 기록하고 싶다고 하자. 다음과 같은 간단한 코드도 오류가 난다.
with open('data.bin', 'w') as f:
f.write(b'\xf1\xf2\xf3\xf4\xf5')
>>>
Traceback (most recent call last):
TypeError: write() argument must be str, not bytes
TypeError 옆을 읽으면 알겠지만 예외가 발생한 이유는 파일을 열 때 이진 쓰기 모드('wb')가 아닌 텍스트 쓰기 모드('w')로 열었기 때문이다. 파일이 텍스트 모드인 경우 write 연산은 이진 데이터가 들어 있는 bytes 인스턴스가 아니라 유니코드 데이터가 들어 있는 str 인스턴스를 요구한다. 이 문제는 'wb'모드를 사용해 파일을 열면 해결할 수 있다.
파일에서 데이터를 읽을 때도 비슷한 문제가 발생할 수 있다.
참고
파이썬 코딩의 기술 - 똑똑하게 코딩하는 법, 브렛 슬라킨 저/오현석 역
'◎ Python > 코딩의 기술 (책)' 카테고리의 다른 글
[코딩의 기술] 6. 인덱스보다는 대입을 사용해 데이터를 언패킹하자 (1) | 2022.11.30 |
---|---|
[코딩의 기술] 5. 복잡한 식을 쓰는 대신 도우미(헬퍼) 함수를 작성하자 (0) | 2022.11.29 |
[코딩의 기술] 4. f-문자열을 통한 인터폴레이션을 사용하라 (1) | 2022.11.28 |
[코딩의 기술] 2. PEP 8 스타일 가이드를 따르자 (2) | 2022.09.15 |
[코딩의 기술] 1. 사용 중인 파이썬의 버전을 알자 (1) | 2022.09.14 |
자기계발 블로그