CHAPTER 7 ・ 品質を守る / Lesson 29

安全に動かす習慣

本番の前に確認する dry-run、元データを守るバックアップ、何度動かしても同じ結果になる作り方です。

目安 20分 ・ インストール不要

このレッスンが終わったら

「実行の前に、何が起きるかを表示する。元ファイルは書き換えず、同じ処理を2回動かしても困らない作りにする」と言える。

STEP 1

動かしてから、気づいた

invoices フォルダに、請求書のファイルが3つあります(invoice_tanaka.txt など)。これを「2025-10_ から始まる名前」にそろえよう、というスクリプトを書きました。うっかり、新しい名前に人の名前を入れ忘れた版が、次のコードです。実行すると、どうなるでしょうか。

新しい名前に、人の名前を入れ忘れた(取り返しがつかない例)サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

3つあった請求書が、1つになりました。3つとも同じ名前(2025-10.txt)に変えようとして、あとから来たファイルが、先のファイルを上書きしたためです。佐藤さんと鈴木さんの請求書は、もうありません。しかも、エラーは1つも出ませんでした。(Windows では同じ名前があるとエラーで止まりますが、Mac などでは、このように黙って上書きされます。)

自動化したスクリプトは、人が手でやるより速く、大量に、確認なしに動きます。だから、間違えたときの被害も、大きくなります。この回では、本番で動かす前の安全の習慣を身につけます。

STEP 2

まず「表示するだけ」で動かす

最初の習慣は、ドライランです。実際には何も変えず、「何をするつもりか」だけを表示します。同じスクリプトを、安全な作りに書き直しました。

ドライランつきの名前変更(まだ何も変えない)サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

「旧名 -> 新名」の一覧が出て、最後に表示したフォルダの中身は、元のままです。一覧を目で確かめ、問題がなければ、DRY_RUN = True を False に書き換えて、もう一度実行してみましょう。名前が変わり、バックアップの件数が表示されます。

STEP 3

仕組みを読む

安全な手順は、次の流れです。本番(実際に変える)が、最後から2番目になっているところが、ポイントです。

  1. 対象を一覧にする件数と名前を表示
  2. チェックする重複・既存を点検
  3. ドライラン表示だけ。目で確認
  4. バックアップ→本番コピーを取ってから変更
  5. 検算する件数・合計が合うか
安全に動かす流れ。変更は、確認を終えたあとに行う
先ほどのスクリプトの、安全のための行
DRY_RUN = True「表示するだけ」のスイッチ。まずは True で動かします
plan = [(旧, 新), ...]変更の計画を先に作ります。この時点では、ファイルに触れません
assert len(new_names) == len(set(new_names))新しい名前に重複がないか確かめます(set は Lesson 07、assert は Lesson 28 で学んだ通りです)
assert not any(new.exists() ...)同じ名前のファイルが、すでにないか確かめます
if not DRY_RUN:本番のときだけ、この中を実行します
shutil.copytree(folder, "invoices_backup")変更の前に、フォルダごとコピーして取っておきます

日本語にすると

「計画を作って見せる。問題がなければ、まずコピーを取り、それから変更する」
STEP 4

変えて、試す

① 既存のファイルを、上書きしない。 ファイルを開くときの指定(モード)が、"w" だと、同じ名前のファイルを黙って上書きします。"x" にすると、すでにあるときは FileExistsError で止まります。また shutil.copy は、ファイルをコピーする命令で、コピーに対して作業すれば、元のデータは守られます。

わざと間違える:すでにあるファイルを x で開くサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

2回目で FileExistsError と出て、1回目の report.txt は無事です。「消してはいけないものは、"x" で守る」と覚えておきましょう。

② 何度動かしても、同じ結果にする。 同じスクリプトを、うっかり2回動かすことは、実務でよくあります。2回動かしても結果が変わらないことを、冪等性(べきとうせい)と呼びます。ファイルに追記する("a")と、2回で内容が倍になります。毎回、上書きして作り直す("w")なら、何回でも同じです。

追記と上書きの違い、フォルダ作成コードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

追記は2行、上書きは1行です。フォルダを作る mkdir も、exist_ok=True と付ければ、2回目でもエラーになりません。名前変更のスクリプトも、1回目で全部変えてしまえば、2回目は対象が0件になるだけで、何も壊れません。

③ 結果を検算し、後片付けを確実にする。 処理の最後に、元データの合計と、出力の合計が一致するかを確かめます。また、途中で作った作業用ファイルは、エラーが起きても消えるよう finally(Lesson 09)に書きます(unlink は、ファイルを削除する命令です)。

検算と、finally での後片付けサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます
STEP 5

自分で書く

お題① 仮ファイルを消す関数 delete_files を、ドライラン対応にしてください。dry_run が True のときは「削除予定: 名前」と表示するだけにします。False のときは、Path(name).unlink() で削除します。最後に、残っている .tmp ファイルの数を表示します。

お題①:ドライラン対応の削除コードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

for の中で if dry_run: と書き、print(f"削除予定: {name}") を実行します。else: に Path(name).unlink() を書きます。呼び出しは dry_run を省略しているので、初期値の True で動きます。

答えの例を見る
from pathlib import Path

for name in ["a.tmp", "b.tmp"]:
  Path(name).write_text("仮ファイル", encoding="utf-8")

def delete_files(names, dry_run=True):
  for name in names:
      if dry_run:
          print(f"削除予定: {name}")
      else:
          Path(name).unlink()

delete_files(["a.tmp", "b.tmp"])
print(len(list(Path(".").glob("*.tmp"))))

お題② 次の write_summary は、2回動かすと、ファイルに同じ行が2つ入ります。何回動かしても1行になるように直してください。

お題②:何度動かしても同じ結果にコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

ファイルを開くときのモードを、追記の "a" から、毎回作り直す "w" に変えます。

答えの例を見る
from pathlib import Path

def write_summary(total):
  with open("summary.txt", "w", encoding="utf-8") as f:
      f.write(f"合計 {total}\n")

write_summary(100)
write_summary(100)
print(len(Path("summary.txt").read_text(encoding="utf-8").splitlines()))

お題③ save_report は、すでに report.txt があっても上書きしてしまいます。"x" モードで開き、すでにあるときは FileExistsError を except で受けて、「すでにあります。上書きしません」と表示するようにしてください。

お題③:上書きしない保存コードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

open の "w" を "x" に変え、try: の中に with ... と print("作成しました") を入れます。except FileExistsError: で、メッセージを表示します。

答えの例を見る
def save_report(text):
  try:
      with open("report.txt", "x", encoding="utf-8") as f:
          f.write(text)
      print("作成しました")
  except FileExistsError:
      print("すでにあります。上書きしません")

save_report("9月分")
save_report("9月分(やり直し)")
STEP 6

仕事ならどう使う?

理解チェックドライラン(DRY_RUN = True)の目的は?

理解チェック月次の集計ファイルを作るスクリプトを、うっかり2回動かしました。結果が変わらず安心なのは、どの作り方でしょう?