CV・NLPハマりどころメモ

画像認識と自然言語処理を研究する中でうまくいかなかったことと、その対策をまとめる自分用メモが中心。

データをopenするときの第2引数 r+, w+, a+の違いって何?[Python]

導入

Pythonでテキストファイルを書き込むときには,ファイルをopenしてから内容を読んだり書き込んだりする.

# 例

with open("text.txt", mode="r") as f:
  text = f.read()

このとき,openの第2引数には読み込みを指定するモード "r" と,書き込みを指定するモード "w" と,上書きを指定するモード "a" が存在する.

さらに,"r+", "w+", "a+" という "+" が付いたモードも存在.

軽くグルると "+" が付いたモードは読み書きを行うためのモードであると紹介されていた.しかし,読み込んで書き込むのであれば,"r+" も "w+" も "a+" も同じ意味のように感じる.これらをあえて使い分けてる理由って一体何なのだろうか?

気になって調べてみたところ,以下のことが分かったのでまとめる.

モード 意味
r+ 既存のデータを読み込んだ後,データの先頭から書き込みを行うモード.
w+ 既存のデータを読み込んだ後,データの内容を全て消去し,新たな書き込みを行うモード.
a+ 既存のデータを読み込んだ後,データの末尾から書き込みを行うモード.

表だけでは分かりにくいので,ケースバイケースで見ていこう.

"r+" と "w+" と "a+" における挙動の違い

"r+" の場合

仮に以下のデータがあったとする.

# text.txt
123456

このデータ(text.txt)において "r+" を使うと,既存のデータを読み込んだ後,データの先頭から書き込みを行う.

# mode r+
with open("text.txt", "r+") as f:
  f.write("abc")

# 結果
# データの先頭から書き込みが開始したため,過去のデータの先頭から3文字が消えている.
text.txt >> abc456

"w+" の場合

次に,"w+" を使うと既存のデータを読み込んだ後,データの内容を全て消去し,新たな書き込みを行う.

たとえ,with構文内でファイルの書き込みを行わなかったとしても,openした時点で内容が消去される.このため実際に使う際には注意が必要だ.

# mode w+ 
with open("text.txt", "w+") as f:
  f.write("abc")

# 結果
# データを一旦消去してから書き込みを行うため,過去のデータが全て初期化され,新たなデータしか記録されない.
text.txt >> abc

"a+" の場合

最後に,"a+" を使うと既存のデータを読み込んだ後,データの末尾から書き込みを行う.先頭から書き込みを行う "r+" とは異なり,末尾から新たなデータが書き込まれる.

# mode w+ 
with open("text.txt", "a+") as f:
  f.write("abc")

# 結果
# データの末尾から書き込みが行われるため,"a+" は "r+" と違って,過去のデータが破壊されなかった.
text.txt >> 123456abc

発展的な内容

ここまで読めば,"r+" と "w+" と "a+" の挙動の違いは把握できたはずだ.では,なぜこのような違いが発生するのかを別の観点から見ていこう.

Openした際のポインタ位置の違い

"r+" と "w+" と "a+" における書き込みの挙動の違いは,openした際のポインタの位置を比べることで把握することもできる.

例えば以下のコマンドで,3種のモードを試した場合,

with open("text.txt", mode) as f:
  print(f.tell()) #  f.tell() はファイルオブジェクトのファイル中における現在の位置を示す整数を返す.

上のコマンドの出力(ポインタの位置)は,

r+ : 0

w+ : 0

a+ : 6

になる.

w+ は一旦データを消去するから当然ポインタの位置は先頭(0)になっている.

r+ のポインタは先頭(0)を指している.このため,先頭からデータを書き込まれると解釈することができる.

a+ のポインタの位置は末尾(6)を指している.このため,末尾からデータの書き込まれると解釈することができる.

f.read() した際の違い

"r+" と "w+" と "a+" における書き込みの挙動の違いは,f.read()を実行した際の挙動を比べることで把握することもできる.

前述のポインタの位置の違いを念頭に置けば当たり前の現象になるのだが,一応検証しておく.

例えば以下のコマンドで,3種のモードを試した場合,

with open("text.txt", mode) as f:
  print(f.read())

上のコマンドの出力は,

r+ : 123456

w+ : 表示なし

a+ : 表示なし

になる.

r+ のポインタは先頭を指しているのに対し,w+は一旦データを消去した後に先頭を指すため表示なし,また,a+のポインタは末尾を指しており当然のことながら末尾以降にデータは存在しないため表示なしとなる.

f.read() を行うとポインタはデータの末尾を指す

以下のように,f.read() をする前と後に f.tell() をはさんでポインタの位置を確かめてみたときにこの現象は起こった.

with open("text.txt", mode) as f:
        print(f.tell()) # openした直後にポインタの位置を確かめる.
        f.read()
        print(f.tell()) # f.read() した後にポインタの位置を確かめる.

各モードにおけるf.read()によるポインタの位置の変化.

r+ : 0 -> 6

w+ : 0 -> 0

a+ : 6 -> 6

r+ だけポインタの位置がずれている.どうやら f.read() を行うとポインタの位置がデータの末尾まで移動してしまうようだ.

ポインタの位置を考えずに "+" のモードを使っていると実際にコーディングする際,はまりそうだ.気を付けよう.

ちなみに,f.write() を行った際にもポインタはデータの末尾を指す.

# 例

with open("text.txt", "r+") as f:
        print(f.tell())
        f.read()
        print(f.tell())
        f.write("abc")
        print(f.tell())

r+ : 0 -> 6 -> 9