Programa de prueba de historia de Python
Este programa permite a los usuarios administrar preguntas y cuestionar a los usuarios de años de eventos históricos. Estoy buscando sugerencias sobre cómo puedo organizar mejor mi código, tal vez aplicar OOP para funciones y evitar el uso de variables globales 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()
Respuestas
Constantes
data_filename debe escribirse con mayúscula, ya que es una constante global.
Regreso anticipado
No es necesario utilizarlo numbercomo condición en su while; en lugar:
number = None
while number is None:
try:
number = int(input(prompt))
except ValueError:
print(errmsg)
return number
puede ser
while True:
try:
return int(input(prompt))
except ValueError:
print(errmsg)
Gestión de menú
display_menupodría usar una secuencia de tuplas, mejor aún una secuencia de tuplas con nombre o @dataclasses, cada una con un atributo de cadena de título y un atributo invocable. Entonces tu display_menupodrías ser
print('What do you want to do?')
print('\n'.join(f'[{i}] {item.title}' for i, item in enumerate(menu, 1)))
Veo que también tienes una Choicesenumeración. Eso no está mal, y potencialmente podría usar tanto una enumeración como la sugerencia anterior, siempre que tenga un diccionario de enum-options-to-function-referencias.
Pruebas de afiliación
answer != 'y' and answer != 'n'
puede ser
answer not in {'y', 'n'}
Estructura de bucle
Evite tener que llamar inputdos veces; esta:
answer = input(prompt).strip().lower()
while answer != 'y' and answer != 'n':
print(errmsg)
answer = input(prompt).strip().lower()
return answer
puede ser
while True:
answer = input(prompt).strip().lower()
if answer in {'y', 'n'}:
return answer
print(errmsg)
Operaciones de archivo
file = open(data_filename, 'a+b')
file.seek(0)
tiene algunos problemas:
- Los identificadores de archivos recién abiertos no necesitan una búsqueda inicial para comenzar; eso es redundante si abre en modo lectura-binario
- Debería utilizar
withy evitar unaclose() - Solo debe molestarse en inicializar su valor predeterminado
datacuando sea necesario - Quieres comprobar si el archivo existe, no su tamaño
Entonces:
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}}
Predicados redundantes
if 0 < num <= len(events):
...
elif num < 0:
print('Number of questions can\'t be negative!')
else:
print('Too much questions!')
necesita ser repensado. Primero, too much questionsdebería ser too many questions. Además, ¿qué pasa si el usuario ingresa 0? Seguramente no son "demasiadas preguntas", pero eso es lo que se imprimirá. Sugirió:
if num < 1:
print('Not enough questions.')
elif num > len(events):
print('Too many questions.')
else:
...
Datos estructurados y con tipos más sólidos
stats['total_successes'] + stats['total_failures']
es un ejemplo de lo que he visto llamado "pasta de datos". Se está abusando de los diccionarios, aquí, cuando algo como una @dataclasssugerencia de tipo contenedor es más apropiado.
Interpolación de cadenas
'Total: {0:10.2f}% successes, {1:10.2f}% failures'.format(total_successes, total_failures)
se expresa más fácilmente como
(
f'Total: {total_successes:10.2f}% successes, '
f'{total_failures:10.2f}% failures'
)
La división de línea es opcional pero mejor para la legibilidad.
La cadena se escapa
'Number of questions can\'t be negative!'
se escribe más fácilmente como
"Number of questions can't be negative!"