我应该强制Python类型检查吗?

Should I force Python type checking?

也许作为我的一个强大的类型语言(Java)的残余,我经常发现自己编写函数,然后强制类型检查。例如:

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

我应该继续这样做吗?这样做/不这样做有什么好处?


在大多数情况下,它会干扰duck类型和继承。

  • 继承:你当然打算写一些有

    1
    assert isinstance(d, dict)

    以确保您的代码也能正确地与dict的子类一起工作。我想这和爪哇的用法相似。但是Python有Java没有的东西,即

  • duck-typing:大多数内置函数不要求对象属于特定的类,只要求它具有某些成员函数,这些成员函数的行为是正确的。例如,for循环只要求循环变量是一个iterable,这意味着它具有成员函数__iter__()next()并且它们的行为是正确的。

因此,如果您不想完全关闭Python的大门,就不要检查生产代码中的特定类型。(不过,它可能对调试有用。)


别这样。

使用"动态"语言(即强类型化的值*、非类型化的变量和后期绑定)的要点是,您的函数可以适当地具有多态性,因为它们将处理支持您的函数所依赖的接口的任何对象("duck typing")。

python定义了许多公共协议(例如iterable),不同类型的对象可以实现这些协议,而不必相互关联。协议本身不是语言特征(不像Java接口)。

实际的结果是,一般来说,只要你理解你的语言中的类型,并且你适当地评论(包括使用docstring,这样其他人也能理解你程序中的类型),你就可以编写更少的代码,因为你不必围绕你的类型系统进行编码。如果您只想编写一段代码,那么就不必为不同的类型编写相同的代码,只需使用不同的类型声明(即使类处于不相交的层次结构中),也不必弄清楚哪些类型转换是安全的,哪些类型转换不是安全的。

理论上还有其他语言提供相同的东西:类型推断语言。最流行的是C++(使用模板)和Haskell。在理论上(可能在实践中),您可以编写更少的代码,因为类型是静态解析的,所以您不必编写异常处理程序来处理传递错误类型的问题。我发现他们仍然要求你编程到类型系统,而不是你程序中的实际类型(他们的类型系统是定理证明者,为了便于理解,他们不分析你的整个程序)。如果您觉得这很好,可以考虑使用这些语言中的一种,而不是Python(或者Ruby、Smalltalk或任何Lisp变体)。

在Python(或任何类似的动态语言)中,当对象不支持特定方法时,您将希望使用异常来捕获,而不是进行类型测试。在这种情况下,要么让它升入堆栈,要么捕获它,然后针对不正确的类型引发异常。这种"请求宽恕比许可更好"的编码是惯用的python,它大大简化了代码。

实践中的*。类更改在Python和Smalltalk中是可能的,但很少发生。这也和用低级语言铸造不同。


如果您坚持要向代码中添加类型检查,您可能需要查看注释以及它们如何简化您必须编写的内容。StackOverflow上的一个问题介绍了一个小的、模糊的类型检查器,利用注释。下面是一个基于您的问题的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
>>> 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)。第三稿和最后一稿做了一些改进,如下所示供您使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

编辑:

这个答案已经写了四年多了,从那时起,python发生了很多变化。由于这些变化和个人在语言中的成长,重新访问类型检查代码并重写它以利用新特性和改进的编码技术似乎是有益的。因此,提供了以下修订版,对statictypes(现更名为static_types)函数修饰器进行了一些边际改进。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#! /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

就我个人而言,我对断言有一种反感。似乎程序员可以看到问题的出现,但却不想去考虑如何处理它们,另一个问题是,如果其中一个参数是从您期望的类派生的类,则您的示例将断言,即使这些类应该工作!-在上面的例子中,我将选择如下内容:

1
2
3
4
5
6
7
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也适用于派生类。例如。:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> 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
>>>

您可以考虑抛出自定义异常,而不是断言。通过检查参数是否具有所需的方法,您甚至可以更全面地概括这些参数。

顺便说一下,它可能是我的挑剔,但我总是试图避免C/C++中的断言,理由是如果它停留在代码中,那么几年时间内的某个人将做出一个应该被它捕获的更改,而不是在调试中对它进行足够的测试,(甚至根本不测试它),编译为可交付的,释放模式,即移除所有断言。。所有的错误检查都是这样做的,现在我们有不可靠的代码和一个主要的头疼的问题来发现。


这是一种非惯用的做事方式。通常在python中,您将使用try/except测试。

1
2
3
4
5
6
7
8
9
def orSearch(d, query):
    try:
        d.get(something)
    except TypeError:
        print("oops")
    try:
        foo = query[:2]
    except TypeError:
        print("durn")


我同意史蒂夫的方法,当你需要做类型检查。我不经常发现需要在Python中进行类型检查,但至少有一种情况需要这样做。如果不检查类型,则可能返回错误的答案,这将在稍后的计算中导致错误。这些类型的错误可能很难被跟踪,而且我在Python中已经多次遇到过它们。像你一样,我首先学会了Java,不必经常处理它们。

假设您有一个简单的函数,它需要一个数组并返回第一个元素。

1
def func(arr): return arr[0]

如果使用数组调用它,则将获得数组的第一个元素。

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

如果使用字符串或任何实现getitem magic方法的类的对象调用它,也将得到响应。

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

这会给你一个答复,但在这种情况下,它的类型是错误的。这可能发生在具有相同方法签名的对象上。直到后来的计算中你才可能发现这个错误。如果您在自己的代码中确实遇到过这种情况,通常意味着在先前的计算中有一个错误,但是在那里进行检查会更早地捕捉到它。但是,如果您正在为其他人编写一个python包,那么您可能需要考虑它。

您不应该对检查造成很大的性能损失,但这会使代码更难读取,这在Python世界中是一件大事。


两件事。

首先,如果你愿意花大约200美元,你可以得到一个相当好的python ide。我用的是Pycharm,给人留下了很深的印象。(由为C编写Resharper的人编写)它将在您编写代码时分析代码,并查找变量类型错误的位置(在一堆其他事物中)。

第二:

在使用pycharm之前,我遇到了同样的问题——也就是说,我会忘记我编写的函数的特定签名。我可能在某个地方找到了这个,但也许是我写的(我现在记不起来了)。但是无论如何,它是一个装饰器,您可以在函数定义周围使用它来为您进行类型检查。

像这样称呼它

1
2
3
4
5
6
@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

不管怎样,这是装饰师的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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