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

Posted

技术标签:

【中文标题】我应该强制 Python 类型检查吗?【英文标题】:Should I force Python type checking? 【发布时间】:2013-07-03 19:25:19 【问题描述】:

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

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

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

【问题讨论】:

您仍在使用强类型语言...dynamic != weak Speaking of "strongly typed" and "weakly typed"... 旁注:在Java中assert语句默认被剥离,在python中它们不是!这意味着您每次都在执行大量多余的函数调用。在大多数情况下,这不会成为瓶颈,但如果要执行一个简单的函数,您必须执行 5-6 次函数调用来检查类型,那么它可能会成为瓶颈。 鼓励交叉发帖@woozyking,在不同的网站上有两个版本的问题绝对没有意义。供将来参考:如果您认为该问题比 SO 更适合其他网站,请将其标记并要求迁移。 在 Stack Exchange 上通常不赞成交叉发布。您想在哪个网站上回答这个问题,我会安排迁移和合并。 【参考方案1】:

这是一种非惯用的做事方式。通常在 Python 中你会使用 try/except 测试。

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

【讨论】:

我向大家保证,我在自己的代码中使用了更好的错误消息。【参考方案2】:

就我个人而言,我不喜欢断言,似乎程序员可能会看到麻烦的到来,但懒得考虑如何处理它们,另一个问题是如果任一参数是派生自的类,您的示例将断言即使这样的课程应该工作,你也期待着那些! - 在你上面的例子中,我会选择类似的东西:

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
>>> 

您可以考虑抛出自定义异常而不是断言。您甚至可以通过检查参数是否具有您需要的方法来进一步概括。

顺便说一句 我可能有点挑剔,但我总是尽量避免在 C/C++ 中断言,因为如果它留在代码中,那么几年后有人会做出应该被它捕获的更改,在调试中没有对其进行足够好的测试以使其发生,(甚至根本不对其进行测试),编译为可交付的,发布模式,-删除所有断言,即所有错误检查以这种方式完成的,现在我们的代码不可靠,并且很头疼要找到问题。

【讨论】:

您的评论很有道理。我也同意,特别是在像 C 这样的语言中,很容易让你大吃一惊,使用断言代替错误检查的想法是相当不妥当的。但是,断言的意义何在?它只是用于单元测试吗? C/C++ 断言仅在构建为调试的代码中执行某些操作,并且通常认为交付调试代码是一个坏主意,在我工作过的行业中,也禁止对以下代码进行单元测试与可交付成果不同 - 出于显而易见的原因,所以我个人认为 C/C++ 断言没有什么意义 - python 断言对于 在测试工具中 的单元测试和给 IDE 提示什么类型的变量是有意义的应该用于自动完成等,只是不要依赖断言。【参考方案3】:

当您需要进行类型检查时,我同意 Steve 的方法。我并不经常发现需要在 Python 中进行类型检查,但至少在一种情况下我会这样做。这就是不检查类型可能会返回错误答案的地方,这将在稍后的计算中导致错误。这些类型的错误很难追踪,我在 Python 中遇到过很多次。和你一样,我是先学 Java 的,不用经常和他们打交道。

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

def func(arr): return arr[0]

如果你用数组调用它,你会得到数组的第一个元素。

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

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

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

这会给你一个响应,但在这种情况下它的类型是错误的。这可能发生在具有相同method signature 的对象上。您可能要等到计算的很久以后才发现错误。如果您确实在自己的代码中遇到了这种情况,这通常意味着之前的计算中存在错误,但是在那里进行检查会更早地发现它。但是,如果您正在为其他人编写 python 包,则可能无论如何都应该考虑这一点。

您不应该为检查带来很大的性能损失,但它会使您的代码更难阅读,这在 Python 世界中是一件大事。

【讨论】:

【参考方案4】:

两件事。

首先,如果你愿意花 200 美元左右,你可以得到一个相当不错的 python IDE。我使用 PyCharm,印象非常深刻。 (它是由为 C# 制作 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

【讨论】:

只需要注意,不,您不必为 python IDE 花费 200 美元。 PyCharm 社区版是免费的,大多数其他体面的 IDE 也是如此。【参考方案5】:

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

继承:你肯定打算写一些具有

效果的东西
assert isinstance(d, dict)

确保您的代码也能与dict 的子类一起正常工作。我认为这类似于 Java 中的用法。但是 Python 有一些 Java 没有的东西,即

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

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

【讨论】:

【参考方案6】:

如果您坚持在您的代码中添加类型检查,您可能需要查看annotations 以及它们如何简化您必须编写的内容。 *** 上的questions 之一引入了一个利用注释的小型混淆类型检查器。这是基于您的问题的示例:

>>> 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

编辑:

写这个答案已经四年多了,从那时起,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

【讨论】:

【参考方案7】:

别这样了。

使用“动态”语言(对于值* 是强类型的,对于变量是非类型化的,以及后期绑定)的重点是你的函数可以是适当的多态的,因为它们可以处理任何支持的对象你的函数所依赖的接口(“duck typing”)。

Python 定义了许多通用协议(例如可迭代),不同类型的对象可以实现这些协议,而无需相互关联。协议本身不是一种语言特性(不像 java 接口)。

这样做的实际结果是,一般来说,只要你理解你的语言中的类型,并且你适当地注释(包括使用文档字符串,所以其他人也理解你程序中的类型),你通常可以少写代码,因为您不必围绕您的类型系统编写代码。您最终不会为不同的类型编写相同的代码,只需使用不同的类型声明(即使这些类位于不相交的层次结构中),并且您不必弄清楚哪些强制转换是安全的,哪些不安全,如果你想尝试只写一段代码。

理论上还有其他语言提供相同的功能:类型推断语言。最流行的是 C++(使用模板)和 Haskell。理论上(可能在实践中),您最终可以编写更少的代码,因为类型是静态解析的,因此您不必编写异常处理程序来处理传递错误的类型。我发现他们仍然要求您针对类型系统进行编程,而不是针对程序中的实际类型进行编程(他们的类型系统是定理证明者,并且为了易于处理,他们不会分析您的整个程序)。如果这听起来不错,请考虑使用其中一种语言而不是 python(或 ruby​​、smalltalk 或任何 lisp 变体)。

在python(或任何类似的动态语言)中,您需要使用异常来捕获对象不支持特定方法时的类型测试,而不是类型测试。在这种情况下,要么让它进入堆栈,要么捕获它,并引发关于不正确类型的异常。这种“请求宽恕胜于许可”的编码是惯用的 Python,并且大大有助于简化代码。

* 在实践中。在 Python 和 Smalltalk 中可以进行类更改,但很少见。这也与使用低级语言进行强制转换不同。


更新:您可以使用 mypy 在生产之外静态检查您的 python。注释您的代码,以便他们可以检查他们的代码是否一致,如果他们愿意,他们可以这样做;或者如果他们愿意的话。

【讨论】:

以上是关于我应该强制 Python 类型检查吗?的主要内容,如果未能解决你的问题,请参考以下文章

FastApi学习 Pydantic 做类型强制检查

如何强制 PyCharm 警告未声明的类型?

打字稿:使用类型声明强制进行空检查?

您可以强制标量或数组 ref 成为 Perl 中的数组吗?

未经检查的强制转换:尝试在同一方法中将 Int 或 String 强制转换为 T(泛型类型)

PyCharm 类型检查无法按预期工作