在Python中检查函数参数的最佳方法

car*_*ose 56 python arguments function

我正在寻找一种有效的方法来检查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
Run Code Online (Sandbox Code Playgroud)

Cec*_*rry 76

In this elongated answer, we implement a Python 3.x-specific type checking decorator based on PEP 484-style type hints in less than 275 lines of pure-Python (most of which is explanatory docstrings and comments) – heavily optimized for industrial-strength real-world use complete with a py.test-driven test suite exercising all possible edge cases.

Feast on the unexpected awesome of bear typing:

>>> @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">
Run Code Online (Sandbox Code Playgroud)

As this example suggests, bear typing explicitly supports type checking of parameters and return values annotated as either simple types or tuples of such types. Golly!

O.K., that's actually unimpressive. @beartype resembles every other Python 3.x-specific type checking decorator based on PEP 484-style type hints in less than 275 lines of pure-Python. So what's the rub, bub?

Pure Bruteforce Hardcore Efficiency

Bear typing is dramatically more efficient in both space and time than all existing implementations of type checking in Python to the best of my limited domain knowledge. (More on that later.)

Efficiency usually doesn't matter in Python, however. If it did, you wouldn't be using Python. Does type checking actually deviate from the well-established norm of avoiding premature optimization in Python? Yes. Yes, it does.

Consider profiling, which adds unavoidable overhead to each profiled metric of interest (e.g., function calls, lines). To ensure accurate results, this overhead is mitigated by leveraging optimized C extensions (e.g., the _lsprof C extension leveraged by the cProfile module) rather than unoptimized pure-Python (e.g., the profile module). Efficiency really does matter when profiling.

Type checking is no different. Type checking adds overhead to each function call type checked by your application – ideally, all of them. To prevent well-meaning (but sadly small-minded) coworkers from removing the type checking you silently added after last Friday's caffeine-addled allnighter to your geriatric legacy Django web app, type checking must be fast. So fast that no one notices it's there when you add it without telling anyone. I do this all the time! Stop reading this if you are a coworker.

If even ludicrous speed isn't enough for your gluttenous application, however, bear typing may be globally disabled by enabling Python optimizations (e.g., by passing the -O option to the Python interpreter):

$ 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')
Run Code Online (Sandbox Code Playgroud)

Just because. Welcome to bear typing.

What The...? Why "bear"? You're a Neckbeard, Right?

Bear typing is bare-metal type checking – that is, type checking as close to the manual approach of type checking in Python as feasible. Bear typing is intended to impose no performance penalties, compatibility constraints, or third-party dependencies (over and above that imposed by the manual approach, anyway). Bear typing may be seamlessly integrated into existing codebases and test suites without modification.

每个人都可能熟悉手动方法.您手动将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
Run Code Online (Sandbox Code Playgroud)

Python 101,对吗?我们许多人都通过了这门课.

Bear typing extracts the type checking manually performed by the above approach into a dynamically defined wrapper function automatically performing the same checks – with the added benefit of raising granular TypeError rather than ambiguous AssertionError exceptions. Here's what the automated approach looks like:

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
Run Code Online (Sandbox Code Playgroud)

It's long-winded. But it's also basically* as fast as the manual approach. *Squinting suggested.

Note the complete lack of function inspection or iteration in the wrapper function, which contains a similar number of tests as the original function – albeit with the additional (maybe negligible) costs of testing whether and how the parameters to be type checked are passed to the current function call. You can't win every battle.

实际上是否可以可靠地生成这样的包装函数,以便在少于275行纯Python中键入检查任意函数?Snake Plisskin说:"真实的故事.抽烟了吗?"

是的.我可能有一个领带.

不,Srsly.为什么"忍受"?

熊跳鸭.鸭子可能会飞,但熊可能会把鸭子扔到鸭子身上.在加拿大,大自然会给你带来惊喜.

下一个问题.

无论如何,熊有什么好热的?

Existing solutions do not perform bare-metal type checking – at least, none I've grepped across. They all iteratively reinspect the signature of the type-checked function on each function call. While negligible for a single call, reinspection overhead is usually non-negligible when aggregated over all calls. Really, really non-negligible.

It's not simply efficiency concerns, however. Existing solutions also often fail to account for common edge cases. This includes most if not all toy decorators provided as stackoverflow answers here and elsewhere. Classic failures include:

  • Failing to type check keyword arguments and/or return values (e.g., sweeneyrod's @checkargs decorator).
  • Failing to support tuples (i.e., unions) of types accepted by the isinstance() builtin.
  • Failing to propagate the name, docstring, and other identifying metadata from the original function onto the wrapper function.
  • Failing to supply at least a semblance of unit tests. (Kind of critical.)
  • Raising generic AssertionError exceptions rather than specific TypeError exceptions on failed type checks. For granularity and sanity, type checking should never raise generic exceptions.

Bear typing succeeds where non-bears fail. All one, all bear!

Bear Typing Unbared

Bear typing shifts the space and time costs of inspecting function signatures from function call time to function definition time – that is, from the wrapper function returned by the @beartype decorator into the decorator itself. Since the decorator is only called once per function definition, this optimization yields glee for all.

Bear typing is an attempt to have your type checking cake and eat it, too. To do so, @beartype:

  1. Inspects the signature and annotations of the original function.
  2. Dynamically constructs the body of the wrapper function type checking the original function. Thaaat's right. Python code generating Python code.
  3. Dynamically declares this wrapper function via the exec() builtin.
  4. Returns this wrapper function.

Shall we? Let's dive into the deep end.

# 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
Run Code Online (Sandbox Code Playgroud)

And leycec said, Let the @beartype bring forth type checking fastly: and it was so.

Caveats, Curses, and Empty Promises

Nothing is perfect. Even bear typing.

Caveat I: Default Values Unchecked

Bear typing does not type check unpassed parameters assigned default values. In theory, it could. But not in 275 lines or less and certainly not as a stackoverflow answer.

The safe (...probably totally unsafe) assumption is that function implementers claim they knew what they were doing when they defined default values. Since default values are typically constants (...they'd better be!), rechecking the types of constants that never change on each function call assigned one or more default values would contravene the fundamental tenet of bear typing: "Don't repeat yourself over and oooover and oooo-oooover again."

Show me wrong and I will shower you with upvotes.

Caveat II: No PEP 484

PEP 484 ("Type Hints") formalized the use of function annotations first introduced by PEP 3107 ("Function Annotations"). Python 3.5 superficially supports this formalization with a new top-level typing module, a standard API for composing arbitrarily complex types from simpler types (e.g., Callable[[Arg1Type, Arg2Type], ReturnType], a type describing a function accepting two arguments of type Arg1Type and Arg2Type and returning a value of type ReturnType).

Bear typing supports none of them. In theory, it could. But not in 275 lines or less and certainly not as a stackoverflow answer.

Bear typing does, however, support unions of types in the same way that the isinstance() builtin supports unions of types: as tuples. This superficially corresponds to the typing.Union type – with the obvious caveat that typing.Union supports arbitrarily complex types, while tuples accepted by @beartype support only simple classes. In my defense, 275 lines.

Tests or It Didn't Happen

Here's the gist of it. Get it, gist? I'll stop now.

As with the @beartype decorator itself, these py.test tests may be seamlessly integrated into existing test suites without modification. Precious, isn't it?

Now the mandatory neckbeard rant nobody asked for.

A History of API Violence

Python 3.5 provides no actual support for using PEP 484 types. wat?

It's true: no type checking, no type inference, no type nuthin'. Instead, developers are expected to routinely run their entire codebases through heavyweight third-party CPython interpreter wrappers implementing a facsimile of such support (e.g., mypy). Of course, these wrappers impose:

  • A compatibility penalty. As the official mypy FAQ admits in response to the frequently asked question "Can I use mypy to type check my existing Python code?": "It depends. Compatibility is pretty good, but some Python features are not yet implemented or fully supported." A subsequent FAQ response clarifies this incompatibility by stating that:
    • "...your code must make attributes explicit and use a explicit protocol representation." Grammar police see your "a explicit" and raise you an implicit frown.
    • "Mypy will support modular, efficient type checking, and this seems to rule out type checking some language features, such as arbitrary runtime addition of methods. However, it is likely that many of these features will be supported in a restricted form (for example, runtime modification is only supported for classes or methods registered as dynamic or 'patchable’)."
    • For a full list of syntactic incompatibilities, see "Dealing with common issues". It's not pretty. You just wanted type checking and now you refactored your entire codebase and broke everyone's build two days from the candidate release and the comely HR midget in casual business attire slips a pink slip through the crack in your cubicle-cum-mancave. Thanks alot, mypy.
  • A performance penalty, despite interpreting statically typed code. Fourty years of hard-boiled computer science tells us that (...all else being equal) interpreting statically typed code should be faster, not slower, than interpreting dynamically typed code. In Python, up is the new down.
  • Additional non-trivial dependencies, increasing:
    • The bug-laden fragility of project deployment, especially cross-platform.
    • The maintenance burden of project development.
    • Possible attack surface.

I ask Guido: "Why? Why bother inventing an abstract API if you weren't willing to pony up a concrete API actually doing something with that abstraction?" Why leave the fate of a million Pythonistas to the arthritic hand of the free open-source marketplace? Why create yet another techno-problem that could have been trivially solved with a 275-line decorator in the official Python stdlib?

I have no Python and I must scream.

  • 我希望有一些实质性的评论.我受到纪律正常化的欢迎.对于主要受"Monty Python's Flying Circus"出版剧本启发的无限制语言,Pythonistas中可接受行为的肮脏窗口令人惊讶...... _narrow._不用说,我在整体上不同意:我们需要大致_更多意识流,模因,笑话,开明的gnosis和noetic诗歌.**更多的多音节新奇.单音节正常性较低.** (25认同)
  • 请将meta保留在meta中. (9认同)
  • @Izik:我寻找高质量的答案,让我在一两周内不必再次搜索。如果问题需要一个简短的答案,那就太好了,如果需要更多的文字,那就这样吧。从长远来看,这比有数百个单行文字有用得多,这些单行文字不会增加我的理解,而且基本上都是相同的。 (6认同)
  • 这是一个非常有用的装饰器 - 它可能值得托管在 github 上,以便我们可以及时了解以后的增强功能 (3认同)
  • 感谢您的努力,但是对于一个简单的问题,此答案太长了。我们大多数人都在寻找“ Google”答案。 (3认同)
  • 有没有计划在 PyPI 上发布? (3认同)
  • 非常有趣的答案,阅读起来很愉快。对于所有对项目状态感兴趣的人:https://github.com/beartype/beartype 和 https://pypi.org/project/beartype/ (3认同)

bru*_*ers 65

最Pythonic的习惯用法是清楚地记录函数所期望的内容,然后尝试使用传递给函数的任何内容,并让异常传播或仅捕获属性错误并引发异常TypeError.应尽可能避免类型检查,因为它违反鸭子类型.价值测试可以 - 根据具体情况而定.

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

  • 我憎恶专制的非答案,减少到:"不要试图做你想做的事,因为我知道的更好." 这是另一个令人遗憾的长期回答.检查类型存在许多有效的理由,其中几个非答案甚至暗示.在Python 3.x下,最佳(而且很明显)答案是**装饰器和函数注释.**另见[sweeneyrod](/sf/users/167115931/)的精彩[`@ checkargs` decorator](/sf/answers/1377947371/)如下._**tl; dr**少原教旨主义; 更多实际答案._ (13认同)
  • 它可能不是Pythonic但我鼓励强制模块之间的接口,特别是如果你正在分发它们.这使得开发和使用变得更加容易,并且在所有语言中都是如此 (8认同)
  • 就我而言,我只使用断言"这不可能发生"的情况(我们都知道最终会发生一天或另一天).请注意,"优化的"字节码(.pyo文件)会跳过断言,因此您最好不要依赖AssertionError来生成代码<g>. (4认同)
  • 这不应该是公认的答案.类型需要排队的另一个非常重要的地方是外部API.有时,不可能从这样的API中传播错误,尤其是本地API,并且必须使用精确类型的参数调用它们.在这种情况下,鸭子打字会对你起作用. (3认同)
  • @carmellose:使用`locals()`可能最终会成为无用的并发症 - 事实上我没有看到它的用例,因为你已经知道你的命名params名称(显然是<g>)并且可以直接访问`args`和`kwargs`如果你的函数使用它们.此外,断言主要用于调试.如果你的函数的契约是arg'a'必须是0到10之间的int而参数'b'必须是非空字符串,那么引发相应的异常类型,即`TypeError`或`ValueError` - 试试`int你的Python shell中的('a')`和`int(None)`. (2认同)
  • 我同意类型检查参数很重要。特别是如果您将函数作为参数传递,那么您想要调用以对实例数据执行某些功能。 (2认同)

rlm*_*lms 21

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

在你的代码中,假设参数(在你的例子中a)像a int和quacks一样走路int.例如:

def my_function(a):
    return a + 7
Run Code Online (Sandbox Code Playgroud)

这意味着您的函数不仅可以使用整数,还可以使用浮点数和任何定义了__add__方法的用户定义的类,因此如果您或其他人想要将函数扩展到更少,则必须进行更少(有时无需)与别人合作.但是,在某些情况下你可能需要一个int,所以你可以这样做:

def my_function(a):
    b = int(a) + 7
    c = (5, 6, 3, 123541)[b]
    return c
Run Code Online (Sandbox Code Playgroud)

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

在回答你的其他问题时,我认为这是最好的(正如其他答案所说的那样:

def my_function(a, b, c):
    assert 0 < b < 10
    assert c        # A non-empty string has the Boolean value True
Run Code Online (Sandbox Code Playgroud)

要么

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
Run Code Online (Sandbox Code Playgroud)

我做的一些类型检查装饰器:

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))
Run Code Online (Sandbox Code Playgroud)


Mat*_*rde 11

一种方法是使用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'
Run Code Online (Sandbox Code Playgroud)

  • 当我不尊重它的合同时,我不期望一个可调用的引发AssertionError,这不是你在标准库中找到的.在Python shell中尝试int('a')和int(None)...是的,`ValueError`和`TypeError`. (8认同)
  • 断言应该被认为是一个简单的选择,通常远比没有好 - 导致早期失败并且可以帮助记录代码.我认为他们在我们的代码中占有一席之地. (7认同)
  • 谢谢,我发现断言很方便.人们出于不同的原因使用Python.有些用于编写生产代码,有些用于编写原型.这是对函数输入施加约束的快速方法.如果我正在为标准库编写函数,我可能会更明确. (2认同)
  • +1 总比没有好,但避免将它们用于外部输入验证,而将它们用于代码内检查。 (2认同)
  • 注意在生产代码中使用“ assert”。根据执行代码的环境,它可能会被忽略。看一下这个答案:/sf/answers/128688801/ (2认同)

小智 6

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

@accepts(int, int, float)
def myfunc(i1, i2, i3):
    pass
Run Code Online (Sandbox Code Playgroud)

  • 在 Python 3.x 下,函数注释(例如,`def myfunc(i1: int, i2: int, i3: float)`)是一种 _profoundly_ 更 Pythonic 的声明类型的方法。请参阅 [sweeneyrod](/sf/users/167115931/) 的 [`@checkargs` 装饰器](/sf/answers/1377947371/) 以获得强大的类型检查解决方案少于 10 (!) 行代码的函数注释。 (3认同)

Gam*_*iac 5

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

  • isinstance(obj, type)函数接受你的变量,obj并给你True它是type你所列出的相同类型.

  • 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())
Run Code Online (Sandbox Code Playgroud)

hasattr更倾向于鸭子打字,而且通常更加pythonic,但这个术语是固执己见的.

正如注释一样,assert语句通常用于测试,否则只需使用if/else语句.


sma*_*rie 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'}
Run Code Online (Sandbox Code Playgroud)

这是利用 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].
Run Code Online (Sandbox Code Playgroud)

  • “beartyping”似乎与大多数类型检查器类似,例如 [typeguard](https://github.com/agronholm/typeguard)、[pytypes](https://github.com/Stewori/pytypes/)、[enforce]( https://github.com/RussBaz/enforce)...除了它不是经过验证和记录的库之外,它不符合 PEP484 (如 [PyContracts](https://pypi.org/project/PyContracts/) ),并且它使用 `exec` 使包装器运行得更快一点(以无法调试为代价)。`valid8` 旨在验证类型和值,并且可以与现有的 PEP484 类型检查器结合使用以便仅专注于值检查 (2认同)