Python의 Bytes와 Byte Array

2022년 5월 6일 수정

이 글은 Pythonbytesbytearray 타입을 간략히 정리하는 글이다. 사용된 예제는 3.7 이상의 버전에서 테스트되었다. 2.x 버전의 바이트 처리가 3.x에서 약간 바뀐 점도 있어서 2.x 버전에서는 예제가 오류가 날 가능성이 있다.

Bytes

bytes 타입은 이름처럼 바이트가 나열된 형태의 자료구조를 구현하는 클래스다. 그런데 클래스를 직접 이용해서 생성하는 것 보다는 아래와 같은 방식으로 가장 먼저 체험할 가능성이 높은 타입 같다.

>>> bs = b'abcd'
>>> bs
b'abcd'
>>> type(bs)
<class 'bytes'>

b 로 시작하는 문자열 표현을 사용했는데, b 의 의미는 bytes 로 이해할 수 있다면 그다지 어려울 것이 없는 것 같다. 말 그대로 4바이트짜리 바이트 버퍼가 생성되었다. 참고로 이렇게 바이트 버퍼를 문자열로 표기하는 것을 binstring 이라고 부른다.

물론 bytes 를 이용해 바로 생성하는 방법으로도 사용할 수 있다.

>>> b = bytes(8)
>>> b
b'\x00\x00\x00\x00\x00\x00\x00\x00'

생성자의 첫 매개변수는 바이트 버퍼의 바이트 카운트, 즉 사이즈다. 위의 경우 8 바이트짜리 버퍼가 생성된다.

안심스럽게(?) 생성된 버퍼는 모두 0으로 초기화되어 있는데, 불행히도 이 bytes 타입은 수정이 불가능한 타입이다. 다르게 표현해서 Immutable 타입이다. 그래서 0으로 초기화되어도 사실 별로 의미 없을지도 모른다.

따라서 의미있는 바이트 버퍼를 만들 때는 아래처럼 튜플이나 리스트로 실제 데이터를 표현해서 초기화할 수 있다.

>>> bytes((1, 2, 3, 4))
b'\x01\x02\x03\x04'
>>> bytes([1, 2, 3, 4])
b'\x01\x02\x03\x04'

그 외에 사용법은 리스트와 비슷하다.

>>> b = bytes([1, 2, 3, 4])
>>> len(b)
4
>>> b[2]
3

이 외에 리스트 슬라이스나 for 루프로 순환하기 등 리스트로 하던 행위를 대충 비슷하게 할 수 있다.

Byte Array

bytearray 타입은 이름처럼 바이트 배열(Byte Array)을 구현하는 클래스인데, bytes 가 수정이 불가능한 immutable 타입인 것에 반해 이 타입은 요소 수정이 가능한 타입이다. 다르게 말해서 mutable bytes 처럼 생각할 수도 있다.

따라서 생성은 아마도 생성자를 통해서 데이터를 넘겨주면 될 것 같다.

>>> ba = bytearray(b'abcd')
>>> ba
bytearray(b'abcd')

예상대로 된다.

자 그럼 mutable bytes라고 했으니 수정이 되는지도 보면 될 것 같다.

>>> ba[0] = 'A'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object cannot be interpreted as an integer

왜 안되는가라고 생각할 수 있는데 'A'Python의 문자열 오브젝트다. 그리고 Byte Array는 이름처럼 바이트 단위로 액세스를 해야 한다. 따라서 문자열 대신 한 바이트 수치를 넣는 방식이면 수정이 될 것 같다.

>>> ba[0] = 0x41
>>> ba
bytearray(b'Abcd')

0x41 즉 16진수 41은 대문자 A의 아스키 코드이다. 그래서 첫 글자가 대문자 A로 바뀌었다.

물론 이런 경우 ord() 함수를 쓰는게 더 편할 수 있다.

>>> ba[1] = ord('B')
>>> ba
bytearray(b'ABcd')

당연하게 이 방법이 직관적이다.

이 외에도 여러가지 방법으로 바이트 배열을 변조할 수 있다.

>>> ba.append(ord('e'))
>>> ba
bytearray(b'ABcde')
>>> del(ba[4])
>>> ba
bytearray(b'ABcd')

마치 리스트를 다루는 것과 비슷하게 쓸 수 있다. 예를 들어 슬라이스나 for 루프로 순환하기 등등 말이다.

바이트를 문자열로 바꾸기

바이트 버퍼나 바이트 배열을 다시 문자열로 받을 수 있는지 살펴보자.

>>> b = b'abcd'
>>> str(b)
"b'abcd'"

아… 뭔가 되기는 하는데 지금 원하는 것은 저게 아니다. 그렇다면 어떻게 해야 할까?

사실 우리가 쓰는 문자열은 유니코드 문자를 사용한다. 그래서 바이트 배열로 구성되는 C 문자열을 그대로 문자열로 변환하는 것은 이상한 행위다.

다행히도 해답은 있다. 이 경우 bytesbytearray 를 decode하면 다시 원래의 문자열을 얻을 수도 있다.

>>> b.decode()
'abcd'

Python 3 버전에 들어서는 encode()decode() 를 쓸 일이 많이 줄어서 좋았는데 다시 보게 되어서 약간 슬프다. 하지만 이번 경우엔 어쩔 수 없이 쓰는게 맞는 것 같다.

한글과 바이트

이제 한글을 쓰는 경우를 살펴보자. 한글도 binstring으로 표현하면 바로 바이트 배열로 쓸 수 있을까?

>>> b'한글'
  File "<stdin>", line 1
SyntaxError: bytes can only contain ASCII literal characters.

불행히도 그렇지 않다. 우리가 입력한 문자의 인코딩을 알려주지 않으면 바이트 버퍼로 바꿀 수 없다. 그럼 어떻게 해야할까?

다행히도 이 경우를 위해 encode() 를 사용할 수 있다.

>>> hbuf = '한글'.encode('utf-8')
>>> hbuf
b'\xed\x95\x9c\xea\xb8\x80'

인코딩을 명확하게 UTF-8 이라고 알려주고 인코딩을 하면 자연스럽게 바이트 버퍼가 튀어나오니 이걸 쓰면 된다.

그렇다면 반대의 경우는 어떻게 해야할까? 이미 힌트는 주어졌다. 했던 것의 반대 방법을 쓰면 된다.

>>> hbuf.decode('utf-8')
'한글'

Encode의 반대는 Decode니까 쉬운 문제였다.

일단 한글 주제로 이야기하긴 했는데, 사실상 유니코드로 표현하는 모든 문자에 해당하는 내용이다. 굳이 영어만 쓴다고 해도 바이트 단위로 작업을 해야한다면 인코딩과 디코딩은 하는 편이 좋다.

앞의 내용에서는 생략했지만 사실 bytes 는 생성자 자체에서 인코딩을 지원한다.

>>> bytes('한글', 'utf-8')
b'\xed\x95\x9c\xea\xb8\x80'

이렇게 써도 된다는 이야기다.