17

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

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

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

4

7 回答 7

14

别那样做。

使用“动态”语言(对于值* 是强类型,对于变量是非类型化和后期绑定)的重点是您的函数可以适当地多态,因为它们将处理任何支持您的接口的对象功能依赖于(“鸭子打字”)。

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

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

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

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

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


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

于 2013-07-05T19:55:23.360 回答
9

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

  • 继承:你当然打算写一些具有以下效果的东西

    assert isinstance(d, dict)
    

    以确保您的代码也可以与dict. 我认为这类似于 Java 中的用法。但是 Python 有一些 Java 没有的东西,即

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

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

于 2013-07-05T18:19:23.243 回答
5

如果您坚持在代码中添加类型检查,您可能需要研究注释以及它们如何简化您必须编写的内容。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

编辑:

自编写此答案以来已经四年多了,从那时起,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
于 2013-07-05T19:16:44.363 回答
3

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

def orSearch(d, query):
    try:
        d.get(something)
    except TypeError:
        print("oops")
    try:
        foo = query[:2]
    except TypeError:
        print("durn")
于 2013-07-05T16:28:34.773 回答
3

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

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++ 中断言,因为如果它留在代码中,那么几年后有人会做出应该被它抓住的改变,而不是测试它在调试中足以发生这种情况,(甚至根本不对其进行测试),编译为可交付的,发布模式, - 删除所有断言,即所有以这种方式完成的错误检查,现在我们有不可靠的代码和专业头疼的发现问题。

于 2013-07-05T16:38:10.807 回答
2

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

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

def func(arr): return arr[0]

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

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

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

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

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

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

于 2013-07-05T17:19:37.073 回答
1

两件事情。

首先,如果你愿意花 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
于 2013-07-05T18:17:24.077 回答