[初心者向け]Pythonのbytes型について初心者向けに書いてみた

bytes型・・・?という人向けです。
2023.05.10

クラスメソッド株式会社データアナリティクス事業本部所属のニューシロです。
今回はPythonのbytes型についてです。初心者向けです。

この記事について

対象者

  • Pythonのbytes型について初めて学ぶ方

説明すること

  • bytes型について簡単な説明
  • str.encode()
  • UnicodeとUTF-8の変換
  • print関数で注意すること

説明しないこと

  • Unicode、UTF-8、ASCII等文字コードについての細かい話

執筆のきっかけ

初心者の私が学んだPythonのbytes型について、自身の理解のためにも記事にしてみようと思いました。大枠を理解するための記事ですので、気になった箇所の細かい内容については別途調べてみてください。

本題

bytes型とは

Pythonのデータ型には多くの種類がありますが、その中の1つにbytes型があります。
bytes型はバイナリデータを扱うデータ型で、文字列の文字コードを表しています。
Python公式ドキュメントでは以下のように説明されています。

バイナリデータを操作するためのコア組み込み型は bytes および bytearray です。

bytes はバイトの不変なシーケンスです。多くのメジャーなプロトコルがASCIIテキストエンコーディングをベースにしているので、 bytes オブジェクトは ASCII 互換のデータに対してのみ動作する幾つかのメソッドを提供していて、文字列オブジェクトと他の多くの点で近いです。

この説明だとちょっとイメージが湧きにくいと思うので、具体例を挙げてbytes型についてみていきましょう。

str型をbytes型に変換してみる

bytes型を用いるために、str型をbytes型に変換してみます。encodeメソッドを用いると、str型をbytes型に変換することができます。

encodeでは引数を指定しない場合、UTF-8を文字コードとして選択するようになっています。

まずは"ねこ"というstr型の文字列をbytes型に変換し、その後print関数を用いて出力してみます。

str_word_1 = "ねこ"
encorded_str_word_1 = str_word_1.encode()

print(f"{str_word_1}: {encorded_str_word_1}")
print(f"type: {type(encorded_str_word_1)}")

↓ 結果

ねこ: b'\xe3\x81\xad\xe3\x81\x93'
type: <class 'bytes'>

bytes型に変換された"ねこ"が出力されました。
このように、bytes型はb'とシングルクォート'で囲われています。また、シングルクォートの代わりにダブルクォート"や3重クォート'''でも構いません。

\xは続く2文字、つまり\xnnで1バイトを表します。よって、このbytes型は6バイトであり、文字コードをわかりやすく表記すると以下のようになります。

# ね                こ
# b'\xe3\x81\xad   \xe3\x81\x93'
      e3  81  ad     e3  81  93

UTF-8では
"ね":e381ad "こ":e38193
それぞれと表されます。出力結果も同様のものが表示されていますね。

UTF-8について調べる

先ほど"ね"はe381ad、"こ"はe38193と記しましたが、そもそもこの値はどのように決められているのか?と疑問に思う方もいると思うので、調べてみましょう。

UTF-8の"ね"と"こ"を調べるために、以下のリンクからまずはUnicodeの"ね"と"こ"を調べます。
Unicodeは文字コードの標準規格です。ややこしいですが、
(文字列) → (Unicode) → (UTF-8) の流れで調べています。

この表から、

"ね":U+306D "こ":U+3053

ということがわかります。これがUnicodeでの文字コード(コードポイントともいう)で、U+以下の4桁の数字を用いて文字を決定しています。

ちなみに以下のコードからも得られます。 こちらはord()でUnicodeでの文字コードを取得し、hex()で16進数表記にしています。

print(f"ね: {hex(ord('ね'))}")
# ね: 0x306d
print(f"こ: {hex(ord('こ'))}")
# こ: 0x3053

これらをUTF-8へ変換します。まずは"ね"を変換します。

流れ

UnicodeのU+306DをUTF-8へ変換します。
306Dを2進数に変換する
U+306Dは3バイトの範囲なので、変換した16桁の数字を1110 xxxx 10xx xxxx 10xx xxxxへ入れる
③ 16進数に戻す

やってみる

306Dを2進数に変換します。16は2の4乗ですので、16進数の1桁分は2進数だと4桁分になります。以下に表を載せておきます。

2進数 10進数 16進数
0 0 0
1 1 1
10 2 2
11 3 3
100 4 4
101 5 5
110 6 6
111 7 7
1000 8 8
1001 9 9
1010 10 A
1011 11 B
1100 12 C
1101 13 D
1110 14 E
1111 15 F

16進数である3 0 6 Dをそれぞれ2進数に変換します。

#    3    0    6    D
  0011 0000 0110 1101

これらを合わせて 0011000001101101が2進数表記となります。

② U+306DはU+0800以上U+FFFF以下ですので、以下のリンクの表より3バイトの範囲ということがわかります。

このように変換した16桁の数字を1110xxxx 10xxxxxx 10xxxxxxの16箇所にあるxへ入れます(これは3バイト文字の先頭は1110スタート、2バイト目以降は10スタートのように分けているのですが、初めてこの仕組みを知った時ものすごく感心しました)。

1110 0011 1000 0001 1010 1101

先ほどとは逆に、2進数を16進数へ変換します。

# 1110 0011 1000 0001 1010 1101
     E    3    8    1    A    D

できました!"ね"をUTF-8で表すとe381adになることがわかりました。これはencode()を用いた変換結果と同じになります。 同様に"こ"を変換するとe38193となることもわかります(割愛しますのでぜひみなさんやってみてください)。

結果

"ねこ"のUTF-8表記で結果であるb'\xe3\x81\xad\xe3\x81\x93'がきちんと文字コードとして出力されていることがわかりましたね。 このように、文字コードを出力した形式がbytes型となります。 bytes型について、なんとなく理解できたでしょうか。初学者の学習のきっかけになれたら嬉しいです。

補足①bytes型をprintする際の注意

ローマ字表記の"neko"をbytes型に変換すると少しややこしいことになります。

str_word_2 = "neko"
encorded_str_word_2 = str_word_2.encode()

print(f"{str_word_2}: {encorded_str_word_2}")
print(f"type: {type(encorded_str_word_2)}")

↓ 結果

neko: b'neko'
type: <class 'bytes'>

"neko"の変換結果が少し変です。b'はついていますが、出力文字自体はそのままで変わっているようには見えません。
どうやら、pythonは1バイト文字でしたら出力時に自動で文字に変換してくれるようです。UTF-8は文字によってバイト数が異なります。先ほどの"ね"や"こ"は3バイト文字ですが、"neko"は4文字とも1バイト文字であるため、Pythonが4文字とも"neko"のまま出力してくれているということです。これについては細かい仕様について調べてもあまりわからなかったので、これから調べていこうと思います。

実験した結果も載せておきます。1バイト文字をstr型、bytes型で出力しています。

byte_1_str = "\x6e"
byte_1_bytes = b'\x6e'

print(byte_1_str)
print(f"type: {type(byte_1_str)}")

print(byte_1_bytes)
print(f"type: {type(byte_1_bytes)}")

↓ 結果

n
type: <class 'str'>
b'n'
type: <class 'bytes'>

bytes型に限らず、str型でも自動で変換してくれています。
どうしてもbytes型の中身が気になる!という方はhex()を用いましょう。bytes型を16進数のままで返してくれます。

str_word_2 = "neko"
encorded_str_word_2 = str_word_2.encode().hex("-")

print(f"{str_word_2}: {encorded_str_word_2}")

以下の結果が得られます。

neko: 6e-65-6b-6f

補足②bytes型をstr型に変換してみる

bytes型に変換した文字列は、decodeメソッドでstr型に戻すことができます。

str_word_1 = "ねこ"
encorded_str_word_1 = str_word_1.encode()
decoded_bytes_word_1 = encorded_str_word_1.decode()

print(f"{encorded_str_word_1}: {decoded_bytes_word_1}")
print(f"type: {type(decoded_bytes_word_1)}")

↓ 結果

b'\xe3\x81\xad\xe3\x81\x93': ねこ
type: <class 'str'>

最後に

bytes型のさわりの部分だけ説明しましたが、書いていると結構長くなってしまいました。きちんと説明するとUnicode、UTF-8、ASCIIは避けて通れませんが、なるべくわかりやすくするために割愛しております。もっとちゃんと知りたい!という方は是非調べてみてください。 私もまだまだ学習中でわからないことが多々ありますので、色々と調べたらまた続きを書こうと思います。

以上です。ここまでお読みいただきありがとうございました。

引用・参照・参考まとめ