写一个资源服务-04.有效提高开发效率 都第四个文章了, 还没有进入政体, 是不是觉得特别拖沓呢?
实际上我在开篇的时候交代过, 这次想要把这个系列的文章写的细一点, 所以给人感觉比较琐碎, 但我觉得新手需要这些细节方面的知识!
此外去年的十二月份事情比较多, 而且姓楊的幾個朋友來找我, 耽误了很多时间……
接下来我们看一下在python里如何对源代码做静态分析和运行测试发现潜在的bug; 提高开发效率。
静态分析 我们知道python是一个动态类型的语言, 很多严重的错误只能在运行阶段暴露, 比如你写错了某个函数的参数类型, 必须实际运行的时候才能发现这一错误。
而静态类型的语言在编译阶段发现大量的类似错误, 这样在编译器的帮助下很快修正错误, 而动态类型的语言为了发现这些错误各种折腾, 费时费力。
那么python有没有办法在运行程序之前检测这些错误? 答案是肯定的, 我们需要借助于python类型注解和mypy工具来达到这一目的。
类型注解 所谓的类型注解就是给python代码添加类型信息, 比如以前的函数是这样定义:
def add (a, b ): return a + b
有了类型注解后变成这样:
def add (a: int , b: int ) -> int : return a + b
这样就特别像rust的函数定义了, 如果你有C系列语言的经验的话可能有点不适应:
int add (int a, int b) { return a + b; }
这是传统的做法, 类型在前; 名称在后, 但是比较新的编程语言把位置换过来了。
不管怎么说有类型注解就是好事, 但是python在运行程序之前并不会检查类型, 换句话说就是运行程序的时候加不加类型注解并无任何区别, 类型注解只是给人看的。
mypy类型静态检查工具 但是有人实现了mypy 静态类型分析工具。
mypy有助于您正确使用变量和函数, 如果试图传递错误类型mypy发出警告。
如下是一个简单例子:
def add (a: int , b: int ) -> int : return a + b r: str = add(1 , 2 ) print (add(1 , 1 ))print (add("hello" , 2 ))r: float = add(40 , 50 )
如果这段代码直接运行的话报错, 第二行 TypeError: can only concatenate str (not “int”) to str```
但是这个错误信息不是真正的bug, 我们确定函数本身没有任何问题, 只是调用函数的时候传入错误类型而已。 接下来用mypy来分析看看输出结果, mypy直接用pip安装就可以了: ```bash $ mypy add.py add.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "str") add.py:6: error: Argument 1 to "add" has incompatible type "str"; expected "int" add.py:7: error: Name "r" already defined on line 4 Found 3 errors in 1 file (checked 1 source file)
三个错误都发现了, 确实很好用很强大。
def add (a: int , b: int ) -> int : return a + b r: int = add(1 , 2 ) print (add(1 , 1 ))print (add(10 , 2 ))r = add(40 , 50 )
在次mypy检查:
$ mypy add.py Success: no issues found in 1 source file
接下来可以放心运行程序了, 如果没有类型注解和mypy编写函数的时候需要大量的输入验证代码, 如同这样的语句:
def add (a, b ): if isinstance (a, int ) and isinstance (b, int ): return a + b
实际应用场景只会更复杂更繁琐。
所以以后python也可以和静态类型语言一样 玩耍了……
测试入门 其实测试这样一个主题的范畴特别大, 这里我们使用一些自动化工具来验证程序的正确性, 打个比方。
python字符串有个lower方法, 它把字符串转换为小写形式。
>>> "Hello" .lower() == "hello" True >>> "hello" .lower() != "HELLO" True >>> "HELLO" .lower() == "Hello" False >>> "HELLO" .lower() == "HELLO" False
这是最原始; 最简陋的四组测试用例, 两组成功; 两组失败, 通过率50%。
如果测试简单程序完全可以这样手动编写, 但是遇到有一定规模的程序这样搞肯定是得不偿失的。
所以人们发展出了自动化测试工具, 比如说python的测试工具pytest。
pytest pytest也是用pip安装即可……
我们有个函数addUInt相加无符号整数, 如果传入有符号整数引发ValueError异常:
def addUInt (a: int , b: int ) -> int : if a < 0 or b < 0 : raise ValueError("输入不能为负数" ) return a + b
接下来给这个函数编写pytest测试用例, 测试用例的名称用test开头, 包括文件名; 类名; 函数名, 只要用test开头, 那么pytest工具自动查找运行这些用例。
我们的addUInt函数在full.py文件里, 而测试用例在test_full.py文件里。
测试用例如下:
import pytestfrom full import *def test_addUInt (): assert addUInt(1 , 1 ) == 2 assert addUInt(1 , 1 ) != 1 def test_addUInt_ValueError (): with pytest.raises(ValueError): addUInt(-1 , 100 )
首先第一个测试函数用assert来断言表达式, 如果表达式结果为true测试通过, 否则测试失败, 而第二个函数如果剖出ValueError异常测试通过, 否则测试失败。
接下来看看测试运行结果:
$ pytest ================================================= test session starts ================================================= platform win32 -- Python 3.10.4, pytest-7.1.3, pluggy-1.0.0 rootdir: 测试文件夹 collected 2 items test_full.py .. [100%] ================================================== 2 passed in 0.02s ==================================================
继续给test_full.py添加两个失败测试:
def test_faile1 (): assert addUInt(48 , 52 ) == 101 def test_faile2 (): with pytest.raises(ValueError): addUInt(20 , 0 )
加上了两个失败用例, 看看这次运行结果:
$ pytest ================================================= test session starts ================================================= platform win32 -- Python 3.10.4, pytest-7.1.3, pluggy-1.0.0 rootdir: 测试路径 collected 4 items test_full.py ..FF [100%] ====================================================== FAILURES ======================================================= _____________________________________________________ test_faile1 _____________________________________________________ def test_faile1(): > assert addUInt(48, 52) == 101 E assert 100 == 101 E + where 100 = addUInt(48, 52) test_full.py:14: AssertionError _____________________________________________________ test_faile2 _____________________________________________________ def test_faile2(): > with pytest.raises(ValueError): E Failed: DID NOT RAISE <class 'ValueError' > test_full.py:18: Failed =============================================== short test summary info =============================================== FAILED test_full.py::test_faile1 - assert 100 == 101 FAILED test_full.py::test_faile2 - Failed: DID NOT RAISE <class 'ValueError' > ============================================= 2 failed, 2 passed in 0.12s =============================================
可见pytest给出的信息多么全面, 只要根据pytest提供的线索, debug易如反掌, 不过前提是你正确编写测试用例, 如果测试用例有问题那叫病入膏肓; 错上加错……
需要补充的是上面介绍的两个工具都有缓存机制, 如果项目规模比较大, 第一次运行需要一定时间, 第二次直接用缓存大幅度加快运行速度。
这些工具的更多用法键后续文章。
未完待续……