Избыточно ли такое построение архитектуры?
У меня есть функции, которые принимают входные данные, и вызывают другую функцию, передавая эти данные.
def add_document_with_input_value():
dir_number = input("Введите номер полки")
document_type = input("Введите тип документа")
document_number = input("Введите номер документа")
document_name = input("Введите владельца документа")
return add_document(dir_number, document_type, document_number, document_name)
И мне показалось, что такие инпуты смотрятся слишком объёмно и как то не очень красиво. Я решил написать функцию, которая будет запрашивать нужное количество запросов на ввод данных
def get_value_input_count(count: int = 1):
if count > 0:
value_count = [input(f"Введите значение №{i + 1}\n") for i in range(count)]
return value_count[:count]
raise ValueError("Введите число больше 0")
Но здесь наткнулся на проблему, что я таким образом не понимаю, что именно сейчас вводить: тип документа или его номер (т.е., придётся как то помнить, в каком порядке вводить значения). Для решения этой проблемы, я решил создать словарь, ключом которого будет имя функции, а значение - список с текстом, необходимым для ввода.
prompt = {
"add_document_with_input_value": ["\n(№1) Введите номер полки", "\n(№2) Введите тип документа", "\n(№3) Введите номер документа", "\n(№4) Введите владельца документа"],
}
И потом декоратор, который и будет выводить мне подсказки, что и в каком порядке вводить.
def get_prompt_entry_value(func):
def wrapper(*args, **kwargs):
name_func = func.__name__
get_prompt = prompt.get(name_func)
if get_prompt:
print(*(i for i in get_prompt))
return func(*args, **kwargs)
return wrapper
И по итогу, получается такая функция:
@get_prompt_entry_value
def add_document_with_input_value():
dir_number, document_type, document_number, document_name = get_value_input_count(4)
return add_document(dir_number, document_type, document_number, document_name)
И собственно сам вопрос: рационально ли было так выкручиваться, или же лучше было оставить исходный вариант, т.к. более ясно, что к чему и делается ли так вообще?
Ответы (2 шт):
По-моему, вы усложняете себе жизнь на ровном месте. Декораторы зачем-то... Как по мне, решение "в лоб" получается и компактно, и понятно:
def input_values_with_prompt(prompts: list) -> list :
return [input(f"Введите {prompt}: ") for prompt in prompts]
def add_document_with_input_value():
prompts = ["номер полки", "тип документа", "номер документа", "владельца документа"]
return add_document(*input_values_with_prompt(prompts))
Отвечая на вопрос в заголовке: в данном случае такая архитектура, очевидно, избыточна, но тем не менее поразбираться в работе тех же декораторов может быть интересно и полезно.
По вашему коду. У вас не очень логично работает: сначала выводится 4 запроса что ввести, потом 4 раза запрашивает. И декоратор неявно полагается на глобальную переменную prompts, что делает его негибким. Тут имеет смысл сделать декоратор, в который передается список запросов, декоратор сам все запрашивает, и раскладывает по аргументам функции, например:
def input_arguments_with_prompts(*prompts):
def decorator(func):
def wrapper(*args, **kwargs):
args = list(args)
for prompt in prompts:
args.append(input(prompt))
return func(*args, **kwargs)
return wrapper
return decorator
@input_arguments_with_prompts(
"Введите номер полки: ",
"Введите тип документа: ",
"Введите номер документа: ",
"Введите владельца документа: "
)
def add_document_with_input_value(dir_number, document_type, document_number, document_name):
print(dir_number, document_type, document_number, document_name)
add_document_with_input_value()
Пример ввода/вывода:
Введите номер полки: 2
Введите тип документа: 12
Введите номер документа: 4
Введите владельца документа: sdfsdf
2 12 4 sdfsdf
Можно заморочиться, и добавить, чтобы декоратор понимал аннотации типа у аргументов декорируемой функции: например, если если у аргумента указан тип int то конвертировать аргумент в соответствующий тип. Набросок кода:
import inspect
def input_arguments_with_prompts(*prompts):
def decorator(func):
def wrapper(*args, **kwargs):
parameters = inspect.signature(func).parameters
# в parameters будет объект вида OrderedDict([('dir_number', <Parameter "dir_number: int">), ('document_type', <Parameter "document_type">), ('document_number', <Parameter "document_number: int">), ('document_name', <Parameter "document_name: str">)])
for prompt, (name, parameter) in zip(prompts, parameters.items()):
while True:
value = input(prompt)
if parameter.annotation is inspect._empty:
converted_value = value
break
else:
try:
converted_value = parameter.annotation(value)
except ValueError:
print(f"Некорректное значение: {value}")
print("Повторите ввод")
else:
break
kwargs[name] = converted_value
return func(*args, **kwargs)
return wrapper
return decorator
@input_arguments_with_prompts(
"Введите номер полки: ",
"Введите тип документа: ",
"Введите номер документа: ",
"Введите владельца документа: "
)
def add_document_with_input_value(dir_number: int, document_type, document_number: int, document_name: str):
print(dir_number, document_type, document_number, document_name)
add_document_with_input_value()
Пример ввода-вывода:
Введите номер полки: wefer
Некорректное значение: wefer
Повторите ввод
Введите номер полки: 12
Введите тип документа: erger
Введите номер документа: 1
Введите владельца документа: fgerg
12 erger 1 fgerg
Вместо простых типов можно указывать функции, которые будут валидировать значения (например, что оно больше нуля), кидать ValueError в случае ошибки, и возвращать значение в случае успеха (в идеале вместо функций должны быть callable классы отнаследованные от typing.NewType(...), чтобы линтеры типа mypy не ругались). Пример:
def larger_than_0(value):
value = int(value)
if value <= 0:
raise ValueError
return value
@input_arguments_with_prompts(
"Введите номер полки: ",
"Введите тип документа: ",
"Введите номер документа: ",
"Введите владельца документа: "
)
def add_document_with_input_value(dir_number: larger_than_0, document_type: str, document_number: larger_than_0, document_name: str):
print(dir_number, document_type, document_number, document_name)
add_document_with_input_value()
Похожим образом (через обработку аннотаций типов) работают такие библиотеки, как fastapi и typer.