โปรแกรมตอบคำถามประวัติ Python

Oct 17 2020

โปรแกรมนี้ช่วยให้ผู้ใช้จัดการคำถามและตอบคำถามผู้ใช้จากเหตุการณ์ในประวัติศาสตร์หลายปี ฉันกำลังมองหาคำแนะนำเกี่ยวกับวิธีการที่ฉันสามารถที่ดีกว่าการจัดระเบียบรหัสของฉันอาจจะใช้ OOP 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()

คำตอบ

3 Reinderien Oct 17 2020 at 20:21

ค่าคงที่

data_filename ควรเป็นตัวพิมพ์ใหญ่เนื่องจากเป็นค่าคงที่ทั่วโลก

ผลตอบแทนก่อนกำหนด

ไม่มีความจำเป็นที่จะใช้เป็นnumberเป็นเงื่อนไขในของคุณwhile; แทน:

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

เป็นไปได้

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

การจัดการเมนู

display_menuสามารถใช้ลำดับของทูเปิลได้ดีกว่า แต่ลำดับของทูเปิลที่มีชื่อหรือ@dataclasses แต่ละตัวมีแอตทริบิวต์สตริงหัวเรื่องและแอตทริบิวต์ที่เรียกได้ แล้วคุณdisplay_menuอาจจะ

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

ฉันเห็นว่าคุณมีChoicesenum ด้วย นั่นไม่ใช่เรื่องแย่และคุณสามารถใช้ทั้ง enum และคำแนะนำข้างต้นได้ตราบใดที่คุณมีพจนานุกรมการอ้างอิง enum-choice-to-function-references

การทดสอบการเป็นสมาชิก

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

เป็นไปได้

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

โครงสร้างห่วง

หลีกเลี่ยงการโทรinputซ้ำสองครั้ง นี้:

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

เป็นไปได้

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

การทำงานของไฟล์

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

มีปัญหาเล็กน้อย:

  • การจัดการไฟล์ที่เพิ่งเปิดใหม่ไม่จำเป็นต้องมีการค้นหาเพื่อเริ่มต้น ที่ซ้ำซ้อนหากคุณเปิดในโหมดอ่านไบนารี
  • คุณควรใช้withและหลีกเลี่ยงการโจ่งแจ้งclose()
  • คุณควรเริ่มต้นเฉพาะdataเมื่อจำเป็นเท่านั้น
  • คุณต้องการตรวจสอบว่ามีไฟล์อยู่หรือไม่ไม่ใช่ขนาดของไฟล์

ดังนั้น:

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}}

เพรดิเคตซ้ำซ้อน

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

ต้องคิดใหม่ ครั้งแรกที่ควรจะเป็นtoo much questions too many questionsนอกจากนี้จะเกิดอะไรขึ้นถ้าผู้ใช้ป้อน 0? นั่นไม่ใช่ "คำถามมากเกินไป" แต่นั่นคือสิ่งที่จะพิมพ์ออกมา แนะนำ:

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

ข้อมูลที่มีโครงสร้างแข็งแรงขึ้น

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

เป็นตัวอย่างของสิ่งที่ฉันเคยเห็นเรียกว่า "data pasta" พจนานุกรมกำลังถูกใช้ในทางที่ผิดที่นี่เมื่อ@dataclassคำแนะนำประเภทที่มีเนื้อหาเหมาะสมกว่า

การแก้ไขสตริง

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

แสดงได้ง่ายกว่าเป็น

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

การแบ่งบรรทัดเป็นทางเลือก แต่ดีกว่าเพื่อความชัดเจน

สตริงหนี

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

เขียนได้ง่ายกว่าด้วย

"Number of questions can't be negative!"