0%

写一个资源共享服务 03.提升效率——mypy和pytest

写一个资源服务-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)

三个错误都发现了, 确实很好用很强大。

  • 第四行, 试图给str类型赋int值

  • 第六行试图调用str类型的参数, 正确类型是int

  • 第七行试图重复定义r变量

    根据mypy提示修改后的代码:

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 pytest
from 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%] # pytest在某些条件下跳过某些用例

====================================================== 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易如反掌, 不过前提是你正确编写测试用例, 如果测试用例有问题那叫病入膏肓; 错上加错……

需要补充的是上面介绍的两个工具都有缓存机制, 如果项目规模比较大, 第一次运行需要一定时间, 第二次直接用缓存大幅度加快运行速度。

这些工具的更多用法键后续文章。

未完待续……