Python >=3.5: 运行时检查类型注释

59

typing模块(或其他模块)是否有一个API可以在运行时进行类型检查,类似于isinstance(),但能够理解typing中定义的类型类别?

我想要运行类似于以下代码的内容:

from typing import List
assert isinstance([1, 'bob'], List[int]), 'Wrong type'

3
不,你不能这样做,但这里有一个类似的问题:https://dev59.com/0loT5IYBdhLWcg3w-DK6#43445558,我曾试图回答它。 - max
@max 感谢你的尝试。我实际上已经尝试通过 gittermypy 的开发人员取得联系,似乎类似的功能正在开发中,我会看看能否请项目的某个人在这里回答并在推进过程中进行更新。 - Bertrand Caron
1
据我理解,typing_inspect 并不完全符合您的需求;它更多地是关于检查类型对象本身。 - Elazar
1
这个紧密相关的问题中有一个非常精细的类型检查实现:在Python数据类中验证详细类型 - Aran-Fey
谢谢,看起来答案中有足够的代码值得打包成一个小但非常有用的程序包 ;) - Bertrand Caron
4个回答

37

我正在寻找类似的东西,发现了库typeguard。它可以在任何地方自动执行运行时类型检查。正如问题中直接检查类型一样,这也是受支持的。根据文档,


from typeguard import check_type

# Raises TypeError if there's a problem
check_type('variablename', [1234], List[int])

7
更好的是,typeguard的@typechecked装饰器可以让你自动完成对函数所有输入和输出的类型提示验证。或者,如果你将其应用于类定义上,则会对其所有方法进行运行时验证。 - crypdick

17

typing 模块中不存在这样的函数,很可能永远都不会有。

检查一个对象是否是某个类的实例——这只意味着“该对象由该类的构造函数创建”——只需要测试一些标记即可。

然而,检查一个对象是否是某种类型的“实例”并不一定是可判定的:

assert isinstance(foo, Callable[[int], str]), 'Wrong type'

虽然检查foo的类型注释很容易(假设它不是lambda),但检查它是否符合这些注释通常是不可判定的,这是由于Rice定理。

即使使用更简单的类型,例如List[int],测试也很容易变得非常低效,只能用于最小的示例。

xs = set(range(10000))
xs.add("a")
xs.pop()
assert isinstance(xs, Set[int]), 'Wrong type'

类型检查器相对高效的操作技巧是保守:类型检查器试图证明foo始终返回int。如果失败,它会拒绝程序,即使该程序可能是有效的,也就是说,这个函数很可能被拒绝,尽管它是完全安全的:


def foo() -> int:
    if "a".startswith("a"):
        return 1
    return "x"

3
是的。在回答类似的问题时,我也得出了这个结论(尽管我删除了那些段落,因为我认为我的解释不是很清楚)。我认为Python typing/PEP 484类型系统是为静态类型检查而构建的,并且不适用于动态类型检查。实际上有用的动态类型系统可以被构建,但它将会非常不同(主要是更简单)与PEP 484相比。并且可以说,一个相当不错的动态类型系统已经包含在你的现成Python解释器中。 - max
在 typing 模块中没有这样的函数,而且很可能永远不会有。-- 哎呀! - Noldorin
2
@Elazar:不错的回答!有点挑剔:我认为 Rice 定理并不足以证明类型系统是不可判定的。决定一个类型系统是否可判定并不容易。 :-) https://3fx.ch/typing-is-hard.html(我不知道 Python 是否有一个明确定义的具有某种参数多态性的类型系统。如果有,那么它很可能是不可判定的。但证明它可能需要大量的工作。) - jcsahnwaldt Reinstate Monica
@jcsahnwaldtReinstateMonica 我认为这里有两个不同的问题。一个问题是任意代码是否存在运行时类型违规,因为它是一个非平凡属性,所以这个问题总是不可判定的。您谈到了在具有语法类型规则的系统中是否存在声音类型检查算法的问题。这些规则可能是不完备的。 - Elazar

7

我最近发现了一个东西,基本上这个装饰器在运行时进行类型检查,如果某些类型定义不匹配就会引发异常。它还可以对嵌套类型(字符串字典等)进行类型检查。

https://github.com/FelixTheC/strongtyping

示例:

from strongtyping.strong_typing import match_typing

@match_typing
def func_a(a: str, b: int, c: list):
   ...

func_a('1', 2, [i for i in range(5)])
# >>> True

func_a(1, 2, [i for i in range(5)])
# >>> will raise a TypeMismatch Exception

0

使用原生的Python,inspect模块可以很容易地解决这个问题 - 不需要外部模块 :)

这有点简单化,确实;它可能无法处理深度嵌套类型(例如需要给定键/值类型的字典),但您可以使用“typing”库来扩展它。

import inspect


def enforce_type_annotation(fn):
    parameters = inspect.signature(fn).parameters
    param_keys = list(parameters.keys())

    def wrapper(*args, **kwargs):
        errors = list()

        # -- iterate over positionals
        for i in range(len(args)):
            param = parameters[param_keys[i]]
            value = args[i]

            # -- if the parameter is not annotated, don't validate.
            if not param.annotation:
                continue

            if not isinstance(value, param.annotation):
                errors.append(
                    f'Positional argument {param} was given type {type(value)} but expected {param.annotation}!'
                )

        # -- this might throw a KeyError if an incorrect argument is provided
        for key, value in kwargs.items():
            param = parameters[key]
            value = kwargs[key]

            # -- if the parameter is not annotated, don't validate.
            if not param.annotation:
                continue

            if not isinstance(value, param.annotation):
                errors.append(
                    f'Keyword argument {param} was given type {type(value)} but expected {param.annotation}!'
                )

        if len(errors):
            raise TypeError('\n'.join(errors))

        return fn(*args, **kwargs)

    return wrapper


@enforce_type_annotation
def foo(bar: bool, barry: str = None):
    return "hello world"


# -- works - keyword arguments remain optional
print(foo(True))

# -- works - all types were passed correctly
print(foo(True, 'Hello'))

# -- does not work, keyword arguments may also be passed as positional
print(foo(True, 1))

# -- does not work, "barry" expects a string
print(foo(True, barry=1))

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