Лучший способ проверить аргументы функции в Python

Я ищу эффективный способ проверки переменных функции python. Например, я хотел бы проверить тип и значение аргументов. Есть ли модуль для этого? Или я должен использовать что-то вроде декораторов или какой-либо конкретной идиомы?

def my_function(a, b, c):
    """an example function I'd like to check the arguments of."""
    # check that a is an int
    # check that 0 < b < 10
    # check that c is not an empty string

Ответы

Ответ 1

Самый Pythonic-идиома состоит в том, чтобы четко документировать то, что ожидает функция, а затем просто попытаться использовать все, что передается вашей функции, и либо разрешить распространение исключений, либо просто уловить ошибки атрибутов и вместо этого поднять TypeError. Следует избегать проверки типов, насколько это возможно, в том, что касается утиного ввода текста. Проверка ценности может быть ОК - в зависимости от контекста.

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

Ответ 2

В этом расширенном ответе мы реализуем специфический для Python 3.x декоратор проверки типа, основанный на подсказках типа PEP 484 -style, содержащих менее 275 строк чистого Python (большинство из которых - пояснительные строки документации и комментарии ) - в значительной степени оптимизированы для реального промышленного применения в сочетании с набором тестов py.test -driven, в котором используются все возможные крайние варианты.

Насладитесь неожиданным набором символов bear typing:

>>> @beartype
... def spirit_bear(kermode: str, gitgaata: (str, int)) -> tuple:
...     return (kermode, gitgaata, "Moksgm'ol", 'Ursus americanus kermodei')
>>> spirit_bear(0xdeadbeef, 'People of the Cane')
AssertionError: parameter kermode=0xdeadbeef not of <class "str">

Как показывает этот пример, медвежья типизация явно поддерживает проверку типов параметров и возвращаемых значений, аннотированных как простые типы или кортежи таких типов. Golly!

О.К., это на самом деле не впечатляет. @beartype напоминает любой другой специфический для Python 3.x декоратор проверки типа, основанный на подсказках типа PEP 484 -style в менее чем 275 строках чистого Python. Так что за руб, баб?

Pure Bruteforce Hardcore Efficiency

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

Однако в Python эффективность обычно не имеет значения. Если бы это было так, вы бы не использовали Python. Действительно ли проверка типов отклоняется от устоявшейся нормы избегания преждевременной оптимизации в Python? Да. Да, это так.

Рассмотрим профилирование, которое добавляет неизбежные накладные расходы к каждой интересующей профилированной метрике (например, вызовам функций, строкам). Чтобы гарантировать точные результаты, эти издержки уменьшаются за счет использования оптимизированных расширений C (например, расширения _lsprof C, используемых модулем cProfile), а не неоптимизированного чистого Python (например, модуля profile). Эффективность действительно имеет значение при профилировании.

Проверка типов ничем не отличается. Проверка типа добавляет накладные расходы к каждому типу вызова функции, проверяемому вашим приложением - в идеале, все они. Чтобы не дать добросовестным (но, к сожалению, недалеким) коллегам убрать проверку типов, которую вы молча добавили после прошлой пятницы с добавлением кофеина allnighter в старое веб-приложение Django, предназначенное для пожилых людей, проверка типов должна быть быстрой. Так быстро что никто не замечает этого там, когда вы добавляете это, никому не говоря I do this all the time! Stop reading this if you are a coworker.

Если даже нелепой скорости недостаточно для вашего прожорливого приложения, однако, медвежья типизация может быть глобально отключена путем включения оптимизации Python (например, путем передачи опции -O интерпретатору Python):

$ python3 -O
# This succeeds only when type checking is optimized away. See above!
>>> spirit_bear(0xdeadbeef, 'People of the Cane')
(0xdeadbeef, 'People of the Cane', "Moksgm'ol", 'Ursus americanus kermodei')

Просто потому что. Добро пожаловать на печатную машинку.

Что за...? Почему "медведь"? Ты Шея Борода, верно?

Медвежья типизация - это проверка типа с нуля, то есть проверка типа, максимально приближенная к ручному подходу проверки типов в Python. Медведь типизации предназначен не налагать никаких потерь производительности, ограничений совместимости или сторонних зависимостей (в любом случае, сверх того, что навязывается ручным подходом). Печатание медведей может быть без проблем интегрировано в существующие кодовые базы и тестовые наборы.

Все, наверное, знакомы с ручным подходом. Вы вручную assert передаете каждый параметр и/или возвращаете значение, возвращаемое каждой функцией в вашей кодовой базе. Какой шаблон может быть проще или банальнее? Мы все видели это сто раз в googleplex раз, и каждый раз, когда мы это делали, нас рвало по рту. Повторение быстро стареет. СУХОЙ, лет

Приготовь рвотные мешки. Для краткости предположим, что упрощенная функция easy_spirit_bear() принимает только один параметр str. Вот как выглядит ручной подход:

def easy_spirit_bear(kermode: str) -> str:
    assert isinstance(kermode, str), 'easy_spirit_bear() parameter kermode={} not of <class "str">'.format(kermode)
    return_value = (kermode, "Moksgm'ol", 'Ursus americanus kermodei')
    assert isinstance(return_value, str), 'easy_spirit_bear() return value {} not of <class "str">'.format(return_value)
    return return_value

Python 101, верно? Многие из нас прошли этот класс.

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

def easy_spirit_bear_wrapper(*args, __beartype_func=easy_spirit_bear, **kwargs):
    if not (
        isinstance(args[0], __beartype_func.__annotations__['kermode'])
        if 0 < len(args) else
        isinstance(kwargs['kermode'], __beartype_func.__annotations__['kermode'])
        if 'kermode' in kwargs else True):
            raise TypeError(
                'easy_spirit_bear() parameter kermode={} not of {!r}'.format(
                args[0] if 0 < len(args) else kwargs['kermode'],
                __beartype_func.__annotations__['kermode']))

    return_value = __beartype_func(*args, **kwargs)

    if not isinstance(return_value, __beartype_func.__annotations__['return']):
        raise TypeError(
            'easy_spirit_bear() return value {} not of {!r}'.format(
                return_value, __beartype_func.__annotations__['return']))

    return return_value

Это многословно. Но это также в основном * так же быстро, как и ручной подход. * Squinting suggested.

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

Могут ли такие функции-обертки действительно генерироваться для проверки типов произвольных функций менее чем в 275 строках чистого Python? Снейк Плисскин говорит: "Правдивая история. Есть дым?"

И да. У меня может быть шея.

Нет, Срсли Почему "медведь"?

Медведь бьет утку. Утка может летать, но медведь может бросить лосося в утку. In Canada, nature can surprise you.

Следующий вопрос.

Что же так горячо в медведях?

Существующие решения не выполняют проверку типа "голый металл" - по крайней мере, ни одно из них я не рассмотрел. Все они итеративно повторно проверяют сигнатуру проверенной на тип функции в при каждом вызове функции. Несмотря на то, что затраты на повторный контроль незначительны для одного вызова, они обычно ничтожны, когда агрегируются по всем вызовам. Действительно, действительно ничтожно мало.

Однако это касается не просто эффективности. Существующие решения также часто не учитывают общие крайние случаи. Это включает в себя большинство, если не все игрушечные декораторы, предоставленные как ответы stackoverflow здесь и в других местах. Классические ошибки включают в себя:

  • Не удается набрать контрольные ключевые аргументы и/или возвращаемые значения (например, sweeneyrod@checkargs decorator).
  • Неспособность поддерживать кортежи (т.е. объединения) типов, принятых встроенным isinstance().
  • Не удалось распространить имя, строку документации и другие идентифицирующие метаданные из исходной функции в функцию-оболочку.
  • Неспособность предоставить хотя бы видимость модульных тестов. (Вид критического.)
  • Повышение общих исключений AssertionError, а не конкретных исключений TypeError при неудачных проверках типов. Для детализации и разумности проверка типов никогда не должна вызывать общих исключений.

Медведь печатает успешно там, где не медведи терпят неудачу. Все один, все медведь!

Медведь печатает без обнаженного

Типографский набор переносит пространственные и временные затраты на проверку сигнатур функций от времени вызова функции до времени определения функции, то есть от функции-оболочки, возвращаемой декоратором @beartype, в сам декоратор. Поскольку декоратор вызывается только один раз для каждого определения функции, эта оптимизация дает радость для всех.

Печатание с помощью медведя - это попытка съесть свой торт для проверки типа и съесть его. Для этого @beartype:

  1. Проверяет подпись и аннотации исходной функции.
  2. Динамически создает тело типа функции-оболочки, проверяя исходную функцию. Даааа Python-код, генерирующий Python-код.
  3. Динамически объявляет эту функцию-обертку через встроенную функцию exec().
  4. Возвращает эту функцию-обертку.

А не ___ ли нам? Давай погрузимся в глубокий конец.

# If the active Python interpreter is *NOT* optimized (e.g., option "-O" was
# *NOT* passed to this interpreter), enable type checking.
if __debug__:
    import inspect
    from functools import wraps
    from inspect import Parameter, Signature

    def beartype(func: callable) -> callable:
        '''
        Decorate the passed **callable** (e.g., function, method) to validate
        both all annotated parameters passed to this callable _and_ the
        annotated value returned by this callable if any.

        This decorator performs rudimentary type checking based on Python 3.x
        function annotations, as officially documented by PEP 484 ("Type
        Hints"). While PEP 484 supports arbitrarily complex type composition,
        this decorator requires _all_ parameter and return value annotations to
        be either:

        * Classes (e.g., 'int', 'OrderedDict').
        * Tuples of classes (e.g., '(int, OrderedDict)').

        If optimizations are enabled by the active Python interpreter (e.g., due
        to option '-O' passed to this interpreter), this decorator is a noop.

        Raises
        ----------
        NameError
            If any parameter has the reserved name '__beartype_func'.
        TypeError
            If either:
            * Any parameter or return value annotation is neither:
              * A type.
              * A tuple of types.
            * The kind of any parameter is unrecognized. This should _never_
              happen, assuming no significant changes to Python semantics.
        '''

        # Raw string of Python statements comprising the body of this wrapper,
        # including (in order):
        #
        # * A "@wraps" decorator propagating the name, docstring, and other
        #   identifying metadata of the original function to this wrapper.
        # * A private "__beartype_func" parameter initialized to this function.
        #   In theory, the "func" parameter passed to this decorator should be
        #   accessible as a closure-style local in this wrapper. For unknown
        #   reasons (presumably, a subtle bug in the exec() builtin), this is
        #   not the case. Instead, a closure-style local must be simulated by
        #   passing the "func" parameter to this function at function
        #   definition time as the default value of an arbitrary parameter. To
        #   ensure this default is *NOT* overwritten by a function accepting a
        #   parameter of the same name, this edge case is tested for below.
        # * Assert statements type checking parameters passed to this callable.
        # * A call to this callable.
        # * An assert statement type checking the value returned by this
        #   callable.
        #
        # While there exist numerous alternatives (e.g., appending to a list or
        # bytearray before joining the elements of that iterable into a string),
        # these alternatives are either slower (as in the case of a list, due to
        # the high up-front cost of list construction) or substantially more
        # cumbersome (as in the case of a bytearray). Since string concatenation
        # is heavily optimized by the official CPython interpreter, the simplest
        # approach is (curiously) the most ideal.
        func_body = '''
@wraps(__beartype_func)
def func_beartyped(*args, __beartype_func=__beartype_func, **kwargs):
'''

        # "inspect.Signature" instance encapsulating this callable signature.
        func_sig = inspect.signature(func)

        # Human-readable name of this function for use in exceptions.
        func_name = func.__name__ + '()'

        # For the name of each parameter passed to this callable and the
        # "inspect.Parameter" instance encapsulating this parameter (in the
        # passed order)...
        for func_arg_index, func_arg in enumerate(func_sig.parameters.values()):
            # If this callable redefines a parameter initialized to a default
            # value by this wrapper, raise an exception. Permitting this
            # unlikely edge case would permit unsuspecting users to
            # "accidentally" override these defaults.
            if func_arg.name == '__beartype_func':
                raise NameError(
                    'Parameter {} reserved for use by @beartype.'.format(
                        func_arg.name))

            # If this parameter is both annotated and non-ignorable for purposes
            # of type checking, type check this parameter.
            if (func_arg.annotation is not Parameter.empty and
                func_arg.kind not in _PARAMETER_KIND_IGNORED):
                # Validate this annotation.
                _check_type_annotation(
                    annotation=func_arg.annotation,
                    label='{} parameter {} type'.format(
                        func_name, func_arg.name))

                # String evaluating to this parameter annotated type.
                func_arg_type_expr = (
                    '__beartype_func.__annotations__[{!r}]'.format(
                        func_arg.name))

                # String evaluating to this parameter current value when
                # passed as a keyword.
                func_arg_value_key_expr = 'kwargs[{!r}]'.format(func_arg.name)

                # If this parameter is keyword-only, type check this parameter
                # only by lookup in the variadic "**kwargs" dictionary.
                if func_arg.kind is Parameter.KEYWORD_ONLY:
                    func_body += '''
    if {arg_name!r} in kwargs and not isinstance(
        {arg_value_key_expr}, {arg_type_expr}):
        raise TypeError(
            '{func_name} keyword-only parameter '
            '{arg_name}={{}} not a {{!r}}'.format(
                {arg_value_key_expr}, {arg_type_expr}))
'''.format(
                        func_name=func_name,
                        arg_name=func_arg.name,
                        arg_type_expr=func_arg_type_expr,
                        arg_value_key_expr=func_arg_value_key_expr,
                    )
                # Else, this parameter may be passed either positionally or as
                # a keyword. Type check this parameter both by lookup in the
                # variadic "**kwargs" dictionary *AND* by index into the
                # variadic "*args" tuple.
                else:
                    # String evaluating to this parameter current value when
                    # passed positionally.
                    func_arg_value_pos_expr = 'args[{!r}]'.format(
                        func_arg_index)

                    func_body += '''
    if not (
        isinstance({arg_value_pos_expr}, {arg_type_expr})
        if {arg_index} < len(args) else
        isinstance({arg_value_key_expr}, {arg_type_expr})
        if {arg_name!r} in kwargs else True):
            raise TypeError(
                '{func_name} parameter {arg_name}={{}} not of {{!r}}'.format(
                {arg_value_pos_expr} if {arg_index} < len(args) else {arg_value_key_expr},
                {arg_type_expr}))
'''.format(
                    func_name=func_name,
                    arg_name=func_arg.name,
                    arg_index=func_arg_index,
                    arg_type_expr=func_arg_type_expr,
                    arg_value_key_expr=func_arg_value_key_expr,
                    arg_value_pos_expr=func_arg_value_pos_expr,
                )

        # If this callable return value is both annotated and non-ignorable
        # for purposes of type checking, type check this value.
        if func_sig.return_annotation not in _RETURN_ANNOTATION_IGNORED:
            # Validate this annotation.
            _check_type_annotation(
                annotation=func_sig.return_annotation,
                label='{} return type'.format(func_name))

            # Strings evaluating to this parameter annotated type and
            # currently passed value, as above.
            func_return_type_expr = (
                "__beartype_func.__annotations__['return']")

            # Call this callable, type check the returned value, and return this
            # value from this wrapper.
            func_body += '''
    return_value = __beartype_func(*args, **kwargs)
    if not isinstance(return_value, {return_type}):
        raise TypeError(
            '{func_name} return value {{}} not of {{!r}}'.format(
                return_value, {return_type}))
    return return_value
'''.format(func_name=func_name, return_type=func_return_type_expr)
        # Else, call this callable and return this value from this wrapper.
        else:
            func_body += '''
    return __beartype_func(*args, **kwargs)
'''

        # Dictionary mapping from local attribute name to value. For efficiency,
        # only those local attributes explicitly required in the body of this
        # wrapper are copied from the current namespace. (See below.)
        local_attrs = {'__beartype_func': func}

        # Dynamically define this wrapper as a closure of this decorator. For
        # obscure and presumably uninteresting reasons, Python fails to locally
        # declare this closure when the locals() dictionary is passed; to
        # capture this closure, a local dictionary must be passed instead.
        exec(func_body, globals(), local_attrs)

        # Return this wrapper.
        return local_attrs['func_beartyped']

    _PARAMETER_KIND_IGNORED = {
        Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD,
    }
    '''
    Set of all 'inspect.Parameter.kind' constants to be ignored during
    annotation- based type checking in the '@beartype' decorator.

    This includes:

    * Constants specific to variadic parameters (e.g., '*args', '**kwargs').
      Variadic parameters cannot be annotated and hence cannot be type checked.
    * Constants specific to positional-only parameters, which apply to non-pure-
      Python callables (e.g., defined by C extensions). The '@beartype'
      decorator applies _only_ to pure-Python callables, which provide no
      syntactic means of specifying positional-only parameters.
    '''

    _RETURN_ANNOTATION_IGNORED = {Signature.empty, None}
    '''
    Set of all annotations for return values to be ignored during annotation-
    based type checking in the '@beartype' decorator.

    This includes:

    * 'Signature.empty', signifying a callable whose return value is _not_
      annotated.
    * 'None', signifying a callable returning no value. By convention, callables
      returning no value are typically annotated to return 'None'. Technically,
      callables whose return values are annotated as 'None' _could_ be
      explicitly checked to return 'None' rather than a none-'None' value. Since
      return values are safely ignorable by callers, however, there appears to
      be little real-world utility in enforcing this constraint.
    '''

    def _check_type_annotation(annotation: object, label: str) -> None:
        '''
        Validate the passed annotation to be a valid type supported by the
        '@beartype' decorator.

        Parameters
        ----------
        annotation : object
            Annotation to be validated.
        label : str
            Human-readable label describing this annotation, interpolated into
            exceptions raised by this function.

        Raises
        ----------
        TypeError
            If this annotation is neither a new-style class nor a tuple of
            new-style classes.
        '''

        # If this annotation is a tuple, raise an exception if any member of
        # this tuple is not a new-style class. Note that the "__name__"
        # attribute tested below is not defined by old-style classes and hence
        # serves as a helpful means of identifying new-style classes.
        if isinstance(annotation, tuple):
            for member in annotation:
                if not (
                    isinstance(member, type) and hasattr(member, '__name__')):
                    raise TypeError(
                        '{} tuple member {} not a new-style class'.format(
                            label, member))
        # Else if this annotation is not a new-style class, raise an exception.
        elif not (
            isinstance(annotation, type) and hasattr(annotation, '__name__')):
            raise TypeError(
                '{} {} neither a new-style class nor '
                'tuple of such classes'.format(label, annotation))

# Else, the active Python interpreter is optimized. In this case, disable type
# checking by reducing this decorator to the identity decorator.
else:
    def beartype(func: callable) -> callable:
        return func

И Лейчек сказал: пусть @beartype быстро проведет проверку типов: и так было.

Предостережения, проклятия и пустые обещания

Ничто не совершенно. Даже медведь печатает.

Предупреждение I: значения по умолчанию сняты

Медвежья типография не печатает, проверяет непазданные параметры, присвоенные значения по умолчанию Теоретически это возможно. Но не в 275 строках или меньше, и, конечно, не в качестве ответа на переполнение стека.

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

Покажи мне неправильно, и я засыплю тебя голосами.

Предостережение II: Нет PEP 484

PEP 484 ("Типовые подсказки") формализовало использование аннотаций функций, впервые введенных в PEP 3107 ("Аннотации функций"). Python 3.5 поверхностно поддерживает эту формализацию с помощью нового модуля typing верхнего уровня, стандартного API для составления произвольно сложных типов из более простых типов (например, Callable[[Arg1Type, Arg2Type], ReturnType], типа, описывающего функцию, принимающую два аргумента: введите Arg1Type и Arg2Type и верните значение типа ReturnType).

Медведь печатать не поддерживает ни один из них. Теоретически это возможно. Но не в 275 или менее строках и, конечно, не в качестве ответа на переполнение стека.

Однако типизация медведей поддерживает объединения типов так же, как встроенная функция isinstance() поддерживает объединения типов: как кортежи. Это внешне соответствует типу typing.Union - с очевидным предостережением, что typing.Union поддерживает произвольно сложные типы, в то время как кортежи, принятые в @beartype, поддерживают только простые классы. В мою защиту 275 строк.

Тесты или не случилось

Вот суть gist этого. Get it, gist? I'll stop now.

Как и в случае самого декоратора @beartype, эти тесты py.test могут быть без проблем интегрированы в существующие тестовые наборы. Драгоценно, не так ли?

Теперь об обязательной напыщенной шее бороды никто не просил.

История насилия API

Python 3.5 не обеспечивает реальной поддержки использования типов PEP 484. ват?

Это правда: нет проверки типов, нет вывода типов, нет типов nuthin '. Вместо этого ожидается, что разработчики будут регулярно запускать все свои кодовые базы через сторонние обертки интерпретатора CPython, реализующие факсимиле такой поддержки (например, mypy). Конечно, эти обертки навязывают:

  • A штраф за совместимость. Как признается в официальном FAQ по Dpy official mypy FAQ в ответ на часто задаваемый вопрос "Могу ли я использовать mypy для проверки типа моего существующего кода Python?": "Зависит от. Совместимость довольно хорошая, но некоторые функции Python еще не реализованы или не полностью поддерживаются ". последующий ответ на часто задаваемые вопросы разъясняет эту несовместимость, утверждая, что:
    • "... ваш код должен делать атрибуты явными и использовать явное представление протокола". Grammar police see your "a explicit" and raise you an implicit frown.
    • "Mypy будет поддерживать модульную, эффективную проверку типов, и это, по-видимому, исключает проверку типов в некоторых языковых функциях, таких как произвольное добавление методов во время выполнения. Однако вполне вероятно, что многие из этих функций будут поддерживаться в ограниченной форме (например, изменение во время выполнения поддерживается только для классов или методов, зарегистрированных как динамические или "исправляемые". "
    • Полный список синтаксических несовместимостей см. в "Решение общих проблем". Это не красиво. Вы просто хотели проверить тип, и теперь вы реорганизовали всю свою кодовую базу и сломали все сборки за два дня с момента релиза кандидата, и приятный миниатюрный специалист по персоналу в повседневной деловой одежде проскользнул сквозь щель в вашем кабинете. Большое спасибо, Mypy.
  • снижение производительности, несмотря на интерпретацию статически типизированного кода. Сорок лет бурной информатики говорят нам, что (при прочих равных условиях) интерпретация статически типизированного кода должна быть быстрее, а не медленнее, чем интерпретация динамически типизированного кода. В Python вверх - это новый вниз.
  • Дополнительные нетривиальные зависимости, увеличивающиеся:
    • Хрупкая ошибка развертывания проекта, особенно кроссплатформенная.
    • Бремя обслуживания разработки проекта.
    • Возможная поверхность атаки.

Я спрашиваю Гвидо: "Почему? Зачем изобретать абстрактный API, если вы не хотите создавать конкретный API, который действительно что-то делает с этой абстракцией?" Зачем оставлять судьбу миллиона Pythonistas артритической руке свободного рынка с открытым исходным кодом? Зачем создавать еще одну техно-проблему, которую можно было бы тривиально решить с помощью 275-строчного декоратора в официальном stdlib Python?

У меня нет Питона, и я должен кричать.

Ответ 3

Изменение: с 2019 года больше поддержки для использования аннотаций типов и статической проверки в Python; проверьте модуль ввода и mypy. Ответ 2013 года следующий:


Проверка типов обычно не Pythonic. В Python более привычно использовать утку, набирающую. Пример:

В вашем коде предположим, что аргумент (в вашем примере a) идет как int и крякает как int. Например:

def my_function(a):
    return a + 7

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

def my_function(a):
    b = int(a) + 7
    c = (5, 6, 3, 123541)[b]
    return c

и функция по-прежнему работает для любого a, который определяет метод __int__.

В ответ на ваши другие вопросы, я думаю, что это лучше всего (как и другие ответы сказали, чтобы сделать это:

def my_function(a, b, c):
    assert 0 < b < 10
    assert c        # A non-empty string has the Boolean value True

или

def my_function(a, b, c):
    if 0 < b < 10:
        # Do stuff with b
    else:
        raise ValueError
    if c:
        # Do stuff with c
    else:
        raise ValueError

Я сделал несколько декораторов для проверки типов:

import inspect

def checkargs(function):
    def _f(*arguments):
        for index, argument in enumerate(inspect.getfullargspec(function)[0]):
            if not isinstance(arguments[index], function.__annotations__[argument]):
                raise TypeError("{} is not of type {}".format(arguments[index], function.__annotations__[argument]))
        return function(*arguments)
    _f.__doc__ = function.__doc__
    return _f

def coerceargs(function):
    def _f(*arguments):
        new_arguments = []
        for index, argument in enumerate(inspect.getfullargspec(function)[0]):
            new_arguments.append(function.__annotations__[argument](arguments[index]))
        return function(*new_arguments)
    _f.__doc__ = function.__doc__
    return _f

if __name__ == "__main__":
    @checkargs
    def f(x: int, y: int):
        """
        A doc string!
        """
        return x, y

    @coerceargs
    def g(a: int, b: int):
        """
        Another doc string!
        """
        return a + b

    print(f(1, 2))
    try:
        print(f(3, 4.0))
    except TypeError as e:
        print(e)

    print(g(1, 2))
    print(g(3, 4.0))

Ответ 4

Один из способов - использовать assert:

def myFunction(a,b,c):
    "This is an example function I'd like to check arguments of"
    assert isinstance(a, int), 'a should be an int'
    # or if you want to allow whole number floats: assert int(a) == a
    assert b > 0 and b < 10, 'b should be betwen 0 and 10'
    assert isinstance(c, str) and c, 'c should be a non-empty string'

Ответ 5

Вы можете использовать атрибуты принятия/возврата типа Enforcement от PythonDecoratorLibrary Это очень легко и удобно:

@accepts(int, int, float)
def myfunc(i1, i2, i3):
    pass

Ответ 6

Существуют различные способы проверить, что такое переменная в Python. Итак, чтобы перечислить несколько:

  • isinstance(obj, type) функция принимает вашу переменную obj и дает вам True - это тот же самый тип type, который вы указали.

  • issubclass(obj, class), которая принимает переменную obj и дает вам True, если obj является подклассом class. Например, issubclass(Rabbit, Animal) даст вам значение True

  • hasattr - еще один пример, продемонстрированный этой функцией, super_len:


def super_len(o):
    if hasattr(o, '__len__'):
        return len(o)

    if hasattr(o, 'len'):
        return o.len

    if hasattr(o, 'fileno'):
        try:
            fileno = o.fileno()
        except io.UnsupportedOperation:
            pass
        else:
            return os.fstat(fileno).st_size

    if hasattr(o, 'getvalue'):
        # e.g. BytesIO, cStringIO.StringI
        return len(o.getvalue())

hasattr больше склоняется к утиновому типу и тому, что обычно более питонично, но этот термин склонен к самоуверенности.

Как примечание, операторы assert обычно используются при тестировании, иначе просто используйте инструкции if/else.

Ответ 7

Обычно вы делаете что-то вроде этого:

def myFunction(a,b,c):
   if not isinstance(a, int):
      raise TypeError("Expected int, got %s" % (type(a),))
   if b <= 0 or b >= 10:
      raise ValueError("Value %d out of range" % (b,))
   if not c:
      raise ValueError("String was empty")

   # Rest of function

Ответ 8

В последнее время я довольно много расследовал эту тему, так как меня не устраивали многие библиотеки, которые я обнаружил там.

Я закончил разработку библиотеки для решения этой проблемы, она называется valid8. Как поясняется в документации, в основном это проверка важности (хотя она поставляется вместе с простыми функциями проверки типов), и вы можете связать ее с помощью проверки типа на основе PEP484, например enforce или pytypes.

Вот как вы могли бы выполнить валидацию только с valid8mini_lambda, чтобы определить логику проверки - но это не обязательно) в вашем случае:

# for type validation
from numbers import Integral
from valid8 import instance_of

# for value validation
from valid8 import validate_arg
from mini_lambda import x, s, Len

@validate_arg('a', instance_of(Integral))
@validate_arg('b', (0 < x) & (x < 10))
@validate_arg('c', instance_of(str), Len(s) > 0)
def my_function(a: Integral, b, c: str):
    """an example function I'd like to check the arguments of."""
    # check that a is an int
    # check that 0 < b < 10
    # check that c is not an empty string

# check that it works
my_function(0.2, 1, 'r')  # InputValidationError for 'a' HasWrongType: Value should be an instance of <class 'numbers.Integral'>. Wrong value: [0.2].
my_function(0, 0, 'r')    # InputValidationError for 'b' [(x > 0) & (x < 10)] returned [False]
my_function(0, 1, 0)      # InputValidationError for 'c' Successes: [] / Failures: {"instance_of_<class 'str'>": "HasWrongType: Value should be an instance of <class 'str'>. Wrong value: [0]", 'len(s) > 0': "TypeError: object of type 'int' has no len()"}.
my_function(0, 1, '')     # InputValidationError for 'c' Successes: ["instance_of_<class 'str'>"] / Failures: {'len(s) > 0': 'False'}

И это тот же пример с использованием подсказок типа PEP484 и проверки типа делегирования на enforce:

# for type validation
from numbers import Integral
from enforce import runtime_validation, config
config(dict(mode='covariant'))  # type validation will accept subclasses too

# for value validation
from valid8 import validate_arg
from mini_lambda import x, s, Len

@runtime_validation
@validate_arg('b', (0 < x) & (x < 10))
@validate_arg('c', Len(s) > 0)
def my_function(a: Integral, b, c: str):
    """an example function I'd like to check the arguments of."""
    # check that a is an int
    # check that 0 < b < 10
    # check that c is not an empty string

# check that it works
my_function(0.2, 1, 'r')  # RuntimeTypeError 'a' was not of type <class 'numbers.Integral'>
my_function(0, 0, 'r')    # InputValidationError for 'b' [(x > 0) & (x < 10)] returned [False]
my_function(0, 1, 0)      # RuntimeTypeError 'c' was not of type <class 'str'>
my_function(0, 1, '')     # InputValidationError for 'c' [len(s) > 0] returned [False].

Ответ 9

Это проверяет тип входных аргументов при вызове функции:

def func(inp1:int=0,inp2:str="*"):

    for item in func.__annotations__.keys():
        assert isinstance(locals()[item],func.__annotations__[item])

    return (something)

first=7
second="$"
print(func(first,second))

Также проверьте с помощью second=9 (он должен дать ошибку утверждения)

Ответ 10

Если вы хотите проверить **kwargs, *args, а также обычные аргументы за один раз, вы можете использовать функцию locals() в качестве первого оператора в определении функции, чтобы получить словарь аргументов.

Затем используйте type(), чтобы проверить аргументы, например, итерации по dict.

def myfunc(my, args, to, this, function, **kwargs):
    d = locals()
    assert(type(d.get('x')) == str)
    for x in d:
        if x != 'x':
            assert(type(d[x]) == x
    for x in ['a','b','c']:
        assert(x in d)

    whatever more...

Ответ 11

def someFunc(a, b, c):
    params = locals()
    for _item in params:
        print type(params[_item]), _item, params[_item]

Демо:

>> someFunc(1, 'asd', 1.0)
>> <type 'int'> a 1
>> <type 'float'> c 1.0
>> <type 'str'> b asd

подробнее о locals()

Ответ 12

Если вы хотите сделать валидацию для нескольких функций, вы можете добавить логику внутри декоратора следующим образом:

def deco(func):
     def wrapper(a,b,c):
         if not isinstance(a, int)\
            or not isinstance(b, int)\
            or not isinstance(c, str):
             raise TypeError
         if not 0 < b < 10:
             raise ValueError
         if c == '':
             raise ValueError
         return func(a,b,c)
     return wrapper

и используйте его:

@deco
def foo(a,b,c):
    print 'ok!'

Надеюсь, это поможет!

Ответ 13

Это не решение для вас, но если вы хотите ограничить вызовы функций некоторыми определенными типами параметров, вы должны использовать PROATOR {The validator prototype validator}. вы можете ссылаться на следующую ссылку. https://github.com/mohit-thakur-721/proator

Ответ 14

def myFunction(a,b,c):
"This is an example function I'd like to check arguments of"
    if type( a ) == int:
       #dostuff
    if 0 < b < 10:
       #dostuff
    if type( C ) == str and c != "":
       #dostuff