Program kuis sejarah Python

Oct 17 2020

Program ini memungkinkan pengguna untuk mengelola pertanyaan dan pengguna kuis dari tahun peristiwa sejarah. Saya mencari saran tentang bagaimana saya bisa mengatur kode saya dengan lebih baik, mungkin menerapkan OOP untuk fungsi dan menghindari penggunaan variabel global data_filename.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from enum import Enum
import os
import pickle
import random


data_filename = 'program.obj'


def read_int(prompt='> ', errmsg='Invalid number!'):
    number = None
    while number is None:
        try:
            number = int(input(prompt))
        except ValueError:
            print(errmsg)
    return number


def display_menu():
    print('What do you want to do?')
    print('[1] List all historical events')
    print('[2] Add event')
    print('[3] Remove event')
    print('[4] Quiz')
    print('[5] Statistics')
    print('[6] Clear statistics') # TODO: move to options submenu
    print('[7] Exit')


def clear_screen():
    print(chr(27) + "[2J")


def pause():
    input('Press any key to continue...')


def yes_or_no(prompt='Proceed? [y|n]\n> ', errmsg='Valid answers are y and n.'):
    answer = input(prompt).strip().lower()
    while answer != 'y' and answer != 'n':
        print(errmsg)
        answer = input(prompt).strip().lower()
    return answer


def read_data_file():
    file = open(data_filename, 'a+b')
    file.seek(0)
    data = {'events': [], 'statistics': {'total_successes': 0, 'total_failures': 0}}
    if os.path.getsize(data_filename) > 0:
        data = pickle.load(file)
    file.close()
    return data


def list_events(data):
    events = data['events']
    if len(events) == 0:
        print('Event list is empty!')
    else:
        print('Historical events:')
        for event in events:
            print(event)


def add_event(data):
    events = data['events']
    year = read_int(prompt='Enter year: ')
    description = input('Enter description: ')
    events.append({'year': year, 'description': description})
    print('Successfully added a new historical event!')


def remove_event(data):
    events = data['events']
    if len(events) == 0:
        print('Event list is empty!')
    else:
        for event in events:
            print(events.index(event), event)
        index = read_int('Which event do you want to delete? ')
        try:
            events.pop(index)
            print('Successfully deleted event!')
        except IndexError:
            print('Number out of range!')


def quiz(data):
    events = data['events']
    stats = data['statistics']
    if len(events) == 0:
        print('Event list is empty!')
    else:
        num = read_int(prompt='How many questions should I ask? ')
        if 0 < num <= len(events):
            for event in random.sample(events, num):
                print(event['description'])
                year = read_int('In which year was following event occurred? ')
                if year == event['year']:
                    stats['total_successes'] += 1
                    print('Good answer!')
                else:
                    stats['total_failures'] += 1
                    print('Bad answer!')
        elif num < 0:
            print('Number of questions can\'t be negative!')
        else:
            print('Too much questions!')


def display_stats(data):
    stats = data['statistics']
    tries = stats['total_successes'] + stats['total_failures']
    if tries == 0:
        total_successes = 0
        total_failures = 0
    else:
        total_successes = stats['total_successes']/tries * 100
        total_failures = stats['total_failures']/tries * 100
    print('Statistics')
    print('Total: {0:10.2f}% successes, {1:10.2f}% failures'.format(total_successes, total_failures))


def clear_stats(data):
    answer = yes_or_no('Are you sure you want to clear statistics? [y|n]\n> ')
    if answer == 'y':
        data['statistics'] = {'total_successes': 0, 'total_failures': 0}
        print('Successfully cleared statistics!')
    else:
        print('Statistics left unchanged.')


# TODO: settings submenu
# def settings(data):
#     print('-' * 10)
#     print('Program settings')
#     print('-' * 10)
#     print('[1] Clear statistics')
#     print('[2] Back')
#     user_choice = read_int()
#     while user_choice != 2:
#         if user_choice == 1:
#             clear_stats(data)


def update_data_file(data):
    file = open(data_filename, 'wb')
    pickle.dump(data, file)
    file.close()


class Choices(Enum):
    list_events = 1
    add_event = 2
    remove_event = 3
    quiz = 4
    statistics = 5
    clear_stats = 6
    exit = 7


program_data = read_data_file()
choice = None
while choice != Choices.exit.value:
    clear_screen()
    display_menu()
    choice = read_int()
    if choice == Choices.list_events.value:
        list_events(program_data)
    elif choice == Choices.add_event.value:
        add_event(program_data)
    elif choice == Choices.remove_event.value:
        remove_event(program_data)
    elif choice == Choices.quiz.value:
        quiz(program_data)
    elif choice == Choices.statistics.value:
        display_stats(program_data)
    elif choice == Choices.clear_stats.value:
        clear_stats(program_data)
    elif choice == Choices.exit.value:
        print('Good bye!')
    else:
        print('Invalid choice!')
    update_data_file(program_data)
    pause()

Jawaban

3 Reinderien Oct 17 2020 at 20:21

Konstanta

data_filename harus menggunakan huruf besar, karena ini adalah konstanta global.

Pulang lebih awal

Tidak perlu digunakan numbersebagai kondisi pada Anda while; sebagai gantinya:

number = None
while number is None:
    try:
        number = int(input(prompt))
    except ValueError:
        print(errmsg)
return number

dapat

while True:
    try:
        return int(input(prompt))
    except ValueError:
        print(errmsg)

Manajemen menu

display_menudapat menggunakan urutan tupel, lebih baik lagi urutan tupel bernama atau @dataclasses, masing-masing memiliki atribut string judul dan atribut yang dapat dipanggil. Maka Anda display_menubisa

print('What do you want to do?')
print('\n'.join(f'[{i}] {item.title}' for i, item in enumerate(menu, 1)))

Saya melihat bahwa Anda juga memiliki Choicesenum. Itu tidak buruk, dan Anda berpotensi menggunakan enum dan saran di atas, selama Anda memiliki kamus enum-choice-to-function-reference.

Tes keanggotaan

answer != 'y' and answer != 'n'

dapat

answer not in {'y', 'n'}

Struktur lingkaran

Hindari menelepon inputdua kali; ini:

answer = input(prompt).strip().lower()
while answer != 'y' and answer != 'n':
    print(errmsg)
    answer = input(prompt).strip().lower()
return answer

dapat

while True:
    answer = input(prompt).strip().lower()
    if answer in {'y', 'n'}:
        return answer
    print(errmsg)

Operasi file

file = open(data_filename, 'a+b')
file.seek(0)

memiliki beberapa masalah:

  • Pegangan file yang baru dibuka tidak memerlukan pencarian awal untuk memulai; itu mubazir jika Anda membuka dalam mode baca-biner
  • Anda harus menggunakan withdan menghindari yang eksplisitclose()
  • Anda hanya perlu repot-repot menginisialisasi default Anda datasaat diperlukan
  • Anda ingin memeriksa apakah file tersebut ada, bukan ukurannya

Begitu:

if os.path.exists(data_filename):
    with open(data_filename, 'rb') as file:
        return pickle.load(file)
return {'events': [], 'statistics': {'total_successes': 0, 'total_failures': 0}}

Predikat yang berlebihan

if 0 < num <= len(events):
    ...
elif num < 0:
    print('Number of questions can\'t be negative!')
else:
    print('Too much questions!')

perlu dipikirkan ulang. Pertama, too much questionsharus too many questions. Juga, bagaimana jika pengguna memasukkan 0? Tentunya itu bukan “terlalu banyak pertanyaan”, tapi itulah yang akan dicetak. Disarankan:

if num < 1:
    print('Not enough questions.')
elif num > len(events):
    print('Too many questions.')
else:
    ...

Data yang diketik lebih kuat, terstruktur lebih kuat

stats['total_successes'] + stats['total_failures']

adalah contoh dari apa yang saya lihat disebut "pasta data". Kamus disalahgunakan, di sini, ketika sesuatu seperti @dataclasspetunjuk tipe yang mengandung lebih tepat.

Interpolasi string

'Total: {0:10.2f}% successes, {1:10.2f}% failures'.format(total_successes, total_failures)

lebih mudah diungkapkan sebagai

(
    f'Total: {total_successes:10.2f}% successes, '
    f'{total_failures:10.2f}% failures'
)

Pembagian garis adalah opsional tetapi lebih baik untuk keterbacaan.

String lolos

'Number of questions can\'t be negative!'

lebih mudah ditulis sebagai

"Number of questions can't be negative!"