これはなに

Effective Python 第2版、項目3「bytesとstrの違いを知っておく」のまとめ。
ポイント

bytesとstrはまったく異なるため、同時に使わないようにしよう- ヘルパー関数を使って、入力が期待する文字列型か確認しよう
- ファイルにUnicodeデータを読み書きするときは、
open時にencodingパラメータを明示的に指定しよう
bytesとstrはまったく異なる

Python3には、文字列データを表すのにbytesとstrの2種類がある。ただし、bytesはバイナリデータ、strはテキストデータであるため、この2つはまったく異なる。
bytes

bytesはバイト型であり、生の符号なし8ビット値からなる。通常はASCIIエンコーディングで表示される。
bytesのリテラルはbプレフィックスを伴うクオーテーションで定義できる。
a = b"hello"
print(a) # b'hello'
print(list(a)) # [104, 101, 108, 108, 111] (ASCIIコード)bytesはバイナリのため、バイト文字列を扱える。
b = b"\x65" # 16進数で表されたバイト 16進数の65は、10進数で101に等しい
print(b) # b'e' ASCIIコードの101は文字eに対応している
print(list(b)) # [101]bytesはテキストエンコーディングを持たない。そのため、これをテキストデータとして扱うには、bytesのdecodeメソッドを呼び出さなければならない。
a = b"hello"
c = a.decode()
print(c) # hello
print(type(c)) # <class 'str'>str

strは文字列型であり、テキスト文字を表すUnicodeコードポイントを含む。
a = "hello"
print(a) # hello
print(list(a)) # ['h', 'e', 'l', 'l', 'o']Unicodeデータのため、Unicode文字列を扱える。
b = "a\u0300"
print(b) # à
print(list(b)) # ['a', '̀']strはバイナリエンコーディングを持たない。そのため、これをバイナリデータとして扱うには、strのencodeメソッドを呼び出さなければならない。
a = "hello"
c = a.encode()
print(c) # b'hello'
print(type(c)) # <class 'bytes'>文字列データの扱い方

Pythonプログラムの核心部分ではUnicodeデータのstrを利用し、文字の符号化に関して一切の仮定をしてはならない。そうすることで、他の文字符号化を受け入れつつ、出力文字の符号化1を統一できる。
核心部分でstrを利用するために、Pythonプログラムを書くときは、インターフェースのもっとも遠い箇所でUnicodeの符号化と複合化をすることが重要である。この方式はUnicodeサンドイッチと呼ばれる。
文字列データを扱うためのヘルパー関数

Pythonでは文字列型が2種類あるため、次の2つの状況がよく起こる。
- 特定の符号化1文字の生の8ビット値を操作する
- 符号化を指定しないUnicode文字列を操作する
本書では、この2つの文字型を変換して、入力値の種類がコードの期待するものとなるようにする2つのヘルパー関数が示されている。
def to_str(bytes_or_str: bytes | str):
if isinstance(bytes_or_str, bytes):
return bytes_or_str.decode("utf-8")
return bytes_or_strdef to_bytes(bytes_or_str: bytes | str):
if isinstance(bytes_or_str, str):
return bytes_or_str.encode("utf-8")
return bytes_or_strbytesとstrを扱うときの注意点

Pythonでbytes(生の8ビット文字列)とstr(Unicode文字)を扱うときに、理解しておくべき大事なことが2つある。
bytesとstrを一緒に使えるとは限らない- ファイルハンドルに絡む操作では、デフォルトでUnicode文字列が必要である
bytesとstrを一緒に使えるとは限らない

bytesとstrは同じように動作するが、一緒に使えるとは限らない。たとえば、bytes同士やstr同士は+演算子で結合できるが、strとbytesは結合できない。
print(b"one" + b"two") # できる
print("one" + "two") # できる
print("one" + b"two") # できない(TypeError: can only concatenate str (not "bytes") to str)
print(b"one" + "two") # できない(TypeError: can't concat str to bytes)同様に、二項演算子による大小比較も、bytesとstrでは比較できない。
print(b"one" > b"two") # できる
print("one" > "two") # できる
print(b"one" > "two") # できない(TypeError: '>' not supported between instances of 'bytes' and 'str')
print("one" > b"two") # できない(TypeError: '>' not supported between instances of 'str' and 'bytes')==により等しいかどうか比較すると、bytesとstrでは、同じ文字列であっても評価値がFalseになる。
print(b"foo" == "foo") # Falseフォーマット文字列における%演算子も、bytesのフォーマット文字列にstrを渡すとエラーになる。これは、Pythonがどのバイナリテキスト符号化を使うべきかわからないためである。
print(b"red %s" % "blue")
# TypeError: %b requires a bytes-like object, or an object that implements __bytes__, not 'str'また、strのフォーマット文字列にbytesを渡すと、エラーにはならないが期待する結果にもならない。これは、bytesインスタンスが__repr__メソッドを呼び出し、その結果で%sを置き換えるためである。
print("red %s" % b"blue") # red b'blue'
# このとき%sに入る文字列は、`repr`関数で得られる結果に等しい
print(repr(b"blue")) # b'blue'ファイルハンドルに絡む操作では、デフォルトでUnicode文字列が必要である

Pythonの組み込み関数openは、テキストモードの場合Unicode文字列が必要である。
たとえば、バイナリデータをファイルに書き込む場合、下記のコードはエラーになる。
with open("data.bin", "w") as f:
f.write(b"\xf1\xf2\xf3\xf4\xf5")
# TypeError: write() argument must be str, not bytesこれは、ファイルがテキスト書き込みモード"w"で開かれたためである。テキストモードで開かれたファイルのwriteメソッドは、Unicodeデータであるstrインスタンスを期待する。bytesデータを書き込みたい場合は、オープンモードを"wb"にする必要がある。
- with open("data.bin", "w") as f:
+ with open("data.bin", "wb") as f:
f.write(b"\xf1\xf2\xf3\xf4\xf5")
これは読み込みの場合も同じである。ファイルをテキスト読み込みモード"r"で開くと、encoding引数を明示的に与えていない場合、システムのデフォルトのテキスト符号化でデータを解釈する。ほとんどのシステムではデフォルトの符号化はUTF-8であるため、バイナリデータを扱えず、エラーが発生する。
with open("data.bin", "r") as f:
data = f.read()
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1 in position 0: invalid continuation byteそのため、バイナリデータを読み込む場合は、オープンモードを"rb"にする必要がある。
- with open("data.bin", "r") as f:
+ with open("data.bin", "rb") as f:
data = f.read()
open時にencodingを明示的に指定する

open関数はencoding引数を明示的に与えていない場合、システムのデフォルトのテキスト符号化でデータを解釈する。これはプラットフォーム依存である。
システムのデフォルトのエンコーディングは、locale.getencoding()で取得できる。
import locale
print(locale.getencoding()) # UTF-8プラットフォーム依存の振る舞いをなくすために、open関数でencodingパラメータを指定するとよい。こうすることで、期待する符号化と異なる符号化でファイルが開かれることを防げる。
with open("data.bin", "w", encoding="utf-8") as f:
f.write("hello")
with open("data.bin", "r", encoding="utf-8") as f:
data = f.read()