Задать вопрос
@Span4ev

Насколько такой метод проверки является корректным и как его можно сократить?

Это работает, но каждый раз гляжу на него с печалью, словно что-то упускаю и есть более простое, лаконичное и правильное решение для такой проверки.
Нужно проверить все входящие аргументы на то что они являются целым числом больше 0
class Clock()
    ....
    def add_time(self, h=0, m=0, d=0, dw=0, mh=0, y=0):

        if type(h) == int and type(m) == int and type(d) == int and type(dw) == int and type(mh) == int and type(y) == int:
            if h >= 0 and m >= 0 and d >= 0 and dw >= 0 and mh >= 0 and y >= 0:

clock = Clock()
clock.add_time(1,1,1,1)

Может есть какой-то синтаксический сахар или тут что-то лишнее, от чего можно избавиться?
Или паковать всё в *kwargs и проходить циклом внутри метода?
Просто не знаю насколько такая проверка адекватна и часто ли используют такой перебор, а не что-то более элегантное
  • Вопрос задан
  • 104 просмотра
Подписаться 1 Простой 3 комментария
Решения вопроса 1
Vindicar
@Vindicar
RTFM!
1. Не надо проверять в рантайме type(m) == int. Достаточно указать в заголовке функции
def add_time(self, h: int = 0, m: int = 0, d: int = 0, dw: int = 0, mh: int = 0, y: int = 0):

Этого будет достаточно, чтобы IDE/статический анализатор сказал программисту "эй, ты фигню передаёшь в этот метод". Да, это не будет контролироваться во время выполнения, но тут уже другой вопрос: если клиент-программист сделал класс, неотличимый от int, стоит ему позволить этот класс передать вместо int. Ответственность будет на нём.

2. Ну для конкретно твоей проверки по значению можно просто:
if any(v < 0 for v in [h,m,d,dw,mh,y]):
    raise ValueError()

Если хочешь красиво, можно и выпендриться:
args = {'h':h, 'm':m, 'd':d, 'dw':dw, 'mh':mh, 'y':y}
bad = list(filter(lambda k: args[k] < 0, args.keys()))
if bad:
    raise ValueError('Invalid values for: ' + ','.join(bad))


Ну а при большом желании можно и целый велосипед для декларативной проверки параметров замутить.
Но смысл?
import functools
import inspect

# пусть чек-функция имеет вид (value) -> bool, и возвращает True для "хороших" значений. Пример:
def not_whitespace(s: str) -> bool:
    'String must not contain only whitespace'  # док строка будет использоваться в сообщении об ошибке
    return bool(s.strip())  # проверяем что строка не состоит из одних пробелов.
# чек-функции можно генерировать и на ходу:
def in_range(low, high):
    def check(value):
        return (low is None or low <= value) and (high is None or value <= high)
    check.__doc__ = f'Value must be between {low} and {high}.'
    return check
# теперь сделаем декоратор, который умеет принимать чек-функции и применять их перед вызовом цели
def check(**checks):
    def wrapper(func):
        sign = inspect.signature(func)
        names = list(sign.parameters.keys())  # имена параметров по порядку
        not_found = set(checks.keys()) - set(names)  # все ли чеки ссылаются на известные параметры?
        if not_found:
            # у нас есть чек на неизвестный параметр!
            raise NameError(', '.join(not_found))
        # всё ок, делаем обёртку над функцией
        
        @functools.wraps(func)
        def wrapped(*args, **kwargs):
            bad = []
            for param_name, check_func in checks.items():
                idx = names.index(param_name)
                if idx < len(args):
                    # параметр был передан через args
                    value = args[idx]
                    if not check_func(value):  # вызываем чек-функцию
                        err = getattr(check_func, '__doc__', '')
                        if err:
                            bad.append(f'{param_name} ({err})')
                        else:
                            bad.append(param_name)
                else:
                    pass  # могут быть хитрости с kwargs-only параметрами. Тут уж извини, мне влом писать.
            if bad:  # нашли ошибки?
                raise ValueError('Bad value for parameters: '+', '.join(bad))
                # тут ещё можно помудрить над скрытием последнего фрейма в traceback, но мне опять влом
            else:  # не нашли, вызываем функцию
                return func(*args, **kwargs)
        
        return wrapped
    return wrapper

# пример использования
# строка должна быть не из одних пробелов
# число должно быть в пределах от 1 до 10 включительно
@check(s=not_whitespace, n=in_range(1, 10))
def repeat(s: str, n: int) -> str:
    return s * n

print(repeat('test ', 3))

try:
    print(repeat('test ', 20))
except ValueError as err:
    print('yep! it failed!', err)

try:
    print(repeat('test ', -1))
except ValueError as err:
    print('yep! it failed!', err)

try:
    print(repeat('    ', 5))
except ValueError as err:
    print('yep! it failed!', err)

try:
    print(repeat('    ', 15))
except ValueError as err:
    print('yep! it failed!', err)
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
mayton2019
@mayton2019
Bigdata Engineer
Вот эту сигнатуру ты сам придумал.

def add_time(self, h=0, m=0, d=0, dw=0, mh=0, y=0):


и дальше уже пошли все остальные беды отсюда. А прикинь еще будет 20 методов которые делают сравнение времени, разность в днях или календарные расчеты и везде-везде у тебя будет это ужасное векторизованное представление времени. Хотя на самом деле время - это просто вещественная точка на оси истории. Одно число. Например для unix-time достаточно целого числа 32х бит чтобы хранить любую дату с 1970 года.

С проверками условий тут ничего не поделать. Все они будут как ты и написал в полном объеме. С проверкой типов можно извернуться и свалить их хотя-бы на type hints.

Посмотри как python3 рекомендует работать с временем https://docs.python.org/3/library/datetime.html

Если очень хочешь создавать свой класс Clock - создавай. Только опиши хотя-бы для себя какой у него SingleResponcibility. Что он делает. Что хранит. Может окажется что тебе его делать не надо. Есть альтернативы.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы