Hej, napisałem pierwszą wersję kalkulatora wykorzystujący samodzielnie zrobiony przeze mnie interfejs menu.
Co można poprawić w tym kodzie? Jakie bugi przeoczyłem? Czy dobrze robię robiąc z MenuItem data klasę czy lepiej przekazywać do konstruktora Menu słownik?
Kod:
~/python_misc ❯ cat calc.py 21:03:15
#!/usr/bin/env python3
from ui.menu import Menu, MenuItem
from ui.user_input import read_number
def add() -> None:
first = read_number('Enter the first operand: ')
second = read_number('Enter the second operand: ')
print(f'The result is {first + second}.')
def subtract() -> None:
first = read_number('Enter the first operand: ')
second = read_number('Enter the second operand: ')
print(f'The result is {first - second}.')
def multiply() -> None:
first = read_number('Enter the first operand: ')
second = read_number('Enter the second operand: ')
print(f'The result is {first * second}.')
def divide() -> None:
first = read_number('Enter the first operand: ')
second = read_number('Enter the second operand (cannot be zero): ',
nonzero=True)
print(f'The result is {first / second}.')
items = [MenuItem('Add', add),
MenuItem('Subtract', subtract),
MenuItem('Multiply', multiply),
MenuItem('Divide', divide)]
menu = Menu('Calculator', items)
menu.loop()
~/python_misc ❯ cat ui/menu.py 21:03:20
from dataclasses import dataclass
from typing import Callable
@dataclass
class MenuItem:
name: str
action: Callable[[], None]
def __str__(self):
return self.name
class Menu:
nesting_level = -1
_quit_choice = -1
def __init__(self, title: str, items: list[MenuItem]):
self.title = title
self.items = items
self.active = False
def _print_title(self) -> None:
print('-' * (len(self.title) + 4))
print(f'| {self.title} |')
print('-' * (len(self.title) + 4))
def _read_choice(self) -> int:
while True:
self._print_title()
for idx, item in enumerate(self.items):
print(f'{idx + 1}) {item}')
print('q)', 'Back' if Menu.nesting_level > 0 else 'Quit')
user_input: int | str = input('Your choice: ').lower()
if user_input == 'q':
return Menu._quit_choice
try:
user_input = int(user_input)
if user_input not in range(1, len(self.items) + 1):
raise ValueError()
return user_input - 1
except ValueError:
print('Error: invalid choice. Try again...')
def _quit(self) -> None:
self.active = False
Menu.nesting_level -= 1
def add_item(self, name, action):
self.items.append(MenuItem(name, action))
def remove_item(self, name):
for idx, item in enumerate(self.items):
if item.name == name:
del self.items[idx]
def loop(self) -> None:
Menu.nesting_level += 1
self.active = True
while self.active:
user_choice: int = self._read_choice()
if user_choice == self._quit_choice:
self._quit()
else:
self.items[user_choice].action()
input('\nPress any key and type ENTER...\n')
~/python_misc ❯ cat ui/user_input.py 21:03:26
from typing import Any, Optional, overload
def get_default_prompt(lower: bool, upper: bool, nonzero: bool):
prompt = 'Enter the number'
if lower or upper or nonzero:
prompt += ' ('
if lower:
prompt += f'starting from {lower}'
if upper:
prompt += f'up to {upper}'
if nonzero:
if lower or upper:
prompt += ', '
prompt += 'cannot be zero'
if lower or upper or nonzero:
prompt += ')'
prompt += ': '
return prompt
@overload
def read_number[T](prompt: str = ..., *,
lower: Optional[T] = ...,
upper: Optional[T] = ...,
nonzero: bool = ...,
_type: type[T]) -> T: ...
@overload
def read_number[T](prompt: str = ..., *,
lower: Optional[T] = ...,
upper: Optional[T] = ...,
nonzero: bool = ...) -> float: ...
def read_number[T](prompt: str = '', *,
lower: Optional[T] = None,
upper: Optional[T] = None,
nonzero: bool = False,
_type: type = float):
if not prompt:
prompt = get_default_prompt(bool(lower),
bool(upper),
bool(nonzero))
while True:
try:
try:
number: Any = _type(input(prompt))
except ValueError as exc:
raise ValueError('not a number') from exc
if nonzero and number == 0:
raise ValueError('number is equal to zero')
if lower and number < lower:
raise ValueError('number too small')
if upper and number > upper:
raise ValueError('number too big')
return number
except ValueError as exc:
print(f'Error: {exc}. Try again...')