Должен ли я принудительно проверять тип Python?

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

def orSearch(d, query):
    assert (type(d) == dict)
    assert (type(query) == list)

Должен ли я продолжать это делать? какие преимущества делать/не делать это?

Ответ 1

Прекрати это делать.

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

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

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

Есть и другие языки, которые теоретически предлагают то же самое: типизированные языки. Наиболее популярными являются C++ (с использованием шаблонов) и Haskell. Теоретически (и, возможно, на практике) вы можете в конечном итоге написать еще меньше кода, потому что типы разрешаются статически, поэтому вам не придется писать обработчики исключений, чтобы иметь дело с передачей неправильного типа. Я считаю, что они по-прежнему требуют, чтобы вы программировали для системы типов, а не для реальных типов в вашей программе (их системы типов являются доказателями теорем, и, чтобы их можно было отслеживать, они не анализируют всю вашу программу). Если вам это нравится, рассмотрите возможность использования одного из этих языков вместо python (или ruby, smalltalk или любого другого варианта lisp).

Вместо тестирования типов в python (или любом другом подобном динамическом языке) вы захотите использовать исключения, чтобы ловить, когда объект не поддерживает определенный метод. В этом случае, пусть он поднимется вверх по стеку или перехватит его и выдаст исключение о неподходящем типе. Этот тип кодирования "лучше просить прощения, чем разрешения" является идиоматическим питоном и значительно способствует упрощению кода.

* На практике. Изменения классов возможны в Python и Smalltalk, но редко. Это также не то же самое, что кастинг на языке низкого уровня.


Обновление: вы можете использовать mypy для статической проверки вашего питона вне производства. Аннотирование вашего кода, чтобы они могли проверить, что их код согласован, позволяет им делать это, если они хотят; или йоло, если они хотят.

Ответ 2

В большинстве случаев это мешало бы печатать на утке и с наследованием.

  • Наследование: вы наверняка намеревались написать что-то с эффектом

    assert isinstance(d, dict)
    

    чтобы убедиться, что ваш код также работает правильно с подклассами dict. Я думаю, это похоже на использование в Java. Но у Python есть что-то, чего нет у Java, а именно

  • Утиная печать: большинство встроенных функций не требуют, чтобы объект принадлежал определенному классу, только если он имеет определенные функции-члены, которые ведут себя правильно. Цикл for, например, требует только, чтобы переменная цикла была итерируемой, что означает, что она имеет функции-члены __iter__() и next(), и они ведут себя корректно.

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

Ответ 3

Если вы настаиваете на добавлении проверки типа в свой код, вы можете посмотреть в аннотации и как они могут упростить то, что вам нужно написать. Один из questions на StackOverflow представил небольшую, запутанную проверку типов, использующую аннотации. Вот пример, основанный на вашем вопросе:

>>> def statictypes(a):
    def b(a, b, c):
        if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c)))
        return c
    return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))

>>> @statictypes
def orSearch(d: dict, query: dict) -> type(None):
    pass

>>> orSearch({}, {})
>>> orSearch([], {})
Traceback (most recent call last):
  File "<pyshell#162>", line 1, in <module>
    orSearch([], {})
  File "<pyshell#155>", line 5, in <lambda>
    return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
  File "<pyshell#155>", line 5, in <listcomp>
    return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
  File "<pyshell#155>", line 3, in b
    if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c)))
TypeError: d should be <class 'dict'>, not <class 'list'>
>>> orSearch({}, [])
Traceback (most recent call last):
  File "<pyshell#163>", line 1, in <module>
    orSearch({}, [])
  File "<pyshell#155>", line 5, in <lambda>
    return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
  File "<pyshell#155>", line 5, in <listcomp>
    return __import__('functools').wraps(a)(lambda *c: b(a.__annotations__, 'return', a(*[b(a.__annotations__, *d) for d in zip(a.__code__.co_varnames, c)])))
  File "<pyshell#155>", line 3, in b
    if b in a and not isinstance(c, a[b]): raise TypeError('{} should be {}, not {}'.format(b, a[b], type(c)))
TypeError: query should be <class 'dict'>, not <class 'list'>
>>> 

Вы можете посмотреть на контролер и спросить: "Что это такое?" Я решил узнать для себя и превратил его в удобочитаемый код. Второй проект устранил функцию b (вы могли бы назвать ее verify). Третий и окончательный проект сделал несколько улучшений и показан ниже для вашего использования:

import functools

def statictypes(func):
    template = '{} should be {}, not {}'
    @functools.wraps(func)
    def wrapper(*args):
        for name, arg in zip(func.__code__.co_varnames, args):
            klass = func.__annotations__.get(name, object)
            if not isinstance(arg, klass):
                raise TypeError(template.format(name, klass, type(arg)))
        result = func(*args)
        klass = func.__annotations__.get('return', object)
        if not isinstance(result, klass):
            raise TypeError(template.format('return', klass, type(result)))
        return result
    return wrapper

Edit:

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

#! /usr/bin/env python3
import functools
import inspect


def static_types(wrapped):
    def replace(obj, old, new):
        return new if obj is old else obj

    signature = inspect.signature(wrapped)
    parameter_values = signature.parameters.values()
    parameter_names = tuple(parameter.name for parameter in parameter_values)
    parameter_types = tuple(
        replace(parameter.annotation, parameter.empty, object)
        for parameter in parameter_values
    )
    return_type = replace(signature.return_annotation, signature.empty, object)

    @functools.wraps(wrapped)
    def wrapper(*arguments):
        for argument, parameter_type, parameter_name in zip(
            arguments, parameter_types, parameter_names
        ):
            if not isinstance(argument, parameter_type):
                raise TypeError(f'{parameter_name} should be of type '
                                f'{parameter_type.__name__}, not '
                                f'{type(argument).__name__}')
        result = wrapped(*arguments)
        if not isinstance(result, return_type):
            raise TypeError(f'return should be of type '
                            f'{return_type.__name__}, not '
                            f'{type(result).__name__}')
        return result
    return wrapper

Ответ 4

Это неидиоматический способ делать вещи. Обычно в Python вы используете тесты try/except.

def orSearch(d, query):
    try:
        d.get(something)
    except TypeError:
        print("oops")
    try:
        foo = query[:2]
    except TypeError:
        print("durn")

Ответ 5

Лично у меня есть отвращение к утверждениям, кажется, что программист видит проблемы, но не может быть обеспокоен тем, как обращаться с ними, другая проблема заключается в том, что ваш пример будет утверждать, что любой параметр - это класс, полученный из те, которые вы ожидаете, хотя такие классы должны работать! - в вашем примере выше я бы пошел на что-то вроде:

def orSearch(d, query):
    """ Description of what your function does INCLUDING parameter types and descriptions """
    result = None
    if not isinstance(d, dict) or not isinstance(query, list):
        print "An Error Message"
        return result
    ...

Тип записи совпадает только в том случае, если тип точно соответствует ожидаемому, isinstance также работает для производных классов. например:.

>>> class dd(dict):
...    def __init__(self):
...        pass
... 
>>> d1 = dict()
>>> d2 = dd()
>>> type(d1)
<type 'dict'>
>>> type(d2)
<class '__main__.dd'>
>>> type (d1) == dict
True
>>> type (d2) == dict
False
>>> isinstance(d1, dict)
True
>>> isinstance(d2, dict)
True
>>> 

Вы можете подумать о том, чтобы выбрасывать пользовательское исключение, а не утверждение. Вы даже можете обобщить еще больше, проверив, что параметры имеют нужные вам методы.

BTW Это может быть изящно для меня, но я всегда стараюсь избегать утверждения в C/С++ на том основании, что если он останется в коде, то кто-то через несколько лет внесет изменения, которые должен быть уловлен им, а не проверять его достаточно хорошо для отладки, чтобы это произошло (или даже вообще не тестировало его), скомпилировать как готовый, режим выпуска, - который удаляет все утверждения, т.е. все проверки ошибок, которые были сделаны, что и теперь у нас есть ненадежный код и серьезная головная боль, чтобы найти проблемы.

Ответ 6

Я согласен с подходом Стива, когда вам нужно провести проверку типов. Я не часто нахожу необходимость выполнять проверку типов в Python, но есть, по крайней мере, одна ситуация, когда я это делаю. Вот где не проверять тип может вернуть неправильный ответ, который приведет к ошибке позже в вычислении. Эти ошибки могут быть трудно отследить, и я много раз сталкивался с ними на Python. Как и вы, я сначала изучил Java, и мне не приходилось иметь дело с ними часто.

Скажем, у вас была простая функция, которая ожидает массив и возвращает первый элемент.

def func(arr): return arr[0]

если вы вызываете его с помощью массива, вы получите первый элемент массива.

>>> func([1,2,3])
1

Вы также получите ответ, если вы вызываете его строкой или объектом любого класса, который реализует getitem магический метод.

>>> func("123")
'1'

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

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

Ответ 7

Две вещи.

Во-первых, если вы готовы потратить ~ 200 долларов, вы можете получить довольно хорошую среду для python. Я использую PyCharm и действительно впечатлен. (Это те же люди, которые делают ReSharper для С#.) Он проанализирует ваш код при его написании и ищет места, где переменные имеют неправильный тип (среди кучи других вещей).

Во-вторых:

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

Назовите его так:

@require_type('paramA', str)
@require_type('paramB', list)
@require_type('paramC', collections.Counter)
def my_func(paramA, paramB, paramC):
    paramB.append(paramC[paramA].most_common())
    return paramB

Во всяком случае, вот код декоратора.

def require_type(my_arg, *valid_types):
    '''
        A simple decorator that performs type checking.

        @param my_arg: string indicating argument name
        @param valid_types: *list of valid types
    '''
    def make_wrapper(func):
        if hasattr(func, 'wrapped_args'):
            wrapped = getattr(func, 'wrapped_args')
        else:
            body = func.func_code
            wrapped = list(body.co_varnames[:body.co_argcount])

        try:
            idx = wrapped.index(my_arg)
        except ValueError:
            raise(NameError, my_arg)

        def wrapper(*args, **kwargs):

            def fail():
                all_types = ', '.join(str(typ) for typ in valid_types)
                raise(TypeError, '\'%s\' was type %s, expected to be in following list: %s' % (my_arg, all_types, type(arg)))

            if len(args) > idx:
                arg = args[idx]
                if not isinstance(arg, valid_types):
                    fail()
            else:
                if my_arg in kwargs:
                    arg = kwargs[my_arg]
                    if not isinstance(arg, valid_types):
                        fail()

            return func(*args, **kwargs)

        wrapper.wrapped_args = wrapped
        return wrapper
    return make_wrapper