プログラミング

Python超入門⑧ クイズゲームを完成させよう

変数、if文、for文、関数、リスト、辞書を組み合わせ、3問のクイズゲームを作ります。

いよいよ、覚えた文法を組み合わせてみましょう。今回は、3問のクイズを出し、正解数を表示するゲームを作ります。

「完成コードをいきなり渡されても、どこから読めばいいの?」となりやすいので、1問の判定 → 関数 → 複数の問題 → 採点の順に組み立てます。後半には、問題を増やす改造と、その解答も用意しました。

準備と、完成したときの動き

Python 3で動かします。quiz.py というファイルを練習用フォルダに作り、各段階のコードへ書き換えて実行してください。段階ごとのコードを全部つなげる必要はありません。

必要な知識は、入力と変数、if文、for文、関数、リストと辞書です。ファイルを使う改造では第7回も使います。

基本版は、次の順番で進みます。

  1. 問題を1つ表示する
  2. 入力された答えと、正解を比べる
  3. 正解なら1点増やす
  4. 全問題が終わったら、結果を表示する

問題を解く処理と問題データを分けるので、後から問題だけを追加できるようになります。

1. まず1問だけ、正解かどうか調べる

answer = input("2 + 3 は? ")

if answer == "5":
    print("正解です!")
else:
    print("正解は5です")

5 と入力すると「正解です!」、4 なら「正解は5です」と表示されます。

ここで比べているのは、整数の5ではなく文字列の "5"です。inputの結果は文字列なので、正解も文字列でそろえています。

ただし、この基本版では " 5 " のような余分な空白があると不正解です。そこは後半で直します。最初から全部対応しようとせず、まず「1問が正しく判定できる」ことを確認しましょう。

2. 1問分の情報を、辞書へまとめる

question = {
    "text": "2 + 3 は?",
    "answer": "5"
}

user_answer = input(question["text"] + " ")
is_correct = user_answer == question["answer"]

print(is_correct)

5 を入力すると True、それ以外なら False が表示されます。

question["text"] は問題文、question["answer"] は正解です。問題文と答えを1つの辞書にまとめると、対応が分かりやすくなります。

+ " " は、問題文の後ろへ空白を1つ足しています。入力する位置が問題文とくっつかないようにするためです。

「表示する問題が違っても、判定の書き方は同じだな」と見えてきたら、次は関数に分けます。

3. 1問を出す処理を関数にする

def ask_question(question):
    user_answer = input(question["text"] + " ")
    return user_answer == question["answer"]

question = {"text": "2 + 3 は?", "answer": "5"}
result = ask_question(question)
print(result)

ask_question は、1問分の辞書を受け取ります。中で入力を受け取り、正解かどうかを戻り値にします。

場所 渡るもの
呼び出す側 → 関数 問題文と正解が入った辞書
関数の中 入力と正解を比較する
関数 → 呼び出す側 正解ならTrue、不正解ならFalse

return user_answer == question["answer"] は、比較結果を返す1行です。わざわざif文でTrueとFalseを返し分けなくても、比較した結果そのものが真偽値になります。

4. 基本版:3問を出して採点しよう

questions = [
    {"text": "2 + 3 は?", "answer": "5"},
    {"text": "Pythonで文字を表示する関数は?", "answer": "print"},
    {"text": "リストの最初の位置は?", "answer": "0"}
]

def ask_question(question):
    user_answer = input(question["text"] + " ")
    return user_answer == question["answer"]

score = 0

for question in questions:
    if ask_question(question):
        print("正解です!")
        score += 1
    else:
        print(f"正解は{question['answer']}です")

print(f"{len(questions)}問中{score}問正解しました")

実行例です。5、print、1 と入力しています。

2 + 3 は? 5
正解です!
Pythonで文字を表示する関数は? print
正解です!
リストの最初の位置は? 1
正解は0です
3問中2問正解しました

for文とif文を分けて読む

for が、リストから問題の辞書を1つずつ取り出します。その辞書を ask_question() へ渡し、返ってきたTrueかFalseを if が使います。

問題 入力 戻り値 判定後のscore
1問目 5 True 1
2問目 print True 2
3問目 1 False 2

最初の score = 0 は、for文より前に置きます。中へ入れると、毎問0点へ戻ってしまうので注意してください。

また、最後の結果表示をfor文の外へ置くと、全問題が終わってから1回だけ表示されます。インデントは、ここでも処理の範囲を決めています。

5. 動いた後に、4通りで確かめよう

1回うまく動いても、別の答えで正しく動くとは限りません。入力を変えて結果を確認することをテストといいます。

試す入力の組 予想する結果
5、print、0 3問正解
4、echo、1 0問正解
5、print、1 2問正解
5、PRINT、0 基本版では2問正解

最後の例は、英字の大文字と小文字を区別するためです。「PRINTでも正解にしたい」と思ったら、それを次の仕様にします。仕様は、このゲームをどう動かしたいかというルールのことです。

6. 改造練習:4つの機能を足してみよう

次の順に改造してみてください。

  1. 問題を5問に増やす
  2. 英字の大文字・小文字と、前後の余分な空白を区別しない
  3. 正解率を小数点以下1桁で表示する
  4. 問題をJSONファイルから読み込む

JSONは、文字や数値、リストのようなデータを保存・交換するための形式です。まずは1〜3、最後に4へ進むと、どの変更で動かなくなったかを追いやすくなります。

1〜3の模範解答

questions = [
    {"text": "2 + 3 は?", "answer": "5"},
    {"text": "Pythonで文字を表示する関数は?", "answer": "print"},
    {"text": "リストの最初の位置は?", "answer": "0"},
    {"text": "Pythonで真を表す値は?", "answer": "True"},
    {"text": "関数を定義するキーワードは?", "answer": "def"}
]

def ask_question(question):
    user_answer = input(question["text"] + " ").strip().lower()
    correct_answer = question["answer"].strip().lower()
    return user_answer == correct_answer

score = 0

for question in questions:
    if ask_question(question):
        print("正解です!")
        score += 1
    else:
        print(f"正解は{question['answer']}です")

if len(questions) > 0:
    rate = score / len(questions) * 100
    print(f"{len(questions)}問中{score}問正解しました")
    print(f"正解率は{rate:.1f}%です")
else:
    print("問題がありません")

解説:どこを、なぜ変えた?

問題数は、辞書を2個追加して5問にしました。採点側で len(questions) を使っているので、問題数を別の場所で書き直す必要はありません。

入力のそろえ方では、strip() が前後の空白を取り除き、lower() が英字を小文字へそろえます。入力と正解の両方へ同じ処理をするので、" PRINT " と "print" を同じものとして比較できます。文字列メソッドの公式説明

これはクイズの採点ルールです。Pythonのコードそのものは大文字・小文字を区別し、真偽値は True と書く必要があります。

正解率は「正解数 ÷ 問題数 × 100」です。5問中4問なら80.0%。:.1f は小数点以下1桁での表示指定です。問題が0問なら割り算できないので、先に個数を確認しています。

7. ファイルから問題を読む改造

まず問題データを用意する

練習フォルダで、quiz.py と同じ場所に questions.json を作り、次をUTF-8で保存してください。同名の大切なファイルは上書きしないようにします。

[
  {"text": "2 + 3 は?", "answer": "5"},
  {"text": "Pythonで文字を表示する関数は?", "answer": "print"},
  {"text": "リストの最初の位置は?", "answer": "0"},
  {"text": "Pythonで真を表す値は?", "answer": "True"},
  {"text": "関数を定義するキーワードは?", "answer": "def"}
]

JSONでは文字列をダブルクォートで囲みます。Pythonのリストと似ていますが、コメントや末尾の余分なカンマなど、Pythonなら書ける形でもJSONでは使えないものがあります。

4まで含めた模範解答

次のコードを quiz.py として保存して動かします。__file__ は実行しているファイルの場所を示すため、ファイルではない対話入力欄へそのまま貼る使い方には向きません。

import json
from pathlib import Path

data_path = Path(__file__).resolve().with_name("questions.json")

try:
    with open(data_path, "r", encoding="utf-8") as file:
        questions = json.load(file)

    if not isinstance(questions, list) or len(questions) == 0:
        raise ValueError("問題は1件以上のリストにしてください")

    for question in questions:
        if not isinstance(question, dict):
            raise ValueError("各問題は辞書の形にしてください")
        if not isinstance(question.get("text"), str):
            raise ValueError("textには問題文の文字列が必要です")
        if not isinstance(question.get("answer"), str):
            raise ValueError("answerには正解の文字列が必要です")

except (OSError, ValueError) as error:
    print(f"問題データを読めません: {error}")
    raise SystemExit(1)

def ask_question(question):
    user_answer = input(question["text"] + " ").strip().lower()
    correct_answer = question["answer"].strip().lower()
    return user_answer == correct_answer

score = 0

for question in questions:
    if ask_question(question):
        print("正解です!")
        score += 1
    else:
        print(f"正解は{question['answer']}です")

rate = score / len(questions) * 100
print(f"{len(questions)}問中{score}問正解しました")
print(f"正解率は{rate:.1f}%です")

5問とも正解したとき、最後の2行は次のようになります。

5問中5問正解しました
正解率は100.0%です

解説:新しく増えた部分だけ読む

ファイルの場所を決める。 Path(__file__).resolve().with_name("questions.json") は、実行中のPythonファイルを基準に、同じフォルダのquestions.jsonを指定します。ターミナルを別の場所から起動しても、問題ファイルを探す基準がぶれないようにしています。pathlibの公式説明

JSONを読み込む。 json.load() は、ファイルのJSONをPythonのデータへ変換します。今回のデータでは、外側がリスト、中身が辞書になります。jsonの公式説明

必要な形かを確認する。 isinstance(値, 型) で、値がリストや辞書、文字列かを調べています。問題が空だったり、"answer" が欠けたりしている場合は、raise ValueError(...) で自分からエラーを知らせます。

読み込めない場合は終了する。 OSError はファイルが見つからない、権限がない、といった入出力の失敗です。JSONの文法エラーも、ここではValueError側で受け取れます。SystemExit(1) は、エラー終了を示してプログラムを止めています。

この形にすると、データが壊れているのに採点を始めてしまうことを防げます。最初は長く感じますが、「読む前の準備」「読む」「形の確認」「出題」と区切れば、採点の中身は前とほぼ同じです。

8. 改造版の確認と、分かっておきたい限界

確認すること 予想する動作
PRINT や前後に空白のある print 正解として扱う
5問中4問だけ正解 正解率80.0%
JSONを空の [] にする 問題を出さず、データのエラーを表示
"answer" を書き忘れる 正解の文字列が必要だと表示
questions.jsonがない ファイルを読めないと表示して終了

なお、全角数字の 5 と半角の 5 は、今回の処理だけでは同じになりません。print() と print も違います。どこまで正解とするかは、クイズのルールに合わせて別途決める必要があります。

また、答えを手元のJSONへ保存しているので、ファイルを開けば正解が見えます。これは学習用のゲームで、不正を防ぐ試験システムではありません。

ここまで作れたら、文法がつながり始めています

変数で点数を持つ。辞書で1問をまとめる。リストで問題を並べる。関数で出題を担当し、if文で採点し、for文で繰り返す。

別々に覚えたものが、1つのゲームの中で役割を持つようになりました。分からなくなったときも、全部を一度に理解し直す必要はありません。「今はどの問題か」「関数は何を返したか」「点数は増えたか」を順番に確認してみてください。

前の記事:Python超入門⑦ エラー処理とファイル保存

最初から復習する:Python超入門① 最初のプログラム

Pythonシリーズの記事一覧

← 記事一覧へ戻る