检查函数参数的最佳方法?[关闭]


70

我正在寻找一种检查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

Answers:


81

最Python化的习惯用法是清楚地记录函数期望的内容,然后尝试使用传递给函数的所有内容,让异常传播或捕获属性错误并引发异常TypeError。应尽可能避免类型检查,因为这与鸭式打字不符。可以根据环境进行价值测试。

验证真正有意义的唯一地方是系统或子系统的入口点,例如Web窗体,命令行参数等。在其他任何地方,只要正确记录了您的函数,调用者都有责任传递适当的参数。


3
@carmellose:使用locals()可能最终会导致无用的复杂性-实际上,我看不到它的用例,因为您已经知道命名的参数名称(显然是<g>),并且可以直接访问argskwargs如果您的函数使用了它们。另外,断言主要用于调试。如果你的函数的契约是:ARG“A”必须在0到10和论据“B”之间的int必须是一个非空字符串,然后提高适当的异常类型,即TypeErrorValueError-尝试int('a')int(None)你的Python壳。
bruno desthuilliers

4
就我而言,我仅对“不可能发生”的情况使用断言(众所周知,这种情况最终会在一天或另一天发生)。请注意,“优化的”字节码(.pyo文件)会跳过断言,因此最好不要将AssertionError用作生产代码<g>。
bruno desthuilliers

9
它可能不是Pythonic,但我鼓励在模块之间强制接口,尤其是在分发模块时。这使开发和使用都变得更加容易,并且在所有语言中都是如此
Peter R

23
我讨厌威权主义的非回答,这些非回答只能归结为:“停止做自己想做的事,因为我知道得更多。” 这是可悲的一长串这样的答案中的又一个。存在许多检查类型的正当理由,其中一些没有答案甚至涉及。在Python 3.x下,最佳的(显然是显而易见的)答案是装饰器和函数注释。另请参见下面的sweeneyrod出色的@checkargs装饰器tl; dr较少的原教旨主义;更多实际答案。
Cecil Curry

7
这不应该是公认的答案。类型需要排队的另一个非常重要的地方是外部API。有时,不可能从这样的API(尤其是本机API)中传播错误,而必须使用精确的参数类型来调用它们。在这种情况下,鸭子打字会积极地对您不利。
Bartek Banachewicz

98

在这个冗长的答案中,我们实现了一个基于Python 3.x的类型检查装饰器,该装饰器基于 在不到275行的纯Python代码(其中大多数是说明性的文档字符串和注释)中PEP 484样式类型提示-已针对工业应用进行了优化强度的实际使用,并带有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">

如该示例所示,熊类型显式支持参数的类型检查,并返回注释为简单类型或此类的元组的返回值。发誓!

好吧,那实际上并不令人印象深刻。@beartype其他基于Python 3.x的类型检查装饰器类似在不到275行的纯Python中,PEP 484样式类型提示的。那擦是什么,小家伙?

纯Bruteforce核心效率

尽我所能掌握的有限领域知识,Bear类型在空间和时间上都比Python中所有现有的类型检查实现显着提高效率。(稍后会更多。

但是,效率通常在Python中并不重要。如果这样做的话,您将不会使用Python。类型检查是否实际上偏离了已建立的避免在Python中过早优化的规范?是。是的,它确实。

考虑进行性能分析,这会给每个配置的相关指标(例如,函数调用,行)增加不可避免的开销。为了确保准确的结果,通过利用优化的C扩展(例如,模块_lsprof利用的C扩展cProfile)而不是未优化的纯Python(例如,profile模块)来减轻这种开销。效率真的profiling的时候无所谓。

类型检查没有什么不同。类型检查会增加应用程序检查的每个函数调用类型的开销–理想情况下,所有这些函数调用类型都会增加开销。为防止好心的(但心胸狭窄)的同事删除上周五星期五将咖啡因添加的通宵软件添加到老年Django网络应用程序后默默添加的类型检查,类型检查必须快速进行。如此之快,以至于您在不通知任何人的情况下都没有注意到它的存在。我一直都这样做!如果您是同事,请停止阅读。

但是,如果即使快的速度都无法满足您繁琐的应用程序,则可以通过启用Python优化(例如,通过将-O选项传递给Python解释器)来全局禁用空头类型:

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

只是因为。欢迎熊打字。

什么...?为什么是“熊”?你是胡子,对不对?

熊类型检查是裸机类型检查-即,类型检查尽可能接近Python中的手动类型检查方法。空头输入的目的是不施加任何性能损失,兼容性约束或第三方依赖关系(无论如何,手动依赖关系不适用)。熊类型可以无缝集成到现有代码库和测试套件中,而无需进行修改。

每个人都可能熟悉手动方法。您可以手动assert传递给代码库中每个函数的每个参数和/或返回从每个函数返回的值。哪种样板可能更简单或更平庸?我们都曾经在googleplex上看到过一百次,并且每次都在我们的嘴里吐了一点。重复会很快变老。,哟。

准备好呕吐袋。为简便起见,我们假设一个简化的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,对吗?我们许多人通过了那堂课。

熊类型将通过上述方法手动执行的类型检查提取到动态定义的包装函数中,该函数自动执行相同的检查-额外的好处是引发了粒状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

long。但它也基本上是*快手动方法。*建议斜眼。

请注意,在包装函数中完全没有进行功能检查或迭代,它包含与原始功能相似的测试数量–尽管测试(是否可以忽略)将类型检查参数传递给参数的额外费用(可能忽略不计)。当前函数调用。您不可能赢得每场战斗。

实际上可以可靠地生成此类包装器函数以在少于275行的纯Python行中类型检查任意函数吗?Snake Plisskin说:“真实的故事。抽烟了吗?”

是的。我可能有胡子。

不,Srsly。为什么是“熊”?

熊击败鸭子。鸭子可能会飞,但是熊可能会向鸭子扔鲑鱼。在加拿大,大自然会让您感到惊讶。

下一个问题。

熊到底有什么热门?

现有解决方案执行裸机类型检查-至少,我没有遇到过。它们都会在每个函数调用上反复检查类型检查函数的签名。尽管单个呼叫可以忽略不计,但在所有呼叫上进行汇总时,重新检查开销通常是不可忽略的。真的,真的不可忽略。

但是,这不仅仅是效率问题。现有解决方案通常也无法解决常见的边缘情况。这包括大多数(如果不是全部)提供的玩具装饰器,作为此处和其他地方的stackoverflow答案。经典失败包括:

  • 无法输入check关键字参数和/或返回值(例如sweeneyrod@checkargsdecorator)。
  • 无法支持isinstance()内建函数接受的元组(即联合)。
  • 无法将名称,文档字符串和其他标识元数据从原始函数传播到包装函数。
  • 无法提供至少一组单元测试。(很关键。
  • 在失败的类型检查中引发通用AssertionError异常,而不是特定TypeError异常。出于粒度和完整性的考虑,类型检查绝不应引发通用异常。

在非熊失败的地方,熊打字成功。所有人,所有人承担!

熊打字裸露

熊打字将检查函数签名的空间和时间成本从函数调用时间转移到函数定义时间,即从@beartype装饰器返回的包装函数到装饰器本身。由于装饰器每个函数定义仅被调用一次,因此该优化为所有人带来了欢乐。

熊打字是一种尝试让您的类型检查蛋糕并吃掉它的尝试。为此,@beartype

  1. 检查原始功能的签名和注释。
  2. 动态构造包装器功能类型的主体,检查原始功能。Thaaat的权利。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生出型快速度检查:它是如此。

警告,诅咒和空洞的承诺

没有什么是完美的。甚至熊打字。

注意事项I:未选中默认值

熊打字也不会键入分配默认值检查unpassed参数。从理论上讲,它可以。但不是少于275行,肯定不是作为stackoverflow的答案。

安全的假设(...可能是完全不安全的)是函数实现者声称,他们在定义默认值时就知道自己在做什么。由于默认值通常是常量(...最好是这样!),因此重新检查在分配了一个或多个默认值的每个函数调用中永不更改的常量类型会违反熊类型的基本宗旨:“不要重复在自己和oooover呜- oooover试“。

告诉我错了,我将为您投票。

警告II:无PEP 484

PEP 484“类型提示”)正式使用了由PEP 3107“功能注释”)首次引入的功能注释。蟒3.5表面上支持该形式化用一个新的顶层typing模块,用于从较简单的类型构成任意复杂类型的标准API(例如,Callable[[Arg1Type, Arg2Type], ReturnType],类型描述的功能,接受类型的两个参数,Arg1Type以及Arg2Type与返回类型的值ReturnType)。

熊打字不支持它们。从理论上讲,它可以。但不是少于275行,肯定不是作为stackoverflow的答案。

但是,Bear类型的确支持类型联合,就像isinstance()内置支持类型联合一样:作为元组。从表面上讲,它对应于typing.Union类型-很明显的警告是typing.Union支持任意复杂的类型,而被接受的元组@beartype支持简单的类。在我的辩护中,有275行。

测试或未发生

这是要点要点?我现在停止。

@beartype装饰器本身一样,这些py.test测试可以无缝集成到现有测试套件中,而无需进行修改。珍贵,不是吗?

现在没有人要求的强制性胡须咆哮。

API暴力的历史

Python 3.5不提供对使用PEP 484类型的实际支持。at?

没错:没有类型检查,没有类型推断,没有任何类型的螺母。取而代之的是,期望开发人员通过实现这种支持的传真(例如,mypy)的大型第三方CPython解释器包装程序例行运行其整个代码库。当然,这些包装器会施加:

  • 一个兼容性的处罚。正如官方的mypy FAQ回答了一个常见问题“我可以使用mypy键入检查我现有的Python代码吗?”时所说的:“这取决于。兼容性相当好,但是某些Python功能尚未实现或未完全支持。” 一个后续的常见问题回应澄清这种不兼容的指出:
    • “ ...您的代码必须使属性显式并使用显式协议表示。” 语法警察看到您的“显式”,并暗中皱起眉头。
    • “ Mypy将支持模块化的高效类型检查,这似乎排除了某些语言功能的类型检查,例如任意运行时添加方法。但是,很可能将以受限形式支持许多这些功能(例如, ,只有注册为动态或“可补丁”的类或方法才支持运行时修改。”
    • 有关语法不兼容的完整列表,请参见“处理常见问题”。这是不是漂亮。您只需要类型检查,现在就可以重构整个代码库,并从候选版本发布开始两天就破坏了每个人的构建,而休闲商务着装的HR小矮人则穿过了小巧玲珑的缝隙。非常感谢,mypy。
  • 一个性能损失,尽管解释静态类型的代码。经过四十年的艰苦努力,计算机科学告诉我们(...在其他所有条件都相同的情况下),解释静态类型的代码比解释动态类型的代码要快而不是慢。在Python中,up是新的down。
  • 其他非平凡的依赖关系,增加了:
    • 项目部署(尤其是跨平台)中充满错误的脆弱性。
    • 项目开发的维护负担。
    • 可能的攻击面。

我问Guido:“为什么?如果您不愿意实际使用抽象来做具体的API,那么为什么还要发明一个抽象API呢?” 为什么要让一百万Pythonista使用者的命运留在自由开源市场的风风火火之下?为什么还要在官方Python stdlib中使用275行装饰器创建另一个本可以解决的技术问题呢?

我没有Python,必须尖叫。


12
请把meta保留在meta中。
meagar

33
我希望能有一些实质性的评论。我受到学科规范化的欢迎。对于不受限制的语言,主要是受“ Monty Python's Flying Circus”脚本的启发,Pythonistas中可接受行为的肮脏窗口令人惊讶……狭窄。不用说,我整体上是不同意的:我们需要更多的意识流,模因,笑话,开明的预言和诗意的诗歌。多音节新颖性。单音节常态较少。
塞西尔·库里

2
这是一个非常有用的装饰器-可能值得在github上托管,因此我们可以随时了解最新的增强功能
user2682863

5
感谢您的努力,但是对于一个简单的问题,此答案太长了。我们大多数人都在寻找“ Google”答案。
伊兹克

3
@Izik:我搜索的是高质量的答案,这使我的意思是,我在一两周内不必再次搜索。如果问题需要一个简短的答案,那就太好了,如果它需要更多的单词,那就这样吧。从长远来看,这比拥有数百个不会增加我的理解并且基本上都相同的单线有用。
Make42

27

编辑:从2019年开始,在Python中使用类型注释和静态检查有了更多支持; 查看输入模块和mypy。2013年的答案如下:


类型检查通常不是Pythonic。在Python中,更常见的是使用duck类型。例:

在您的代码中,假设参数(在您的示例中a)像int一样而像an嘎嘎叫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

并且该函数仍可用于a定义该__int__方法的任何函数。

在回答您的其他问题时,我认为这是最好的方法(因为其他答案都可以这样做:

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

16

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

9
我不期望当不遵守可伸缩对象的约定时,它会引发AssertionError,这不是您在标准库中所能找到的。在您的Python Shell中尝试int('a')和int(None)...是,ValueError然后TypeError
bruno desthuilliers

3
谢谢,我觉得断言很方便。人们使用Python的原因有所不同。有些人用来编写生产代码,有些人只是用来编写原型。这是对函数输入施加约束的快速方法。如果我正在为标准库编写函数,则可能会更明确。
马修·普洛德

9
断言应该被认为是一个简单的选择,通常总比没有好得多,这会导致早期失败并有助于文档代码。我认为它们在我们的代码中占有非常重要的位置。
KenFar 2014年

1
+1总比没有好,但请避免将其用于外部输入验证,而将其用于内部代码检查。
Christophe Roussy

4
请注意assert在生产代码中使用。取决于执行代码的环境,它可能会被忽略。看看这个答案:stackoverflow.com/a/1838411/345290
Renan Ivo

7

您可以使用Python DecoratorLibrary中的Type Enforcement接受/返回装饰器。 它非常简单易读:

@accepts(int, int, float)
def myfunc(i1, i2, i3):
    pass

2
下的Python 3.x中,功能注解(例如,def myfunc(i1: int, i2: int, i3: float))是深刻声明类型的更Python手段。请参见sweeneyrod@checkargs装饰器,以在少于10(!)行的代码中包含功能注释的健壮的类型检查解决方案。
Cecil Curry

如何导入PythonDecoratorLibrary
Pablo

5

有多种方法可以检查Python中的变量。因此,列举一些:

  • isinstance(obj, type)函数接受您的变量,obj并给出Truetype您列出的类型相同的变量。

  • issubclass(obj, class)函数接受一个变量obj,并给你Trueif是否obj是的子类class。因此,例如,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型检查,如关联强制pytypes

这是在您的情况下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 '20

1
“beartyping”似乎类似于大多数类型检查,如typeguardpytypes强制执行......但它不是一个验证和文档库,它是不符合PEP484(如PyContracts),它使用exec使包装运行一个小更快(以无法调试为代价)。valid8旨在同时验证类型和值,并且可以与现有的PEP484类型检查器结合使用,以便仅专注于值检查
smarie

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

对; 但答案中使用的可以从您提到的内容中子类化。
glglgl

正确,但这仅是示例。我将更新示例。
Mats Kindahl 2013年

@MatsKindahl:错误消息也可能会有所帮助,例如:raise TypeError("Expected an int, got '%s'" % type(a))
bruno desthuilliers 2013年

2

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

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

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


0

如果要检查**kwargs*args以及在一气呵成普通参数,你可以使用locals()函数作为第一个语句在函数定义得到的参数的字典。

然后使用type()来检查参数,例如在遍历字典的同时。

def myfunc(my, args, to, this, function, **kwargs):
    d = locals()
    assert(type(d.get('x')) == str)
    for x in d:
        if x != 'x':
            assert(type(d[x]) == x
    for x in ['a','b','c']:
        assert(x in d)

    whatever more...

0

如果要对多个功能进行验证,可以将逻辑添加到装饰器中,如下所示:

def deco(func):
     def wrapper(a,b,c):
         if not isinstance(a, int)\
            or not isinstance(b, int)\
            or not isinstance(c, str):
             raise TypeError
         if not 0 < b < 10:
             raise ValueError
         if c == '':
             raise ValueError
         return func(a,b,c)
     return wrapper

并使用它:

@deco
def foo(a,b,c):
    print 'ok!'

希望这可以帮助!


3
如果您确实坚持要进行类型检查,请至少使用isinstance,并引发TypeError。
bruno desthuilliers 2013年

@brunodesthuilliers感谢您的注意!我将编辑答案。
Paulo Bu

为什么不return func(a, b, c)呢?
glglgl 2013年

1
@PauloBu:glglgl的意思是说唱歌手不应该只调用修饰后的func,它还应该返回函数调用的结果。
bruno desthuilliers 2013年

1
我在这里说这句话可能会很麻烦,但是如果您确实需要大量类型检查,也可以考虑使用另一种语言
Christophe Roussy


-1
def myFunction(a,b,c):
"This is an example function I'd like to check arguments of"
    if type( a ) == int:
       #dostuff
    if 0 < b < 10:
       #dostuff
    if type( C ) == str and c != "":
       #dostuff
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.