Python-Verlaufs-Quizprogramm
Mit diesem Programm können Benutzer Fragen verwalten und Benutzer anhand jahrelanger historischer Ereignisse testen. Ich suche nach Vorschlägen, wie ich meinen Code besser organisieren, möglicherweise OOP für Funktionen anwenden und die Verwendung globaler Variablen vermeiden kann 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()
Antworten
Konstanten
data_filename sollte groß geschrieben werden, da es eine globale Konstante ist.
Vorzeitige Rückkehr
Es ist nicht erforderlich, numberals Bedingung für Ihre zu verwenden while; stattdessen:
number = None
while number is None:
try:
number = int(input(prompt))
except ValueError:
print(errmsg)
return number
kann sein
while True:
try:
return int(input(prompt))
except ValueError:
print(errmsg)
Menüverwaltung
display_menukönnte eine Folge von Tupeln verwenden, besser noch eine Folge von benannten Tupeln oder @dataclasses, die jeweils ein Titelzeichenfolgenattribut und ein aufrufbares Attribut haben. Dann könntest du display_menusein
print('What do you want to do?')
print('\n'.join(f'[{i}] {item.title}' for i, item in enumerate(menu, 1)))
Ich sehe, dass Sie auch eine ChoicesAufzählung haben. Das ist nicht schlecht, und Sie könnten möglicherweise sowohl eine Aufzählung als auch den obigen Vorschlag verwenden, solange Sie über ein Wörterbuch mit Aufzählungsoptionen für Funktionsreferenzen verfügen.
Mitgliedschaftstests
answer != 'y' and answer != 'n'
kann sein
answer not in {'y', 'n'}
Schleifenstruktur
Vermeiden Sie es, inputzweimal anzurufen . Dies:
answer = input(prompt).strip().lower()
while answer != 'y' and answer != 'n':
print(errmsg)
answer = input(prompt).strip().lower()
return answer
kann sein
while True:
answer = input(prompt).strip().lower()
if answer in {'y', 'n'}:
return answer
print(errmsg)
Dateivorgänge
file = open(data_filename, 'a+b')
file.seek(0)
hat ein paar Probleme:
- Neu geöffnete Dateihandles erfordern keine anfängliche Suche nach Anfang. Das ist redundant, wenn Sie im Lese-Binär-Modus öffnen
- Sie sollten
witheine explizite verwenden und vermeidenclose() - Sie sollten sich nur
databei Bedarf die Mühe machen, Ihre Standardeinstellung zu initialisieren - Sie möchten überprüfen, ob die Datei vorhanden ist, nicht ihre Größe
So:
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}}
Redundante Prädikate
if 0 < num <= len(events):
...
elif num < 0:
print('Number of questions can\'t be negative!')
else:
print('Too much questions!')
muss überdacht werden. Erstens too much questionssollte sein too many questions. Was ist auch, wenn der Benutzer 0 eingibt? Sicherlich sind das nicht "zu viele Fragen", aber das wird gedruckt. Empfohlen:
if num < 1:
print('Not enough questions.')
elif num > len(events):
print('Too many questions.')
else:
...
Stärker typisierte, stärker strukturierte Daten
stats['total_successes'] + stats['total_failures']
ist ein Beispiel für das, was ich als "Datennudeln" bezeichnet habe. Wörterbücher werden hier missbraucht, wenn so etwas wie ein @dataclassenthaltener Typ besser geeignet ist.
String-Interpolation
'Total: {0:10.2f}% successes, {1:10.2f}% failures'.format(total_successes, total_failures)
wird leichter ausgedrückt als
(
f'Total: {total_successes:10.2f}% successes, '
f'{total_failures:10.2f}% failures'
)
Die Linienaufteilung ist optional, aber für die Lesbarkeit besser.
String entkommt
'Number of questions can\'t be negative!'
ist leichter zu schreiben als
"Number of questions can't be negative!"