これはなに
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_str
def to_bytes(bytes_or_str: bytes | str):
if isinstance(bytes_or_str, str):
return bytes_or_str.encode("utf-8")
return bytes_or_str
bytes
と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()