ファイルの読み込みと書き込み
`with open` での読み書き、r/w/a/x の違い、各行の末尾の改行、なぜ `encoding` を毎回書くのか、小さな作業には pathlib。
どうやって学びますか?
解決する問題
22 章にわたって作ってきたものは、プログラムが終了した瞬間にすべて消えてしまいます。
basket = []
basket.append("pen")
print(basket)['pen']もう一度実行すると、basket はまた空になります。リスト、辞書、関数 — すべてはメモリの中にあって、メモリの寿命はプログラムの寿命とちょうど同じです。
前の章では、プログラムとプログラムの間でコードを共有することを扱いました。この章はデータの話です — 1 回の実行が、その作業を次の実行、あるい別の誰かのために残す方法です。
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("first line\n")
fh.write("second line\n")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(fh.read(), end="")first line
second lineプログラムは終了しましたが、notes.txt はまだそこに残っています。
この章を終える頃には
with open(...)でファイルを読み書きできるようになり、なぜwithなのかを言える"r"、"w"、"a"、"x"の違いを言える — 特に"w"が何を消すか- ファイルを 1 行ずつ読めるようになり、各行の末尾の
\nを扱える - なぜ毎回
encoding="utf-8"を書くのかを言える - 小さな作業には
pathlib.Pathを使える FileNotFoundErrorとUnicodeDecodeErrorを読み解ける
前提条件: モジュールとインポート.
なぜ with なのか
ファイルを開くのは、オペレーティングシステムから何かを借りることです。そして借りたものは返さなければなりません。閉じないままだと、書いた内容がディスクに届かないことがあります。閉じないファイルが増えると、プログラムは止まってしまいます。
with ブロックはその返却を自動でやってくれます — ブロックが終わればファイルは閉じられます。中で何かが例外を投げた場合でも同じです。
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(fh.closed)
print(fh.read())True
ValueError: I/O operation on closed file.(エラー行の前にも Python はトレースバックの行をいくつか表示しますが、ここでは最後の行だけを示しています。)
ブロックの後も名前 fh は残っています — スコープの働きはこれまでと全く同じです — ただしそのファイルは閉じられています。この講座では open() をいつでも with と合わせて書きます。
読み込み
内容を一度にまるごと受け取れるのが .read() です:
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(repr(text))
print(len(text))'first line\nsecond line\n'
23repr をわざわざ使っているのは、ふつうの print では改行が隠れてしまうからです。ファイルは 1 本の長いテキストで、行末はその内側に \n として入っています — それを覚えることが、この章の半分です。
1 行ずつ読む
大きなファイルに .read() は向いていません — まるごとをメモリに持ち込んでしまうからです。ファイルそのものをループさせると、Python は 1 行ずつ読みます:
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))'pen,15.0,3\n'
'bag,850.0,1\n'
'ink,120.0,2\n'どの行も \n で終わります。これがいちばんよくある混乱の元です — line == "pen" は一致しませんし、"pen\n" と "pen" は別の辞書キーになります。しかもその理由は見えません。(int() と float() は数字の周りの空白と \n を自分では無視するので、int("3\n") は確かに 3 を返します — しかし文字列はそのままの形で保持されるため、\n は比較にも表示にも残ります。)
だから最初の一手は、ほぼ必ず .strip() です:
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
name, price, qty = line.strip().split(",")
print(f"{name:<5} {float(price) * int(qty):>8.2f}")pen 45.00
bag 850.00
ink 240.00.strip()、次に .split(",")、そして float() と int() — この順番はファイルを読むたびに出てきます。ファイルから出てくるものはすべてテキストで、決して数字ではありません。第7章の変換がここでも必要です。
すべての行を一度に欲しいときは、方法が 2 つあります:
with open("orders.txt", "r", encoding="utf-8") as fh:
lines = fh.read().splitlines()
print(lines)['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2'].splitlines() は行末を取り除きます。.readlines() もありますが、こちらは \n を残します — そこが多くの人がつまずくところです。
書き込みと、危険な 1 文字
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("new\n")以前は old content が入っていました。今は:
new"w" はファイルを空にします — 開いた瞬間に、まだ何も書かれる前です。警告はありませんし、取り返しもつきません。
今の内容を残したまま末尾に追加したいときは、"a":
with open("notes.txt", "a", encoding="utf-8") as fh:
fh.write("new\n")old content
newそして、すでにファイルがあるならまったく書き込みたくないときは、"x":
FileExistsError: [Errno 17] File exists: 'notes.txt'4 つのモードを 1 行で:"r" は読み、"w" は消して書き、"a" は末尾に追加し、"x" は作るだけ。
"w"を打つ前に一呼吸おいてください。 この 1 文字で、本物の仕事を失う人がいます。開こうとしているファイルが自分のプログラムのものでないなら、"w"を書く前に考えてください。
\n は自分で書く
print は改行を自動でやってくれます。write はしません。
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)'penbag'だから行のリストを書く最善の方法はこうです:
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
fh.write("\n".join(rows) + "\n")'pen\nbag\n'"\n".join(rows) は改行を行と行の間に置き、末尾の + "\n" がファイルを改行で終わらせます — テキストファイルの通常のルールであり、多くのツールが期待する形です。
encoding="utf-8" — 常に書く
encoding を書かないと、Python はオペレーティングシステムのデフォルトを使います。それはマシンごとに違います。結果、自分のいる場所では動くのに、別の誰かのところでは壊れるプログラムができます。
間違ったもので読むと:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byteだから open() を書くときは、毎回 encoding="utf-8" を書いてください — 読みも書きも同じです。例外はバイナリモード ("rb"、"wb")だけで、エンコーディングというものが存在しないので、そもそもこの問題は起きません。
pathlib — 小さな作業への近道
小さなファイルなら、with ブロック一式はちょっと重いと感じることがあります。
from pathlib import Path
path = Path("orders.txt")
print(path.exists())
print(len(path.read_text(encoding="utf-8").splitlines()))
out = Path("report.txt")
out.write_text("done\n", encoding="utf-8")
print(out.read_text(encoding="utf-8"), end="")True
3
doneread_text と write_text は内部で開いて閉じるので、with は要りません。Path にはほかにも便利なものが — .exists()、.name、.suffix、.parent — があり、フォルダーのパスは / でつなげます — Path("data") / "orders.txt" のように。
どっちをいつ使うか? ファイルを一度にまるごと欲しいときは Path、行単位で読むときは with open(...)。 ただ 1 点だけ覚えておいてください:write_text はいつでも "w" と同じ振る舞いをします — ファイルがすでにあるなら、警告なしに中身が消えます。追記する方法はなく、それには with open(..., "a") が必要です。
完全な例
orders.txt:
pen,15.0,3
bag,850.0,1
ink,120.0,2main.py:
"""Read an order file, total it, and write a report beside it."""
from pathlib import Path
TAX_RATE = 0.15
def read_order(path):
"""Each line is name,price,quantity. Blank lines are skipped."""
lines = []
for raw in path.read_text(encoding="utf-8").splitlines():
if not raw.strip():
continue
name, price, quantity = raw.split(",")
lines.append((name, float(price), int(quantity)))
return lines
def report(lines):
rows = []
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
rows.append(f"{name:<6} {quantity:>3} {amount:>9.2f}")
rows.append("-" * 20)
rows.append(f"{'total':<6} {'':>3} {total:>9.2f}")
return rows
def main():
order = read_order(Path("orders.txt"))
rows = report(order)
Path("report.txt").write_text("\n".join(rows) + "\n", encoding="utf-8")
print(Path("report.txt").read_text(encoding="utf-8"), end="")
print()
print("lines read:", len(order))
if __name__ == "__main__":
main()pen 3 51.75
bag 1 977.50
ink 2 276.00
--------------------
total 1305.25
lines read: 3見る価値のある 4 点です。
3 つの関数が、それぞれ別の仕事をしています。 read_order はディスクから読み、report は計算だけで、main は書きます。report にはファイルをまったく渡しません — リストを渡してリストを返します。だからテストにファイルは不要で、第28章でそれがまさに役に立つことになります。
空行は先に処理しています。 ファイル末尾に \n が 1 つあるだけでは空行にはなりません — .splitlines() がそれを落とすからです。しかし手で編集したファイルでは、途中や末尾に本物の空行 (\n が 2 つ続く)が入るのはまったく普通のことです。扱わないと raw.split(",") は 3 つに分かれず、ValueError を投げます。if not raw.strip(): continue が 1 行でそれを片付けます。
float(price) と int(quantity) は省けません — ファイルから出るのはすべてテキストで届くからです。忘れると price * quantity は文字列を黙って繰り返すだけになります — 一番悪い種類のバグ、壊れない種類のバグです。
レポートは別のファイルに書きます。 orders.txt を "w" で開くことは決してありません — 入力ファイルはそのまま保たれます。これはぜひ身につけたい習慣です:読んでいるものに書き込まない。
うまくいかないとき
FileNotFoundError: [Errno 2] No such file or directory: 'orders.txt' ファイルがないか、違うフォルダーからプログラムを実行しています。パスは実行した場所から決まり、スクリプトのあるフォルダーではありません。Path("orders.txt").resolve() を表示すると、どこを見ているか分かります。
UnicodeDecodeError: 'utf-8' codec can't decode byte ... 別のエンコーディングで書かれたファイルか、そもそもテキストファイルではありません。encoding を直すか、バイナリなら "rb" で開いてください。
ValueError: I/O operation on closed file. with ブロックの外でファイルを使いました。必要な読み込みはブロックの中で済ませましょう。
ValueError: not enough values to unpack カンマが期待どおりにない行があります — たいてい空行です。空行は飛ばし、迷ったら print(repr(raw)) を使いましょう。
ファイルを書いたのに、空だった with ブロックが終わる前に読み取られています。書き込みがディスクに届くのは、ファイルを閉じたときです。
行が全部 1 行につながってしまった write は行を分けません。"\n" は自分で書くものです。
ファイルの中身が全部消えた "w" で開いてしまいました。読むなら "r"、末尾に追加するなら "a" を使いましょう。
ステップ 4 / 6 — 予想する
理解度をチェック
repr が使われているので、何も隠れません。何が表示されるでしょう?
# orders.txt
# pen,15.0,3
# bag,850.0,1
# ink,120.0,2
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))- A'pen,15.0,3' 'bag,850.0,1' 'ink,120.0,2'
- B'pen,15.0,3\nbag,850.0,1\nink,120.0,2\n'
- C'pen,15.0,3\n' 'bag,850.0,1\n' 'ink,120.0,2\n'
- D['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2']
ファイルを "w" で開きましたが、何も書きません。何が表示されるでしょう?
# notes.txt already contains: important work
with open("notes.txt", "w", encoding="utf-8") as fh:
print("opened")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- Aopened 'opened\n'
- BA `FileExistsError`
- Copened 'important work\n'
- Dopened ''
2つを別々に書きました。ファイルの中身はどうなりますか?
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)
with open("out.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- A'pen\nbag\n'
- B'pen\nbag'
- C'pen bag'
- D'penbag'
回答にはアカウントが必要です
ログインして答えを確認
問題は上にあります。頭の中で答えを考えることがいちばん大切です。ログインすると、正解・解説・3 段階のヒントを確認できます。
やってみよう
people.txt というファイルを作り、各行に名前と数字をカンマ区切りで入れます — 少なくとも 4 行書き、途中にわざと 1 行の空行を残してください。
次に summary.py を 3 つの関数で書きます:ファイルをペアのリストとして読む関数、そのリストをテキストの行に変える関数 (数字が最大なののは誰かと合計)、そしてそれを summary.txt に書く main です。if __name__ == "__main__": を忘れないでください。
それから 5 つの実験です:
.strip()を外し、各行の最後の部分をrepr()で表示します。int()はエラーを出しますか?(出ません —int("10\n")は10を返します。)では\nはどこに隠れていて、空行では何が起きるでしょう?- ある行から数字だけを消し、カンマはあるのに数字がない状態にします。メッセージを読みます。
mainの"w"を"a"に変え、プログラムを 3 回実行します。summary.txtはどうなりましたか?people.txtを"w"で開いてみる… やめてください。 コピーを 1 つ作り、そのコピーに対して同じことをし、何が残ったか見てください。Path("people.txt").resolve()を表示し、別のフォルダーからプログラムを実行します。パスは変わりますか?
最後の項目は、ほぼ誰もが一度は犯すミスの説明です:プログラムは動いたのに、別の場所から実行したら FileNotFoundError が出た、というものです。