@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 и проходить циклом внутри метода?
Просто не знаю насколько такая проверка адекватна и часто ли используют такой перебор, а не что-то более элегантное
  • Вопрос задан
  • 99 просмотров
Решения вопроса 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. Что он делает. Что хранит. Может окажется что тебе его делать не надо. Есть альтернативы.
Ответ написан
Ваш ответ на вопрос

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

Войти через центр авторизации
Похожие вопросы