Почему мой Python декоратор иногда имеет тип 'str'? - PullRequest
0 голосов
/ 11 января 2020

Фон:

Я пишу функцию декоратора, val_limiter, которая ограничивает значение аргумента, передаваемого дочерней функцией. Ради тщательности (и потенциального будущего использования) я бы хотел, чтобы моя функция могла принимать числовые строки, а также обрабатывать нечисловые строки соответствующим образом. Так, например, посмотрите на следующий код:

@val_limiter('5')
def test(val)
    return val

Возвращаемое значение выполнения test(3) равно 3. Функция успешно переводит '5' в 5. Проблема возникает, когда я пытаюсь сделать что-то вроде:

@val_limiter('foo')
def test(val)
    return val

Это бросает TypeError: 'str' object is not callable на @val_limiter('foo'). Я хотел бы обработать исключение для чего-то вроде test(3), где оно будет возвращать сообщение об ошибке (например: 'The argument must be a number (you tried 'foo')


Проблема:

Когда я запускаю type(val_limiter('5')), я получаю function, но когда я запускаю type(val_limiter('foo')), я получаю str. Почему это происходит? Как лучше всего обработать это исключение?


Исходный код:

import operator
from functools import wraps

def val_limiter(val=0, limit=max, equal=True, force=False):

    val_types = [int, float, str]
    if type(val) not in val_types:
        val = int(val)
    if type(val) == str:
        try:
            val = int(val)
        except ValueError:
            return f'First argument must be a number (tried {val})'

    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            ops = {
                "<": operator.lt,
                ">": operator.gt
            }
            for i in args:
                i = int(i)
                modifier = 0
                op = '<'
                try:
                    eq_string = 'is'
                    if not equal:
                        eq_string = 'cannot exceed'
                        modifier = 1 if limit == min else -1
                    if limit == min: op = '>'
                    if ops[op](val + modifier, i):
                        if force:
                            return val + modifier
                        raise ValueError
                except ValueError:
                    return f'The {limit.__name__}imum value accepted {eq_string} {val} (tried {i})'
            return fn(int(*args), **kwargs)
        return wrapper
    return decorator

* edit: вот полный ответ об ошибке:

Traceback (most recent call last):
  File "/home/ec2-user/environment/playground/test.py", line 79, in test_bb_arg_1_is_str_fail
    @val_limiter('foo')
TypeError: 'str' object is not callable

* edit: я в настоящее время выполняю это через файл теста. Вот отрывок неудачного теста:

import unittest

class TestValLimiter(unittest.TestCase):

    def test_bb_arg_1_is_str_fail(self):
        @val_limiter('foo')
        def test(val):
            return val
        self.assertEqual(test(3), 'First argument must be a number (tried foo)')

if __name__ == '__main__':
    unittest.main()

Ответы [ 2 ]

0 голосов
/ 11 января 2020

Проблема здесь:

if type(val) == str:
    try:
        val = int(val)
    except ValueError:
        return f'First argument must be a number (tried {val})'

Когда вы передаете 'foo', он (правильно) определяется как str и пытается преобразовать его в int. Но 'foo' не может быть проанализирован как int, поэтому он возвращает сообщение об ошибке.

Декораторы просто syntacti c sugar:

@val_limiter('foo')
def test(val)
    return val

примерно эквивалентно :

def test(val)
    return val
test = val_limiter('foo')(test)

Таким образом, для случая 'foo' эта последняя эквивалентная строка становится такой:

test = 'First argument must be a number (tried foo)'(test)

, которая пытается "вызвать" str, как если бы это была функция .

Главный вывод из этого заключается в том, что вы не должны ловить исключения, когда вы не можете сделать с ними ничего полезного. Многие вводные курсы по программированию учат , как ловить исключения, но не , когда делает это (на самом деле, упражнения, которые учат вас, как это делать, часто делают это неправильно, преобразовывая исключения в print или return s, которые не имеют смысла, если только вызывающая сторона явно не проверяет возвращаемые типы и значения, то есть то, что исключения должны избегать ).

В этом В этом случае вам может потребоваться более полезное сообщение об ошибке, поэтому перехватывать его можно , если вы вызываете новое исключение с более приятным сообщением об ошибке (вы делаете более дружелюбное сообщение, но по-прежнему существует ошибка, которую вы не видите не имеет контекста для обработки), вместо того, чтобы молча возвращать мусор (возвращая что-либо, но функция здесь возвращает мусор). Таким образом, в этом случае вы можете изменить:

return f'First argument must be a number (tried {val})'

на:

raise ValueError(f'First argument must be a number (tried {val!r})')

, что вызывает исключение, которое не может случайно использоваться как функция (способ без ошибок возврат будет использоваться).

0 голосов
/ 11 января 2020

Проблема заключается в том, что ваш блок try / исключением в самой внешней функции вашего декоратора возвращает str, когда он ожидает вызова:

def deco(a=1):
    # here
    if isinstance(a, int):
        pass
    else:
        # this string is returned instead of the function inner_deco
        return 'An error occurred!'
    def inner_deco(f):
        def wrapper(*args, **kwargs):
            return f(*args, **kwargs)
        return wrapper

    # this never happens
    return inner_deco

Проверка типа не требуется на первом уровне сделайте это в функции wrapper:

def outer(a=1):
    def inner(f):
        def wrapper(*args, **kwargs):
            # Your error handling should happen here
            if isinstance(a, str):
                raise TypeError("I expected an int!")
            return a + f(*args, **kwargs)
        return wrapper
    return inner


@outer(a='2')
def error(b=1, c=2):
    return b + c


@outer(a=2)
def success(b=1, c=2):
    return b+c


error()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in wrapper
TypeError: I expected an int!


success()
5
...