Featured image of post bytesとstrを同時に使わない - Effective Python 項目3

bytesとstrを同時に使わない - Effective Python 項目3

これはなに Link to this heading

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

ポイント Link to this heading

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

bytesstrはまったく異なる Link to this heading

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

bytes Link to this heading

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はテキストエンコーディングを持たない。そのため、これをテキストデータとして扱うには、bytesdecodeメソッドを呼び出さなければならない。

a = b"hello"
c = a.decode()
print(c)  # hello
print(type(c))  # <class 'str'>

str Link to this heading

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はバイナリエンコーディングを持たない。そのため、これをバイナリデータとして扱うには、strencodeメソッドを呼び出さなければならない。

a = "hello"
c = a.encode()
print(c)  # b'hello'
print(type(c))  # <class 'bytes'>

文字列データの扱い方 Link to this heading

Pythonプログラムの核心部分ではUnicodeデータのstrを利用し、文字の符号化に関して一切の仮定をしてはならない。そうすることで、他の文字符号化を受け入れつつ、出力文字の符号化1を統一できる。

核心部分でstrを利用するために、Pythonプログラムを書くときは、インターフェースのもっとも遠い箇所でUnicodeの符号化と複合化をすることが重要である。この方式はUnicodeサンドイッチと呼ばれる。

文字列データを扱うためのヘルパー関数 Link to this heading

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

bytesstrを扱うときの注意点 Link to this heading

Pythonでbytes(生の8ビット文字列)とstr(Unicode文字)を扱うときに、理解しておくべき大事なことが2つある。

  1. bytesstrを一緒に使えるとは限らない
  2. ファイルハンドルに絡む操作では、デフォルトでUnicode文字列が必要である

bytesstrを一緒に使えるとは限らない Link to this heading

bytesstrは同じように動作するが、一緒に使えるとは限らない。たとえば、bytes同士やstr同士は+演算子で結合できるが、strbytesは結合できない。

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)

同様に、二項演算子による大小比較も、bytesstrでは比較できない。

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')

==により等しいかどうか比較すると、bytesstrでは、同じ文字列であっても評価値が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文字列が必要である Link to this heading

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を明示的に指定する Link to this heading

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()

参考文献・URL Link to this heading

Effective Python 第2版's image

Effective Python 第2版

www.oreilly.co.jp
組み込み関数's image

組み込み関数

Python インタプリタには数多くの関数と型が組み込まれており、いつでも利用できます。それらをここにアルファベット順に挙げます。,,,, 組み込み関数,,, A, abs(), aiter(), all(), anext(), any(), ascii(),, B, bin(), bool(), breakpoint(), bytearray(), bytes(),, C, callabl...

docs.python.org

  1. 理想はUTF-8。 ↩︎ ↩︎

Licensed under CC BY-NC-SA 4.0
最終更新 10月 07, 2023
Hugo で構築されています。
テーマ StackJimmy によって設計されています。