7-2. 【資格・標準ライブラリ応用】CBT風 擬似Web模試&弱点分析エンジン

第3章(資格試験対策講座:Python3 エンジニア認定各種対応)で学んだ標準ライブラリの知識や型ヒント、テスト駆動の考え方を応用し、CBT(Computer Based Testing)形式の模擬試験を出題・採点し、受験者の弱点分野を自動分析する「CBT風 擬似Web模試&弱点分析エンジン」を構築します。

1. アプリケーションの要件と設計

このシステムは、以下のPython標準機能とモジュールを組み合わせて実現します。

  1. dataclasses / typing: 問題データや受験結果のデータ構造を型安全に定義。
  2. itertools: 問題のシャッフルやグループ化(カテゴリ別集計)を効率的に処理。
  3. 自動採点 & 弱点分析: 解答結果を照合し、カテゴリごとの正答率を計算して苦手分野を特定。
  4. 単体テスト (pytest): 採点ロジックや正答率計算の信頼性を担保。

2. 実装コード(cbt_exam_engine.py)

以下のコードを cbt_exam_engine.py というファイル名で保存し、手元で実行してみましょう。

Python

from dataclasses import dataclass, field
from typing import List, Dict, Any
import itertools
import random

# 1. データ構造の定義 (dataclasses & typing)
@dataclass
class Question:
    question_id: int
    category: str      # 分野 (例: "文法基礎", "標準ライブラリ", "オブジェクト指向")
    prompt: str
    choices: List[str]
    correct_index: int # 正解の選択肢インデックス (0始まり)
    explanation: str

@dataclass
class ExamResult:
    score: int
    total_questions: int
    category_scores: Dict[str, Dict[str, int]] = field(default_factory=dict)

class CBTExamEngine:
    """CBT風模試の出題・採点・弱点分析を行うエンジンクラス"""
    
    def __init__(self, questions: List[Question]):
        self.questions = questions

    def evaluate_exam(self, user_answers: List[int]) -> ExamResult:
        """ユーザーの回答リストを受け取り、採点とカテゴリ別集計を行う"""
        if len(user_answers) != len(self.questions):
            raise ValueError("すべての問題に回答してください。")

        correct_count = 0
        category_stats: Dict[str, Dict[str, int]] = {}

        for q, user_ans in zip(self.questions, user_answers):
            # カテゴリごとの統計初期化
            if q.category not in category_stats:
                category_stats[q.category] = {"total": 0, "correct": 0}
            
            category_stats[q.category]["total"] += 1

            # 正誤判定
            if q.correct_index == user_ans:
                correct_count += 1
                category_stats[q.category]["correct"] += 1

        score_percentage = int((correct_count / len(self.questions)) * 100)
        return ExamResult(
            score=score_percentage,
            total_questions=len(self.questions),
            category_scores=category_stats
        )

# --- 模擬問題データの準備 (第3章の知識に対応) ---
SAMPLE_QUESTIONS = [
    Question(
        question_id=1,
        category="標準ライブラリ",
        prompt="itertools モジュールに関する記述として正しいものはどれか。",
        choices=[
            "無限のイテレータを生成する関数は含まれていない",
            "count, cycle, repeat などの無限イテレータ生成関数がある",
            "リスト内包表記よりも必ず実行速度が遅くなる",
            "インポートには C言語のコンパイラが必要である"
        ],
        correct_index=1,
        explanation="itertoolsには count(), cycle(), repeat() などの強力な無限イテレータ関数が備わっています。"
    ),
    Question(
        question_id=2,
        category="文法基礎",
        prompt="Pythonの可変デフォルト引数(mutable default argument)に関する問題として、関数定義時にリストをデフォルト引数に指定した際の挙動で正しいものはどれか。",
        choices=[
            "関数が呼び出されるたびに新しい空のリストが毎回作成される",
            "デフォルト引数のオブジェクトはモジュールロード時(定義時)に1度だけ生成され、呼び出し間で共有される",
            "SyntaxError(構文エラー)になる",
            "自動的にタプルに変換される"
        ],
        correct_index=1,
        explanation="可変オブジェクト(listやdictなど)をデフォルト引数にすると、関数定義時に1度だけ生成されたオブジェクトが後続の呼び出しでも共有されるため、意図しないバグの原因になります。"
    ),
    Question(
        question_id=3,
        category="オブジェクト指向",
        prompt="Pythonのプロパティ(@property)デコレータの主な用途として適切なものはどれか。",
        choices=[
            "プライベートな属性に対するアクセスやバリデーション付きの取得・設定をメソッドのように見せかけて安全に行う",
            "クラスのインスタンス化を禁止する",
            "メソッドを非同期処理(async/await)に変更する",
            "メモリ消費量を強制的に半分にする"
        ],
        correct_index=0,
        explanation="@property を使うことで、属性へのアクセス構文を保ったままゲッターやセッターのロジックを挟むことができます。"
    )
]

# --- 実行用メイン関数 ---
if __name__ == "__main__":
    print("🎓 【Python3 認定試験模試シミュレーター】\n")
    engine = CBTExamEngine(SAMPLE_QUESTIONS)
    
    # 模擬解答のシミュレーション(例: 問題1は正解[1]、問題2は不正解[0]、問題3は正解[0])
    mock_user_answers = [1, 0, 0]
    
    result = engine.evaluate_exam(mock_user_answers)
    print(f"📊 総合スコア: {result.score}% ({result.total_questions}問中)")
    print("\n--- 分野別弱点分析 ---")
    for cat, stats in result.category_scores.items():
        rate = (stats["correct"] / stats["total"]) * 100
        print(f"・{cat}: 正答率 {rate:.0f}% ({stats['correct']}/{stats['total']})")

3. 品質保証のための単体テスト(test_cbt_engine.py)

第6章・第7章で学んだ pytest を用いて、この模試エンジンの採点ロジックと弱点分析機能が正しく動作するかをテストします。

Python

# test_cbt_engine.py
import pytest
from cbt_exam_engine import Question, CBTExamEngine

@pytest.fixture
def sample_engine():
    q_list = [
        Question(1, "基礎文法", "Q1", ["A", "B"], 0, "Exp1"),
        Question(2, "標準ライブラリ", "Q2", ["A", "B"], 1, "Exp2")
    ]
    return CBTExamEngine(q_list)

def test_evaluate_exam_all_correct(sample_engine):
    # すべて正解の場合のテスト
    result = sample_engine.evaluate_exam([0, 1])
    assert result.score == 100
    assert result.category_scores["基礎文法"]["correct"] == 1

def test_evaluate_exam_partial_correct(sample_engine):
    # 半分不正解の場合のテスト
    result = sample_engine.evaluate_exam([0, 0])
    assert result.score == 50
    assert result.category_scores["標準ライブラリ"]["correct"] == 0

def test_evaluate_exam_invalid_length(sample_engine):
    # 回答数が一致しない場合にValueErrorが発生することのテスト
    with pytest.raises(ValueError):
        sample_engine.evaluate_exam([0])  # 2問必要なところ1問しか渡さない

テストの実行コマンド:

Bash

pytest -v

まとめ

本記事では、第3章で習得した資格試験の出題範囲(標準ライブラリや文法仕様)をベースに、dataclasses や typing による堅牢なデータ設計、および pytest による単体テストを組み合わせた「CBT風 擬似Web模試&弱点分析エンジン」の構築を学びました。

コメント

タイトルとURLをコピーしました