Chương trình câu hỏi lịch sử Python

Oct 17 2020

Chương trình này cho phép người dùng quản lý các câu hỏi và câu đố người dùng từ các năm sự kiện lịch sử. Tôi đang tìm các gợi ý về cách tôi có thể tổ chức mã của mình tốt hơn, có thể áp dụng OOP cho các hàm và tránh sử dụng biến toàn cục 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()

Trả lời

3 Reinderien Oct 17 2020 at 20:21

Hằng số

data_filename nên được viết hoa, vì nó là một hằng số toàn cầu.

Trả lại sớm

Không cần phải sử dụng numbernhư điều kiện của bạn while; thay thế:

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

có thể

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

Quản lý menu

display_menucó thể sử dụng một chuỗi các bộ giá trị, tốt hơn là một chuỗi các bộ hoặc các bộ được đặt tên @dataclass, mỗi bộ có một thuộc tính chuỗi tiêu đề và một thuộc tính có thể gọi. Sau đó, bạn display_menucó thể là

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

Tôi thấy rằng bạn cũng có một Choicesenum. Điều đó không tệ, và bạn có thể sử dụng cả enum và gợi ý ở trên, miễn là bạn có một từ điển gồm các tham chiếu enum-choice-to-function-reference.

Kiểm tra tư cách thành viên

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

có thể

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

Cấu trúc vòng lặp

Tránh phải gọi inputhai lần; điều này:

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

có thể

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

Hoạt động tệp

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

có một số vấn đề:

  • Các xử lý tệp mới mở không cần tìm kiếm bắt đầu ban đầu; điều đó là thừa nếu bạn mở ở chế độ đọc-nhị phân
  • Bạn nên sử dụng withvà tránh mộtclose()
  • Bạn chỉ nên khởi tạo mặc định của mình datakhi cần thiết
  • Bạn muốn kiểm tra xem tệp có tồn tại hay không chứ không phải kích thước của nó

Vì thế:

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

Các vị từ thừa

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

cần được suy nghĩ lại. Đầu tiên, too much questionsnên được too many questions. Ngoài ra, nếu người dùng nhập 0 thì sao? Chắc chắn đó không phải là "quá nhiều câu hỏi", nhưng đó là những gì sẽ được in ra. Đề xuất:

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

Dữ liệu được đánh máy mạnh hơn, có cấu trúc mạnh mẽ hơn

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

là một ví dụ về những gì tôi đã thấy được gọi là "mì ống dữ liệu". Từ điển đang bị lạm dụng, ở đây, khi một cái gì đó giống như @dataclassgợi ý loại chứa là thích hợp hơn.

Nội suy chuỗi

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

được diễn đạt dễ dàng hơn là

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

Việc phân chia dòng là tùy chọn nhưng tốt hơn để dễ đọc.

Chuỗi thoát

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

được viết dễ dàng hơn là

"Number of questions can't be negative!"