检查函数参数的最佳方法是什么?

86

我正在寻找一种高效的方法来检查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
14个回答

125

在这个详细的答案中,我们使用基于PEP 484风格类型提示的Python 3.x特定类型检查装饰器,代码量不到275行纯Python(其中大部分是解释性文档字符串和注释),并且针对工业级别真实世界使用进行了大量优化,配备一个py.test驱动的测试套件,涵盖所有可能的边缘情况。

享受熊类型的意想不到的神奇:

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

正如这个例子所示,Bear Typing 明确支持对参数和返回值进行类型检查,注释可以是简单类型或这些类型的元组。哇!

好吧,实际上并没有什么了不起的。在纯 Python 3.x 中,@beartype 基于 PEP 484 风格类型提示的类型检查装饰器和其他的一样,并且只有不到 275 行代码。那么问题来了?

纯暴力硬核效率

Bear typing 在空间和时间效率上比目前所有已知的 Python 类型检查实现都要高效,这是我有限的领域知识所能了解到的。

然而,在 Python 中,效率通常并不重要。如果很重要的话,你也不会使用 Python。类型检查真的会偏离 Python 避免过早优化的规范吗?是的。没错。

考虑性能分析,它为每个感兴趣的指标(例如函数调用、行)添加不可避免的开销。为了保证准确的结果,需要利用优化的 C 扩展(例如 cProfile 模块使用的 _lsprof C 扩展),而不是未经优化的纯 Python(例如 profile 模块)。在性能分析时效率真正很重要。

类型检查也不例外。类型检查为应用程序中每个进行类型检查的函数调用添加开销,理想情况下应该所有函数调用都进行类型检查。因此,类型检查必须快速。 快到你加上它后,没有人会注意到它。但请注意,为了防止那些善意的但可悲的小心眼的同事从你那儿删除你上周在老旧的 Django 网站中添加的类型检查,请确保让它够快。我一直这样做!如果你是同事,请停止阅读此内容。

然而,如果你的贪婪应用程序要求甚至于疯狂的速度都无法满足,那么可以通过启用 Python 优化(例如通过将 -O 选项传递给 Python 解释器)来全局禁用 Bear typing:

$ 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')

仅仅因为喜欢。欢迎使用Bear typing。

什么鬼?为什么是“bear”?你是个脖子胡子程序员,对吧?

Bear typing是裸金属类型检查 - 即,尽可能接近Python类型检查手动方法的类型检查。Bear typing旨在不会带来性能损失、兼容性约束或第三方依赖项(除了手动方法所施加的)。Bear typing可以无需修改即可无缝集成到现有的代码库和测试套件中。

大家都应该熟悉手动方法。您需要手动assert每个传递给和/或从您的代码库中的每个函数返回的参数类型。什么样的样板代码更简单或更平凡呢?我们已经看过了一百次谷歌多一点,每当我们这样做时,我们都会呕吐一点。重复太快就会变得陈旧。DRY,yo。别重复自己

准备好你的呕吐袋。为了简洁起见,让我们假设一个简化的 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, 对吧?我们中的很多人都通过了那门课。

Bear typing将上述方法手动执行的类型检查提取到一个动态定义的包装函数中,自动执行相同的检查 - 并具有引发粒度更细的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

虽然有点冗长,但这个包装函数基本上和手动方法一样快。* 建议眯起眼睛看。

请注意,包装函数中完全没有对函数进行检查或迭代,但它包含了与原始函数类似数量的测试 - 尽管额外的(可能可以忽略不计的)测试成本是测试参数是否要进行类型检查以及如何将这些参数传递给当前函数调用。你不能赢得每一场战斗。

是否可以可靠地生成此类包装函数以在不到275行的纯Python代码中对任意函数进行类型检查?Snake Plisskin说:"真事。我可以抽根烟吗?"

是的。我可能有颈胡子。

不,认真点,为什么叫“bear(熊)”?

熊打败了鸭子。鸭子可以飞,但熊可以向鸭子扔鲑鱼。在加拿大,自然界会让你惊奇。

下一个问题。

为什么熊这么火?

现有的解决方案并不执行裸机类型检查 - 至少在我搜索的范围内没有找到。它们都会在每次函数调用时迭代重新检查类型检查函数的签名。虽然单个调用可以忽略不计,但聚合所有调用的重新检查开销通常是不可忽略的。真的非常不可忽略。

然而,这并不仅仅是效率问题。现有解决方案也经常无法考虑常见的边界情况。这包括此处和其他地方提供的大多数玩具装饰器。经典的失败包括:

  • 未能对关键字参数和/或返回值进行类型检查(例如,sweeneyrod@checkargs装饰器)。
  • 未能支持由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's 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's annotated type.
                func_arg_type_expr = (
                    '__beartype_func.__annotations__[{!r}]'.format(
                        func_arg.name))

                # String evaluating to this parameter's 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's 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's 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's 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

并且leycec说,让@beartype快速地进行类型检查:于是它就这样做了。

警告、诅咒和空洞的承诺

没有什么是完美的,即使是熊类型检查。

警告1:默认值未检查

熊类型检查不会对分配了默认值的未传递参数进行类型检查。理论上可以,但不能在275行以下的代码或堆栈溢出回答中实现。

安全的(也许完全不安全的)假设是函数实现者声称他们在定义默认值时知道自己在做什么。由于默认值通常是常量(最好如此!),为每个函数调用重新检查从未更改的常量的类型来分配一个或多个默认值将违反熊类型检查的基本准则:“不要一遍又一遍地重复自己”。

证明我错,并且我将给予你点赞。

警告2:没有PEP 484

PEP 484("类型提示")正式化了函数注释的使用,这是由PEP 3107("函数注释")首次引入的。Python 3.5通过一个新的顶级typing模块 superficially支持这个规范,该模块提供了一个标准的API,用于从较简单的类型(例如Callable[[Arg1Type, Arg2Type], ReturnType],描述接受两个类型为Arg1TypeArg2Type的参数并返回类型为ReturnType的函数的类型)组合任意复杂的类型。

熊类型检查都不支持它们。理论上可以,但不能在275行以下的代码或堆栈溢出回答中实现。

然而,熊类型检查确实支持类型的联合,就像isinstance()内置函数支持类型的联合一样:作为元组。这表面上对应于typing.Union类型 - 其中明显的警告是typing.Union支持任意复杂的类型,而@beartype接受的元组仅支持简单类。为自己辩护,275行。

测试或它就没有发生过

这是相关内容的梗概。你懂了,吗?我现在停止。

@beartype装饰器本身一样,这些py.test测试可在不修改的情况下无缝集成到现有的测试套件中。非常宝贵,不是吗?

现在是必须的没人问的领结胡须抱怨。

API暴力的历史

Python 3.5实际上不支持使用PEP 484类型。 什么?

事实如此:没有类型检查、没有类型推断、没有任何类型功能。相反,开发人员期望经常运行整个代码库通过重量级的第三方CPython解释器包装器来实现此类支持的仿真(例如mypy)。当然,这些包装器会强加:

  • 兼容性问题。正如官方 mypy FAQ在回答常见问题“我能用 mypy 对我的现有 Python 代码进行类型检查吗?”时承认的:“这要看情况。 兼容性非常好,但是一些 Python 特性尚未实现或完全支持。”后续 FAQ 回复通过以下声明澄清了此不兼容性:
    • “…您的代码必须使属性显式并使用显式协议表示。” 语法警察看到您的“a explicit”并向您提出牢骚。
    • “Mypy 将支持模块化、高效的类型检查,这似乎排除了对某些语言特性(例如任意运行时添加方法)进行类型检查。但是,许多这些功能可能会以受限制的形式得到支持(例如,仅对注册为动态或可‘修补’的类或方法支持运行时修改)。”
    • 有关语法不兼容性的完整列表,请参见“处理常见问题”。情况不容乐观。 你只是想进行类型检查,现在却重构了整个代码库,并且在候选发布的两天内破坏了所有人的构建,穿着休闲商务装的可爱 HR 小人将一张粉红色的通行证从你的小隔间裂缝中滑了进来。谢谢你, mypy。
  • 性能问题,尽管解释静态类型代码。 四十年的坚实计算机科学告诉我们(其他条件相等),解释静态类型代码应该比解释动态类型代码更快,而不是更慢。 在 Python 中,上就是下。
  • 额外的非平凡依赖项,增加了:
    • 项目部署的漏洞和脆弱性,特别是跨平台的情况。
    • 项目开发的维护负担。
    • 可能存在的攻击面。

我问 Guido:“为什么?如果你不准备付出一个具体的 API 来使用那个抽象的 API,为什么还要发明一个抽象的 API 呢?” 为什么要让百万 Python 程序员的命运落入自由开源市场的关节炎手中? 为什么要创造另一个可以用官方 Python 标准库中的 275 行修饰符轻松解决的技术问题呢?

我一无所知,只能尖叫。


19
请保持“meta”在翻译中的原样,并让翻译更加通俗易懂,但不改变原意。 - user229044
47
我本希望能得到一些实质性的评论,但却被规范化的纪律所迎接。虽然“蒙提·派森飞行马戏团”发表的剧本启发了Python程序员使用毫无保留的语言,但在Python领域内,可接受的行为范围非常狭窄。不用说,我完全不同意这种看法:我们需要更多源自意识流、网络文化和内部玩笑的启示性、有思想深度的诗歌,以及多音节的新奇词汇。要减少单音节的平庸表述。 - Cecil Curry
3
这是一个非常有用的修饰器 - 可能值得在 GitHub 上托管,以便我们可以随时更新后续增强功能。 - user2682863
8
感谢您的努力,但这个答案对于一个简单的问题来说太长了。大多数人都在寻找“谷歌”式的答案。 - Izik
9
@Izik: 我寻找高质量的答案,能够让我不必在一两周后再次搜索。如果问题需要简短的回答,那很好,如果需要更多的话,那就是这样。从长远来看,这比拥有成百上千个不能增进我的理解力且基本上都是相同的一句话更有用。 - Make42
显示剩余6条评论

93

最Pythonic的惯用语是清楚地记录函数的期望,并尝试使用传递给函数的任何内容,然后让异常传播或仅捕获属性错误并引发TypeError。应该尽可能避免类型检查,因为它违反了鸭子类型。值测试可能没问题 - 具体取决于上下文。

真正有意义的验证只有在系统或子系统入口点(例如Web表单、命令行参数等)才有意义。除此之外,只要您的函数已经正确记录,就由调用者负责传递适当的参数。


3
使用locals()可能会造成无用的复杂性,实际上我看不出它的用途,因为你已经知道你的命名参数名称(显然<g>),如果你的函数使用argskwargs,你可以直接访问它们。此外,断言主要用于调试。如果您的函数的约定是参数'a'必须是一个介于0和10之间的整数,并且参数'b'必须是一个非空字符串,则应该引发相应的异常类型,即TypeErrorValueError - 在Python shell中尝试int('a')int(None) - bruno desthuilliers
4
就我个人而言,我只在“绝对不可能发生”的情况下使用assertion(然而众所周知这种情况总有一天会发生)。请注意,“优化过的”字节码(.pyo文件)会跳过assertions,因此在生产代码中最好不要依赖AssertionError。<g> - bruno desthuilliers
12
虽然这可能不符合 Pythonic 的风格,但我鼓励在模块之间执行接口规范,特别是在分发模块时。这会使开发和使用变得更加容易,在所有编程语言中都适用。 - Peter R
44
我厌恶专制的无回答,这种回答可以简化为:“停止尝试做你想做的事情,因为我知道得更好。”这是诸多这类回答中的又一例。有许多合理的原因需要检查类型,其中几个甚至在这个无回答中提到。在Python 3.x下,最佳(也是明显的)答案是装饰器加函数注释。请参见sweeneyrod的精彩 @checkargs decorator简而言之:少些教条主义,多些真正的回答。 - Cecil Curry
10
这不应该被接受为答案。还有一个非常重要的地方需要类型匹配,那就是外部API。有时候无法将错误传播到这样的API之外,尤其是本地API,它们只能使用精确类型的参数进行调用。在这种情况下,鸭子类型会对你产生负面影响。 - Bartek Banachewicz
显示剩余5条评论

33

更新:截至2019年,Python对使用类型注释和静态检查提供了更多支持;请查看typing模块和mypy。以下是2013年的回答:


在Python中,一般不会进行类型检查,这与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

而且这个函数对于任何定义了__int__方法的a仍然有效。

回答你的其他问题,我认为最好采取以下方法(正如其他答案所说):

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

1
checkargs和coerceargs无法用于未指定所有参数默认类型的函数,例如:g(a:int,b) - Igor Malin
如果其中一个参数是可选的,这也不起作用。 - AcidBurn

19

一种方法是使用 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'

12
当我不遵守可调用对象的契约时,我不希望它引发AssertionError,标准库中也不会出现这种情况。在Python shell中尝试使用int('a')和int(None)... 会得到ValueError和TypeError。请注意,这里并没有解释任何内容。 - bruno desthuilliers
3
谢谢,我觉得使用断言很方便。人们使用Python有不同的原因。有些人用来编写生产代码,而其他人只是用它来进行原型设计。这是在函数输入上快速设置约束条件的一种方法。如果我要为标准库编写一个函数,我可能会更加明确地表达。 - Matthew Plourde
12
断言应该被视为一个简单的选项,通常比什么都不做要好得多——它可以引起早期故障并帮助记录代码。我认为它们在我们的代码中有很好的位置。 - KenFar
2
+1 可以起到一定作用,但是避免将它们用于外部输入验证,应将其用于代码内部检查。 - Christophe Roussy
5
在生产代码中使用assert时要小心。这可能会被忽略,具体取决于您的代码在哪个环境下执行。请查看以下答案:https://dev59.com/TnNA5IYBdhLWcg3wfN2O#1838411 - Renan Ivo

7
你可以使用来自PythonDecoratorLibrary的 Type Enforcement accept/returns 装饰器,它非常简单易懂:
@accepts(int, int, float)
def myfunc(i1, i2, i3):
    pass

5
在Python 3.x版本下,函数注释(例如def myfunc(i1: int, i2: int, i3: float))是一种更加符合Python风格的声明类型的方法。参见sweeneyrod@checkargs装饰器,使用函数注释在少于10行的代码中提供了一个强大的类型检查解决方案。 - Cecil Curry
你如何导入 PythonDecoratorLibrary 库? - Pablo

6

在Python中,有不同的方法可以检查变量。以下是其中的几种方法:

  • isinstance(obj, type) 函数接受变量 obj 并给出一个值为 True 的结果,如果它与你列出的 type 相同。

  • issubclass(obj, class) 函数接受变量 obj 并给出一个值为 True 的结果,如果 objclass 的子类。例如,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 倾向于鸭子类型,通常更符合 Python 程序员的编程思维方式,但这个术语可能存在不同看法。

需要注意的是,assert 语句通常用于测试,否则请使用 if/else 语句。


5

最近我对这个主题进行了相当多的调查,因为我对我发现的许多不满意。

最终我开发了一个名为valid8的库来解决这个问题。如文档中所述,它主要用于值验证(尽管它还附带了简单类型验证函数),您可能希望将其与基于PEP484的类型检查器(例如enforcepytypes)相关联。

以下是在您的情况下仅使用valid8(实际上还有mini_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].

你能说一下 valid8 与 bear_typing 相比如何吗? - Make42
2
"beartyping" 看起来与大多数类型检查器相似,例如 typeguardpytypesenforce... 除了它不是一个经过验证和记录的库,它不符合 PEP484 标准(像 PyContracts),并且使用 exec 使包装器运行更快(代价是无法进行调试)。valid8 旨在验证类型和值,并可以与现有的 PEP484 类型检查器结合使用,以便仅关注值检查。 - smarie

4

调用函数时,这检查输入参数的类型:

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(它必须给出断言错误)


这将仅适用于 Python >= 3。 - Carmellose

2

通常情况下,您需要按照以下步骤操作:

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

2
预期的异常分别是TypeError和ValueError。 - bruno desthuilliers
没错;但是答案中使用的那些可以从你提到的这些子类化。 - glglgl
正确,但那只是例子。我会更新这个例子。 - Mats Kindahl
@MatsKindahl:一条错误信息也许会有帮助,例如: raise TypeError("期望一个整数,但得到了 '%s'" % type(a)) - bruno desthuilliers

1
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() 的信息


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接