2.6. 텍스트 대 바이트¶
Python에는 원시 문자 데이터를 위한 두 가지 시퀀스 타입이 있습니다:
str– 유니코드 코드포인트의 시퀀스. 파일 경로, 로그 메시지, JSON 페이로드 등 사람이 읽을 수 있는 모든 텍스트에 사용됩니다.bytes– 0부터 255 범위의 정수 시퀀스. UART 프레임, 이미지 버퍼, 네트워크 패킷, 레지스터 값 등 원시 이진 데이터에 사용됩니다.
이 둘은 명시적 변환 없이 혼합할 수 없습니다. str을 하드웨어 write 메서드에 전달하면 TypeError가 발생하며, 그 반대도 거부됩니다.
str 은 유니코드 문자를 저장하고, bytes 는 원시 옥텟(octet)을 저장합니다. 이 둘 사이를 오가는 것이 인코딩(str → bytes)과 디코딩(bytes → str)입니다.¶
2.6.1. bytes 리터럴¶
bytes 리터럴은 b가 접두사로 붙은 문자열 형태의 리터럴입니다:
header = b"OMV"
crlf = b"\r\n"
payload = b"\x01\x02\x03"
bytes 리터럴 안에는 ASCII 문자만 직접 사용할 수 있으며, ASCII가 아닌 값은 \xHH 16진수 이스케이프로 작성해야 합니다.
2.6.2. 인코딩과 디코딩¶
str.encode()는 명명된 인코딩(기본값"utf-8")을 사용하여 문자열을 바이트로 변환합니다.bytes.decode()는 그 반대를 수행합니다.
>>> "hello".encode()
b'hello'
>>> "héllo".encode()
b'h\xc3\xa9llo' # é is two bytes in UTF-8
>>> b"hello".decode()
'hello'
UTF-8은 기본값이며, ASCII가 아닌 문자가 포함될 수 있는 모든 경우에 적합한 선택입니다. 데이터가 순수 ASCII임이 보장될 때만 "ascii"를 사용하세요. 그래야 ASCII가 아닌 바이트가 끼어들었을 때 조용히 통과되지 않고 UnicodeError가 발생합니다.
2.6.3. 인덱싱과 슬라이싱¶
bytes 값은 인덱싱할 때 한 바이트짜리 문자열의 시퀀스가 아니라 정수의 시퀀스처럼 동작합니다:
>>> data = b"abc"
>>> data[0]
97 # the int 97, not 'a'
>>> data[0:1]
b'a' # slicing returns bytes
흔한 실수는 data[0] == "a"로 비교한 뒤 결과가 False인 것에 놀라는 것입니다 – data[0]은 한 문자짜리 문자열이 아니라 정수이므로 두 값은 절대 일치할 수 없습니다.
2.6.4. ord와 chr – 문자와 정수 잇기¶
bytes를 인덱싱하면 정수가 반환되지만 프로그램의 나머지 부분은 대개 문자 단위로 생각하기 때문에, Python은 둘 사이를 오가기 위한 두 가지 내장 함수를 제공합니다:
>>> ord("a")
97
>>> chr(97)
'a'
>>> ord("A"), chr(0x41)
(65, 'A')
ASCII 문자의 경우 코드포인트는 바이트 값과 같으므로, ord("a")와 b"a"[0]은 모두 97을 반환합니다. 덕분에 바이트 비교를 실제로 관심 있는 문자 관점에서 읽을 수 있습니다:
>>> data = b"abc"
>>> data[0] == ord("a") # instead of the magic number 97
True
그리고 chr()는 바이트의 인쇄 가능한 형태를 보고 싶을 때 로깅이나 디버깅에 유용합니다:
>>> chr(data[0])
'a'
ASCII가 아닌 문자의 경우 ord()는 유니코드 코드포인트를 반환하는데, 이는 인코딩된 형태의 어떤 단일 바이트와도 같지 않습니다. 바이트 표현은 인코딩에 따라 달라집니다.
2.6.5. 가변 버퍼를 위한 bytearray¶
bytes는 불변입니다 – 모든 “수정”은 새 객체를 반환하고 원본은 그대로 둡니다. 수정하거나, 추가하거나, 조금씩 채워 넣을 데이터에는 bytearray를 사용하세요. 이는 bytes와 동일한 내용을 담지만 제자리 변경을 지원합니다:
>>> s = b"hello"
>>> s[0] = ord("H")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment
>>> s = bytearray(b"hello")
>>> s[0] = ord("H")
>>> s
bytearray(b'Hello')
2.6.5.1. bytearray 생성¶
bytearray 생성자는 여러 입력을 받습니다:
bytearray(8)– 0 바이트 8개로 이루어진 버퍼.bytearray(b"hello")– bytes 값의 가변 복사본.bytearray("hello", "utf-8")– 주어진 인코딩을 사용하여 문자열로부터 만든 bytearray.bytearray([72, 73, 74])– 0부터 255 범위의 정수 시퀀스로부터 만든 bytearray(여기서는b"HIJ").
>>> bytearray(4)
bytearray(b'\x00\x00\x00\x00')
>>> bytearray(b"abc")
bytearray(b'abc')
>>> bytearray("café", "utf-8")
bytearray(b'caf\xc3\xa9')
2.6.5.2. bytearray 수정¶
인덱스 및 슬라이스 할당은 list와 똑같이 동작합니다:
>>> buf = bytearray(8) # 8 zero bytes
>>> buf[0] = 0xFF # one byte at a time
>>> buf[1:4] = b"ABC" # replace a slice
>>> buf
bytearray(b'\xffABC\x00\x00\x00\x00')
개별 바이트는 0부터 255 범위의 정수여야 합니다. 다른 타입을 할당하면 TypeError 또는 ValueError가 발생합니다.
슬라이스 할당은 버퍼의 길이를 변경할 수 있습니다. 슬라이스를 더 긴 값으로 교체하면 bytearray가 커지고, 더 짧은 값으로 교체하면 줄어듭니다. b""로 교체하면 슬라이스가 완전히 삭제됩니다:
>>> buf = bytearray(b"abcdef")
>>> buf[1:3] = b"XYZ" # 2 bytes replaced with 3
>>> buf
bytearray(b'aXYZdef')
>>> buf[1:4] = b"" # delete the inserted run
>>> buf
bytearray(b'adef')
bytearray.append()와 bytearray.extend() 메서드는 매번 전체 버퍼를 재할당하지 않고 끝에 바이트를 추가합니다:
>>> buf = bytearray()
>>> buf.append(0x01)
>>> buf.extend(b"abc")
>>> buf
bytearray(b'\x01abc')
2.6.5.3. bytearray에서 읽기¶
인덱싱, 슬라이싱, 반복, 그리고 bytes 검사 메서드(bytes.startswith(), bytes.find(), bytes.strip() 등)는 모두 bytes 값에서와 동일하게 동작합니다. 인덱싱은 정수를 반환하고, 슬라이싱은 또 다른 bytearray를 반환합니다:
>>> buf = bytearray(b"OpenMV")
>>> buf[0]
79
>>> buf[0:4]
bytearray(b'Open')
>>> buf.startswith(b"Open")
True
2.6.5.4. bytes와 bytearray 간 변환¶
bytes와 bytearray는 각자의 생성자로 서로 변환됩니다. API가 특정 형태를 요구할 때 이를 사용하세요:
>>> ba = bytearray(b"hello")
>>> snapshot = bytes(ba) # immutable copy
>>> ba[0] = ord("H")
>>> ba, snapshot
(bytearray(b'Hello'), b'hello')
2.6.5.5. 제로 복사 슬라이싱을 위한 memoryview¶
bytes나 bytearray를 슬라이싱하면 보통 바이트를 새 버퍼로 복사합니다. memoryview는 복사 없이 동일한 바이트를 노출합니다:
>>> buf = bytearray(b"OpenMV Cam")
>>> view = memoryview(buf)
>>> view[0:6] # shares storage with buf
<memoryview ...>
>>> bytes(view[0:6]) # materialise as bytes when needed
b'OpenMV'
bytearray에 대한 뷰는 쓰기도 가능합니다 – 뷰를 변경하면 기저 버퍼가 변경됩니다:
>>> view[0] = ord("o")
>>> buf
bytearray(b'openMV Cam')
슬라이스를 복사하는 것이 낭비일 때 – 일반적으로 같은 대형 버퍼를 이리저리 전달하거나 조각 단위로 처리할 때 – memoryview를 사용하세요. 작은 bytes에 대한 일상적인 문자열 스타일 작업에는 일반 슬라이싱으로 충분합니다.