例外処理の書き方の基本

はじめに

exceptのeに例外が入る。

ということは、最初に頭に思い浮かんだ。

京都駅から程近い三十三間堂にて。

ちなみに、木村拓哉さんがYouTubeで三十三間堂についてアップしていた。

本題に入ろう。

ビンゴゲームを使用していく。

Ctrl + D、または、Ctrl + Cで、例外を発生させる(私はmacユーザーです)

以下の赤字部分に注目していただきたい。

import random
import logging

B_RANGES = [(1, 15), (16, 30), (31, 45), (46, 60), (61, 75)]
CELL_W = 4

def generate_card():
  cols = [random.sample(range(lo, hi + 1), 5) for lo, hi in B_RANGES]
  card = [[cols[c][r] for c in range(5)] for r in range(5)]
  card[2][2] = "FR"
  return card

def print_card(card, marked=None):
  header = "".join(f"{ch:^{CELL_W}}" for ch in "BINGO")
  print(header)

  for r in range(5):
    cells = []
    for c in range(5):
      v = card[r][c]
      if v == "FR":
        s = "[FR]"
      elif marked and marked[r][c]:
        s = f"[{str(v).rjust(2)}]"  
      else:
        s = f"{str(v).rjust(2)}"

      cells.append(f"{s:^{CELL_W}}")

    print("".join(cells))

def new_marked():
  m = [[False]*5 for _ in range(5)]
  m[2][2] = True # FREEは最初からマーク
  return m

def mark_number(card, marked, n):
  for r in range(5):
    for c in range(5):
      if card[r][c] == n:
        marked[r][c] = True
        return True
  return False

def count_lines(marked):
  lines = 0
  for r in range(5):
    if all(marked[r][c] for c in range(5)): lines += 1
  for c in range(5):
    if all(marked[r][c] for r in range(5)): lines += 1  
  if all(marked[i][i] for i in range(5)): lines += 1
  if all(marked[i][4-i] for i in range(5)): lines += 1  
  return lines

def main():
  logging.basicConfig(
    filename="bingo.log",
    level=logging.INFO, 
    format="%(asctime)s %(levelname)s:%(message)s",
    datefmt="%Y-%m-%d %H:%M:%S")
  logging.info("ビンゴゲームを開始します。")
  card = generate_card()
  marked = new_marked()
  pool = list(range(1, 76))
  random.shuffle(pool)
  history = []
  turn = 0

  print_card(card, marked)
  print("Enter=next  h=history  q=quit")

  while True:
    # Ctrl + D、または、Ctrl + Cで、例外を発生させる
    cmd = input("> ").strip().lower()

    # 入力まわりの例外だけをキャッチ
    # try:
    #   cmd = input("> ").strip().lower()
    # except (EOFError, KeyboardInterrupt):
    #   print("\nBye!")
    #   break

    # try:
    #     cmd = input("> ").strip().lower()
    # except Exception as e:
    #     print(type(e))   # 例外の型
    #     print(e)         # 例外メッセージ

    # コマンド分岐(進行しない分岐は必ず continue)
    if cmd in ("q", "quit", "exit"):
      print("Bye!")
      break

    elif cmd in ("h", "history"):
      print(f"Called: {', '.join(map(str, history)) or '(none)'}")
      continue

    elif cmd in ("", "n", "next"):
      # Enter(または n/next)のときだけ1ターン進行
      print("1ターン進行します。")
    else:
      print(f"無効なコマンドです: {cmd!r}  (Enter / h / q を使用)")
      continue

    # ここから1ターン進行(有効な進行コマンドのときだけ実行)
    if not pool:
      print("No numbers left. Draw game.")
      break

    n = pool.pop()
    history.append(n)
    turn += 1

    mark_number(card, marked, n)
    lines = count_lines(marked)

    print(f"\nTurn {turn}  Called: {n}  Remaining: {len(pool)}  Lines: {lines}")
    print_card(card, marked)

    if lines >= 1:
      print("\nBINGO! congrats!")
      break

if __name__ == "__main__":
  main()

これを実行する。キーボード入力の選択肢は3つ。

Ctrl + Dを押すと、EOFErrorが発生する。

EOFErrorとは、input関数は、文字列を受け取って返すけど、Ctrl + Dを受け取ると返す文字列がないということ。EOLとは、End Of Fileの略。パソコンは、キーボードからの入力もファイルの読み込みも同じ仕組みで扱っている。

Ctrl + Cを押すと、KeyboadInterruptエラーが発生する。

これは、文字通り、キーボードからの中断。

入力周りの例外をキャッチ

以下の赤文字部分に着目していただきたい。

import random
import logging

B_RANGES = [(1, 15), (16, 30), (31, 45), (46, 60), (61, 75)]
CELL_W = 4

def generate_card():
  cols = [random.sample(range(lo, hi + 1), 5) for lo, hi in B_RANGES]
  card = [[cols[c][r] for c in range(5)] for r in range(5)]
  card[2][2] = "FR"
  return card

def print_card(card, marked=None):
  header = "".join(f"{ch:^{CELL_W}}" for ch in "BINGO")
  print(header)

  for r in range(5):
    cells = []
    for c in range(5):
      v = card[r][c]
      if v == "FR":
        s = "[FR]"
      elif marked and marked[r][c]:
        s = f"[{str(v).rjust(2)}]"  
      else:
        s = f"{str(v).rjust(2)}"

      cells.append(f"{s:^{CELL_W}}")

    print("".join(cells))

def new_marked():
  m = [[False]*5 for _ in range(5)]
  m[2][2] = True # FREEは最初からマーク
  return m

def mark_number(card, marked, n):
  for r in range(5):
    for c in range(5):
      if card[r][c] == n:
        marked[r][c] = True
        return True
  return False

def count_lines(marked):
  lines = 0
  for r in range(5):
    if all(marked[r][c] for c in range(5)): lines += 1
  for c in range(5):
    if all(marked[r][c] for r in range(5)): lines += 1  
  if all(marked[i][i] for i in range(5)): lines += 1
  if all(marked[i][4-i] for i in range(5)): lines += 1  
  return lines

def main():
  logging.basicConfig(
    filename="bingo.log",
    level=logging.INFO, 
    format="%(asctime)s %(levelname)s:%(message)s",
    datefmt="%Y-%m-%d %H:%M:%S")
  logging.info("ビンゴゲームを開始します。")
  card = generate_card()
  marked = new_marked()
  pool = list(range(1, 76))
  random.shuffle(pool)
  history = []
  turn = 0

  print_card(card, marked)
  print("Enter=next  h=history  q=quit")

  while True:
    # Ctrl + D、または、Ctrl + Cで、例外を発生させる
    # cmd = input("> ").strip().lower()

    # 入力まわりの例外をキャッチ
    try:
      cmd = input("> ").strip().lower()
    except (EOFError, KeyboardInterrupt):
      print("\nBye!")
      break

    # try:
    #     cmd = input("> ").strip().lower()
    # except Exception as e:
    #     print(type(e))   # 例外の型
    #     print(e)         # 例外メッセージ

    # コマンド分岐(進行しない分岐は必ず continue)
    if cmd in ("q", "quit", "exit"):
      print("Bye!")
      break

    elif cmd in ("h", "history"):
      print(f"Called: {', '.join(map(str, history)) or '(none)'}")
      continue

    elif cmd in ("", "n", "next"):
      # Enter(または n/next)のときだけ1ターン進行
      print("1ターン進行します。")
    else:
      print(f"無効なコマンドです: {cmd!r}  (Enter / h / q を使用)")
      continue

    # ここから1ターン進行(有効な進行コマンドのときだけ実行)
    if not pool:
      print("No numbers left. Draw game.")
      break

    n = pool.pop()
    history.append(n)
    turn += 1

    mark_number(card, marked, n)
    lines = count_lines(marked)

    print(f"\nTurn {turn}  Called: {n}  Remaining: {len(pool)}  Lines: {lines}")
    print_card(card, marked)

    if lines >= 1:
      print("\nBINGO! congrats!")
      break

if __name__ == "__main__":
  main()

Ctrl + D、Ctrl + Cのどちらを押してもBye!と優雅に表示されるようになった。

例外の型と例外メッセージを出力

以下の赤文字部分のコメントアウトを外した。

import random
import logging

B_RANGES = [(1, 15), (16, 30), (31, 45), (46, 60), (61, 75)]
CELL_W = 4

def generate_card():
  cols = [random.sample(range(lo, hi + 1), 5) for lo, hi in B_RANGES]
  card = [[cols[c][r] for c in range(5)] for r in range(5)]
  card[2][2] = "FR"
  return card

def print_card(card, marked=None):
  header = "".join(f"{ch:^{CELL_W}}" for ch in "BINGO")
  print(header)

  for r in range(5):
    cells = []
    for c in range(5):
      v = card[r][c]
      if v == "FR":
        s = "[FR]"
      elif marked and marked[r][c]:
        s = f"[{str(v).rjust(2)}]"  
      else:
        s = f"{str(v).rjust(2)}"

      cells.append(f"{s:^{CELL_W}}")

    print("".join(cells))

def new_marked():
  m = [[False]*5 for _ in range(5)]
  m[2][2] = True # FREEは最初からマーク
  return m

def mark_number(card, marked, n):
  for r in range(5):
    for c in range(5):
      if card[r][c] == n:
        marked[r][c] = True
        return True
  return False

def count_lines(marked):
  lines = 0
  for r in range(5):
    if all(marked[r][c] for c in range(5)): lines += 1
  for c in range(5):
    if all(marked[r][c] for r in range(5)): lines += 1  
  if all(marked[i][i] for i in range(5)): lines += 1
  if all(marked[i][4-i] for i in range(5)): lines += 1  
  return lines

def main():
  logging.basicConfig(
    filename="bingo.log",
    level=logging.INFO, 
    format="%(asctime)s %(levelname)s:%(message)s",
    datefmt="%Y-%m-%d %H:%M:%S")
  logging.info("ビンゴゲームを開始します。")
  card = generate_card()
  marked = new_marked()
  pool = list(range(1, 76))
  random.shuffle(pool)
  history = []
  turn = 0

  print_card(card, marked)
  print("Enter=next  h=history  q=quit")

  while True:
    # Ctrl + D、または、Ctrl + Cで、例外を発生させる
    # cmd = input("> ").strip().lower()

    # 入力まわりの例外をキャッチ
    # try:
    #   cmd = input("> ").strip().lower()
    # except (EOFError, KeyboardInterrupt):
    #   print("\nBye!")
    #   break

    try:
        cmd = input("> ").strip().lower()
    except Exception as e:
        print(type(e))   # 例外の型
        print(e)         # 例外メッセージ

    # コマンド分岐(進行しない分岐は必ず continue)
    if cmd in ("q", "quit", "exit"):
      print("Bye!")
      break

    elif cmd in ("h", "history"):
      print(f"Called: {', '.join(map(str, history)) or '(none)'}")
      continue

    elif cmd in ("", "n", "next"):
      # Enter(または n/next)のときだけ1ターン進行
      print("1ターン進行します。")
    else:
      print(f"無効なコマンドです: {cmd!r}  (Enter / h / q を使用)")
      continue

    # ここから1ターン進行(有効な進行コマンドのときだけ実行)
    if not pool:
      print("No numbers left. Draw game.")
      break

    n = pool.pop()
    history.append(n)
    turn += 1

    mark_number(card, marked, n)
    lines = count_lines(marked)

    print(f"\nTurn {turn}  Called: {n}  Remaining: {len(pool)}  Lines: {lines}")
    print_card(card, marked)

    if lines >= 1:
      print("\nBINGO! congrats!")
      break

if __name__ == "__main__":
  main()

これで例外の型と例外メッセージが表示されるようになった。

最初と変わらないように見えるが、EOFErrorの場合は、<class ‘EOLError’>が表示されている。

KeybordInterruptは、Exceptionのサブクラスではないため、<class ‘KeybordInterrupt’>が表示されない。

個別に捕まえると表示される。

    try:
        cmd = input("> ").strip().lower()
    except KeyboardInterrupt as e:
        print(type(e))   # 例外の型
        print(e)         # 例外メッセージ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です