Python でファイルを開くとき、"r" と "rb" で何が違うのか。単に「改行コードの扱い」だけではない、内部動作の根本的な違いを解説する。
モードによる違いの概要
バイト列を文字列にデコード/エンコードする。改行コードの変換も行う
バイト列をそのまま扱う。変換は一切行わない
テキストモードの内部処理
テキストモードでファイルを読むと、以下の処理が自動的に行われる。
ディスクからバイト列を読み込む
エンコーディングに従って文字列にデコード
改行コードを統一(\r\n → \n)
Python の str オブジェクトとして返す
# テキストモード
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read() # str 型
print(type(content)) # <class 'str'>
バイナリモードの内部処理
バイナリモードでは変換が行われない。
ディスクからバイト列を読み込む
そのまま bytes オブジェクトとして返す
# バイナリモード
with open("data.txt", "rb") as f:
content = f.read() # bytes 型
print(type(content)) # <class 'bytes'>
改行コードの変換
テキストモードでは OS に応じた改行コードの変換が行われる。
\r\n(Windows)や \r(古い Mac)を \n に統一する
\n を OS のネイティブ改行コードに変換する(Windows では \r\n)
# Windows で作成されたファイル(\r\n 改行)
# テキストモード
with open("windows.txt", "r") as f:
content = f.read()
print(repr(content)) # 'line1\nline2\n'(\r が消える)
# バイナリモード
with open("windows.txt", "rb") as f:
content = f.read()
print(repr(content)) # b'line1\r\nline2\r\n'(そのまま)
この変換を無効にするには newline="" を指定する。
# 改行変換を無効にする
with open("data.txt", "r", newline="") as f:
content = f.read() # \r\n がそのまま残る
エンコーディングの自動検出
テキストモードではエンコーディングを指定しないと、システムのデフォルトエンコーディングが使われる。
import locale
# デフォルトエンコーディングを確認
print(locale.getpreferredencoding()) # UTF-8 など
# 明示的に指定するのがベストプラクティス
with open("data.txt", "r", encoding="utf-8") as f:
content = f.read()
デフォルトは通常 UTF-8
デフォルトは CP932(Shift_JIS)などロケール依存。Python 3.15 から UTF-8 がデフォルトになる予定
バッファリングの違い
テキストモードとバイナリモードでバッファリングの動作も異なる。
# バイナリモードは buffering=0 が可能(バッファなし)
with open("data.bin", "rb", buffering=0) as f:
pass # OK
# テキストモードは buffering=0 不可
try:
with open("data.txt", "r", buffering=0) as f:
pass
except ValueError as e:
print(e) # can't have unbuffered text I/O
テキストモードでバッファなしにできないのは、エンコーディング変換がバッファを必要とするためだ。
io モジュールの構造
Python 3 の io モジュールは、階層的な構造を持っている。
OS レベルの低レベル I/O。バイト列をそのまま扱う。
FileIO をラップしてバッファリングを提供。
BufferedReader/Writer をラップしてエンコーディング変換と改行処理を提供。
import io
# バイナリモードの内部構造
with open("data.txt", "rb") as f:
print(type(f)) # <class '_io.BufferedReader'>
print(type(f.raw)) # <class '_io.FileIO'>
# テキストモードの内部構造
with open("data.txt", "r") as f:
print(type(f)) # <class '_io.TextIOWrapper'>
print(type(f.buffer)) # <class '_io.BufferedReader'>
print(type(f.buffer.raw)) # <class '_io.FileIO'>
seek() と tell() の違い
テキストモードでは seek() と tell() の動作が複雑になる。
# バイナリモードはバイト位置で動作
with open("data.txt", "rb") as f:
f.seek(10) # 10 バイト目に移動
pos = f.tell() # 10
# テキストモードはエンコーディング依存
with open("data.txt", "r", encoding="utf-8") as f:
# UTF-8 では 1 文字 = 1〜4 バイト
# seek(10) が 10 文字目とは限らない
f.seek(0) # 先頭への seek は常に安全
pos = f.tell() # 不透明な値(実装依存)
テキストモードでは seek(0) 以外の seek() は避けるべきだ。任意の位置に移動したいならバイナリモードを使う。
使い分けの指針
| 用途 | モード | 理由 |
|---|---|---|
| テキストファイル | テキスト | 文字列として扱える、改行が統一される |
| 画像・音声 | バイナリ | 変換されると壊れる |
| JSON・XML | テキスト | 文字列データとして解析する |
| ZIP・PDF | バイナリ | バイナリフォーマット |
| ログ出力 | テキスト | 人間が読む文字列 |
| ネットワークデータ | バイナリ | プロトコルに従う必要がある |
まとめ
エンコーディング変換、改行変換、str 型で扱う。人間が読むファイル向け。
変換なし、bytes 型で扱う。機械的なデータや正確なバイト操作が必要な場合向け。
エンコーディングを明示してテキストモードを使う。バイナリデータならバイナリモード。