首页 > 解决方案 > 如何为特定类型添加检查

问题描述

很多时候,我发现自己的类型与 Python 中的基本类型极为相似,但它们有一些我希望类型检查器知道的特定属性。

例如,我可能有ColorValue = NewType('ColorValue', int)where aColorValue的范围应该在 0 到 255 之间。

然后,我希望类型检查器让我知道我是否实际上不匹配类型规范。例如,类似:

red: ColorValue = 300 # value of 300 is not compatible with type ColorValue

理想情况下,我希望能够设置这样的东西

ColorValue = NewType('ColorValue', int, check=lambda value: 0 <= value <= 255)

有没有办法让类型检查器检查特定属性?

编辑:

需要明确的是,我希望这种检查由 mypy 或 pytype 之类的类型检查器完成,并且我不希望错误仅在运行时发生。

标签: pythonpython-3.xtyping

解决方案


考虑以下名为“restrict.py”的模块

def restrict(cls, cache=[]):  
    cache.append(cls)
    return cls

def static_check(file, restrict):
    import re        
    cache = restrict.__defaults__[0]   
    with open(file) as f:
        lines = f.readlines()
    for cls in cache:
        c = cls.__name__
        for lix, l in enumerate(lines):
            m = re.findall(f"{c}[^=)]*\)", l)
            for m in m:
                try:
                    print("Attempting", m)
                    strargs = m.split(c)[1]
                    cmd = f"cls{strargs}"
                    eval(cmd)
                    print(m, "in line", lix, "evaluated")
                except ValueError as e:
                    print(m, "in line", lix,"threw",e)

和另一个名为 main.py 的模块,您要对其进行测试

from restrict import restrict, static_check

@restrict
class Restricted():
    def __new__(cls, x:int=None) -> int:
        if x is None:
            raise ValueError("Unspecified initialization")
        elif x < 0:
            raise(ValueError("<0"))
        elif x > 255:
            raise(ValueError(">255"))
        return int(x)

def output_foo(x):
    Restricted(-1)
    return Restricted(999)

Restricted(1)

if __name__ == "__main__":
    static_check(__file__, restrict)   

python main.py从终端运行会打印你

Attempting Restricted()
Restricted() in line 5 threw Unspecified initialization
Attempting Restricted(-1)
Restricted(-1) in line 16 threw <0
Attempting Restricted(999)
Restricted(999) in line 17 threw >255
Attempting Restricted(1)
Restricted(1) in line 19 evaluated

不使用该子句保护 static_checkif __name__ == "__main__"将允许您在导入时进行检查。

旧答案

您可以在解析时检查,例如假设您有一个restricted.py使用以下代码命名的文件:

class Restricted():

    def __new__(cls, x):
        import sys
        lineno = sys._getframe().f_back.f_lineno
        if x < 0:
            print(f"Value {x} is too low in line {lineno}")
        if x > 255:
            print(f"Value {x} is too high in line {lineno}")
        return int(x)

def invalid_foo(x:Restricted=Restricted(300)):
    return x

def valid_foo(x:Restricted=Restricted(222)):
    return x

Value 300 is too high in line 13它会在您导入模块/解析代码时打印,例如从 bashpython restricted.py中除mypy restricted.py.

显然,mypy 和 pytype 都没有单独打印消息,所以看起来他们实际上并没有导入模块,而是直接解析文件。可以将 bash 中的类型检查和导入融合在一起tpcheck() { mypy $1 && python $1; },然后您可以调用tpcheck restricted.py两者来完成。

请注意:NewType 实际上并没有创建一个新类。正如 _doc__ 所说:“在运行时,NewType(name, tp) 返回一个简单地返回其参数的虚拟函数”。

另一种选择可能是自动单元测试生成,例如使用auger-python. 例如,当我们将以下代码添加到前面的代码段时:

def output_foo():
    return Restricted(999)

if __name__ == '__main__':
    import auger

    with auger.magic([Restricted]):
        output_foo()

tpcheck 还向我显示了 output_foo 中的错误,即Value 999 is too high in line 22. 请注意,我展示了螺旋钻的一个错误,我必须手动修复(参见https://github.com/laffra/auger/issues/23)。此外,mypy 抱怨缺少对螺旋钻的导入,所以我不得不重新定义tpcheck() { mypy $1 --ignore-missing-imports && python3 $1; }.

但归根结底,这归结为调用函数,并在运行时执行它。看不出你怎么能逃避它,但至少你可以尝试尽可能地自动化它。


推荐阅读