静态分析器是帮助你检查代码的工具,而不需要真正运行你的代码。静态分析器的最基本形式是你最喜欢的编辑器中的语法高亮器。如果你需要编译你的代码,比如C++,你的编译器,如LLVM,也可能提供一些静态分析器功能来警告你潜在的问题(例如,在C++中把赋值 "=" 误为平等 "==" )。在Python中,我们有一些工具来识别潜在的错误或指出违反编码标准的情况。
在完成本教程后,你将学会这些工具中的一些。具体来说。
- Pylint、Flake8 和 mypy 这些工具能做什么?
- 什么是违反编码风格?
- 我们如何使用类型提示来帮助分析器识别潜在的bug?
让我们开始吧。

概述
本教程分三部分,分别是
- Pylint简介
- Flake8简介
- mypy简介
Pylint
Lint是很久以前创建的一个C语言静态分析器的名字。Pylint借用了它的名字,它是最广泛使用的静态分析器之一。它可以作为一个Python包,我们可以用pip 。
$ pip install pylint
然后我们的系统中就有了命令pylint 。
Pylint可以检查一个脚本或整个目录。例如,如果我们把下面的脚本保存为lenet5-notworking.py 。
import numpy as np
import h5py
import tensorflow as tf
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, Dense, AveragePooling2D, Dropout, Flatten
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.callbacks import EarlyStopping
# Load MNIST digits
(X_train, Y_train), (X_test, Y_test) = mnist.load_data()
# Reshape data to (n_samples, height, wiedth, n_channel)
X_train = np.expand_dims(X_train, axis=3).astype("float32")
X_test = np.expand_dims(X_test, axis=3).astype("float32")
# One-hot encode the output
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
# LeNet5 model
def createmodel(activation):
model = Sequential([
Conv2D(6, (5,5), input_shape=(28,28,1), padding="same", activation=activation),
AveragePooling2D((2,2), strides=2),
Conv2D(16, (5,5), activation=activation),
AveragePooling2D((2,2), strides=2),
Conv2D(120, (5,5), activation=activation),
Flatten(),
Dense(84, activation=activation),
Dense(10, activation="softmax")
])
return model
# Train the model
model = createmodel(tanh)
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
earlystopping = EarlyStopping(monitor="val_loss", patience=4, restore_best_weights=True)
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=32, callbacks=[earlystopping])
# Evaluate the model
print(model.evaluate(X_test, y_test, verbose=0))
model.save("lenet5.h5")
我们可以要求Pylint在运行之前告诉我们我们的代码有多好。
$ pylint lenet5-notworking.py
其输出结果如下。
************* Module lenet5-notworking
lenet5-notworking.py:39:0: C0301: Line too long (115/100) (line-too-long)
lenet5-notworking.py:1:0: C0103: Module name "lenet5-notworking" doesn't conform to snake_case naming style (invalid-name)
lenet5-notworking.py:1:0: C0114: Missing module docstring (missing-module-docstring)
lenet5-notworking.py:4:0: E0611: No name 'datasets' in module 'LazyLoader' (no-name-in-module)
lenet5-notworking.py:5:0: E0611: No name 'models' in module 'LazyLoader' (no-name-in-module)
lenet5-notworking.py:6:0: E0611: No name 'layers' in module 'LazyLoader' (no-name-in-module)
lenet5-notworking.py:7:0: E0611: No name 'utils' in module 'LazyLoader' (no-name-in-module)
lenet5-notworking.py:8:0: E0611: No name 'callbacks' in module 'LazyLoader' (no-name-in-module)
lenet5-notworking.py:18:25: E0601: Using variable 'y_train' before assignment (used-before-assignment)
lenet5-notworking.py:19:24: E0601: Using variable 'y_test' before assignment (used-before-assignment)
lenet5-notworking.py:23:4: W0621: Redefining name 'model' from outer scope (line 36) (redefined-outer-name)
lenet5-notworking.py:22:0: C0116: Missing function or method docstring (missing-function-docstring)
lenet5-notworking.py:36:20: E0602: Undefined variable 'tanh' (undefined-variable)
lenet5-notworking.py:2:0: W0611: Unused import h5py (unused-import)
lenet5-notworking.py:3:0: W0611: Unused tensorflow imported as tf (unused-import)
lenet5-notworking.py:6:0: W0611: Unused Dropout imported from tensorflow.keras.layers (unused-import)
-------------------------------------
Your code has been rated at -11.82/10
如果你向Pylint提供了一个模块的根目录,该模块的所有组件都将被Pylint检查。在这种情况下,你会在每一行的开头看到不同文件的路径。
这里有几件事需要注意。首先,来自Pylint的投诉有不同的类别。最常见的是我们会看到关于惯例(即风格问题)、警告(即代码的运行方式可能与你的意图不一致)和错误(即代码可能无法运行并抛出异常)的问题。它们由代码识别,如E0601,其中第一个字母是类别。
Pylint可能会给出假阳性结果。在上面的例子中,我们看到Pylint将从tensorflow.keras.datasets 的导入标记为错误。这是由Tensorflow包中的一个优化引起的,当我们导入Tensorflow时,并不是所有的东西都会被Python扫描和加载,而是创建了一个LazyLoader来帮助只加载一个大包的必要部分。这可以节省启动程序的大量时间,但也会使Pylint感到困惑,因为我们似乎导入了一些不存在的东西。
此外,Pylint的一个关键功能是帮助我们使我们的代码与PEP8的编码风格保持一致。例如,当我们定义了一个没有文档串的函数时,Pylint会抱怨我们没有遵循编码惯例,即使代码没有做错什么。
但Pylint最重要的用途是帮助我们识别潜在的问题。我们把y_train 错写成Y_train ,大写的Y ,Pylint会告诉我们,我们在使用一个变量,却没有给它分配任何值。它并没有直接告诉我们出了什么问题,但肯定会给我们指出正确的地方来校对我们的代码。同样,当我们在第23行定义变量model ,Pylint告诉我们,在外部范围有一个同名的变量。因此,后来对model 的引用可能不是我们所想的。同样地,未使用的导入可能只是我们拼错了模块的名字。
所有这些都是由Pylint提供的提示。我们仍然要用我们的判断力来纠正我们的代码(或者忽略Pylint的抱怨)。
但如果你知道Pylint应该停止抱怨什么,你可以请求忽略这些。例如,我们知道import 语句是好的,我们可以调用Pylint与。
$ pylint -d E0611 lenet5-notworking.py
其中代码E0611的所有错误将被Pylint忽略。你可以用逗号分隔的列表来禁用多个代码,例如。
$ pylint -d E0611,C0301 lenet5-notworking.py
如果你想只在特定的一行或代码的特定部分禁用一些问题,你可以给你的代码加上一个特殊的注释,如下所示。
...
from tensorflow.keras.datasets import mnist # pylint: disable=no-name-in-module
from tensorflow.keras.models import Sequential # pylint: disable=E0611
from tensorflow.keras.layers import Conv2D, Dense, AveragePooling2D, Dropout, Flatten
from tensorflow.keras.utils import to_categorical
神奇的关键词pylint: ,将引入Pylint的特定指令。代码E0611和名称no-name-in-module 是一样的。在上面的例子中,由于这些特殊的注释,Pylint会抱怨最后两条导入语句,但不会抱怨前两条。
Flake8
Flake8这个工具确实是PyFlakes、McCabe和pycodestyle的一个包装器。当你安装flake8的时候
$ pip install flake8
的时候,你会安装所有这些依赖项。
类似于Pylint,在安装这个包之后,我们有命令flake8 ,我们可以传入一个脚本或一个目录进行分析。但是Flake8的重点是倾向于编码风格。因此,对于上述相同的代码,我们会看到下面的输出。
$ flake8 lenet5-notworking.py
lenet5-notworking.py:2:1: F401 'h5py' imported but unused
lenet5-notworking.py:3:1: F401 'tensorflow as tf' imported but unused
lenet5-notworking.py:6:1: F401 'tensorflow.keras.layers.Dropout' imported but unused
lenet5-notworking.py:6:80: E501 line too long (85 > 79 characters)
lenet5-notworking.py:18:26: F821 undefined name 'y_train'
lenet5-notworking.py:19:25: F821 undefined name 'y_test'
lenet5-notworking.py:22:1: E302 expected 2 blank lines, found 1
lenet5-notworking.py:24:21: E231 missing whitespace after ','
lenet5-notworking.py:24:41: E231 missing whitespace after ','
lenet5-notworking.py:24:44: E231 missing whitespace after ','
lenet5-notworking.py:24:80: E501 line too long (87 > 79 characters)
lenet5-notworking.py:25:28: E231 missing whitespace after ','
lenet5-notworking.py:26:22: E231 missing whitespace after ','
lenet5-notworking.py:27:28: E231 missing whitespace after ','
lenet5-notworking.py:28:23: E231 missing whitespace after ','
lenet5-notworking.py:36:1: E305 expected 2 blank lines after class or function definition, found 1
lenet5-notworking.py:36:21: F821 undefined name 'tanh'
lenet5-notworking.py:37:80: E501 line too long (86 > 79 characters)
lenet5-notworking.py:38:80: E501 line too long (88 > 79 characters)
lenet5-notworking.py:39:80: E501 line too long (115 > 79 characters)
以字母E开头的错误代码来自pycodestyle,以字母F开头的错误代码来自PyFlakes。我们可以看到它抱怨编码风格的问题,如使用(5,5) ,因为在逗号后没有空格。我们还可以看到它可以识别在赋值前使用变量的情况。但是它并没有捕捉到一些代码的气味,比如函数createmodel() 重复使用已经定义在外部范围的变量model 。
与Pylint类似,我们也可以要求Flake8忽略一些投诉。比如说。
flake8 --ignore E501,E231 lenet5-notworking.py
那么这些行将不会被打印在输出中。
lenet5-notworking.py:2:1: F401 'h5py' imported but unused
lenet5-notworking.py:3:1: F401 'tensorflow as tf' imported but unused
lenet5-notworking.py:6:1: F401 'tensorflow.keras.layers.Dropout' imported but unused
lenet5-notworking.py:18:26: F821 undefined name 'y_train'
lenet5-notworking.py:19:25: F821 undefined name 'y_test'
lenet5-notworking.py:22:1: E302 expected 2 blank lines, found 1
lenet5-notworking.py:36:1: E305 expected 2 blank lines after class or function definition, found 1
lenet5-notworking.py:36:21: F821 undefined name 'tanh'
我们也可以使用神奇的注释来禁用一些投诉,例如。
...
import tensorflow as tf # noqa: F401
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
Flake8将寻找注释# noqa: ,以跳过这些特定行的一些投诉。
Mypy
Python不是一种类型化的语言,因此,与C或Java不同,你不需要在使用前声明一些函数或变量的类型。但是最近Python引入了类型提示符号,所以我们可以指定一个函数或变量打算是什么类型,而不像类型化语言那样强制要求其遵守。
在Python中使用类型提示的最大好处之一是为静态分析器的检查提供额外的信息。Mypy是可以理解类型提示的工具。即使没有类型提示,Mypy仍然可以提供类似于Pylint和Flake8的投诉。
我们可以从PyPI安装Mypy。
$ pip install mypy
然后可以将上面的例子提供给mypy 命令。
$ mypy lenet5-notworking.py
lenet5-notworking.py:2: error: Skipping analyzing "h5py": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
lenet5-notworking.py:3: error: Skipping analyzing "tensorflow": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:4: error: Skipping analyzing "tensorflow.keras.datasets": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:5: error: Skipping analyzing "tensorflow.keras.models": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:6: error: Skipping analyzing "tensorflow.keras.layers": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:7: error: Skipping analyzing "tensorflow.keras.utils": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:8: error: Skipping analyzing "tensorflow.keras.callbacks": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:18: error: Cannot determine type of "y_train"
lenet5-notworking.py:19: error: Cannot determine type of "y_test"
lenet5-notworking.py:36: error: Name "tanh" is not defined
Found 10 errors in 1 file (checked 1 source file)
我们看到与上面的Pylint类似的错误,虽然有时没有那么精确(例如,变量y_train 的问题)。然而我们看到上面mypy的一个特点。它希望我们使用的所有库都有一个存根,这样就可以进行类型检查了。这是因为类型提示是可选的。如果一个库的代码没有提供类型提示,代码仍然可以工作,但mypy无法验证。有些库提供了类型存根,使mypy能够更好地检查。
让我们来考虑另一个例子。
import h5py
def dumphdf5(filename: str) -> int:
"""Open a HDF5 file and print all the dataset and attributes stored
Args:
filename: The HDF5 filename
Returns:
Number of dataset found in the HDF5 file
"""
count: int = 0
def recur_dump(obj) -> None:
print(f"{obj.name} ({type(obj).__name__})")
if obj.attrs.keys():
print("\tAttribs:")
for key in obj.attrs.keys():
print(f"\t\t{key}: {obj.attrs[key]}")
if isinstance(obj, h5py.Group):
# Group has key-value pairs
for key, value in obj.items():
recur_dump(value)
elif isinstance(obj, h5py.Dataset):
count += 1
print(obj[()])
with h5py.File(filename) as obj:
recur_dump(obj)
print(f"{count} dataset found")
with open("my_model.h5") as fp:
dumphdf5(fp)
这个程序应该是加载一个HDF5文件(比如Keras模型),并打印其中存储的每个属性和数据。我们使用了h5py 模块(该模块没有类型存根,因此mypy无法识别其使用的类型),但我们在定义的函数dumphdf5() 中加入了类型提示。这个函数希望得到一个HDF5文件的文件名,并打印出里面存储的所有内容。最后,将返回存储的数据集的数量。
我们把这个脚本保存到dumphdf5.py ,然后把它传到mypy中,我们会看到下面的情况。
$ mypy dumphdf5.py
dumphdf5.py:1: error: Skipping analyzing "h5py": module is installed, but missing library stubs or py.typed marker
dumphdf5.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
dumphdf5.py:3: error: Missing return statement
dumphdf5.py:33: error: Argument 1 to "dumphdf5" has incompatible type "TextIO"; expected "str"
Found 3 errors in 1 file (checked 1 source file)
我们误用了我们的函数,将一个打开的文件对象传入dumphdf5() ,而不是仅仅传入文件名(作为一个字符串),mypy可以识别这个错误。我们还声明该函数应返回一个整数,但我们在函数中没有返回语句。
然而,在这段代码中还有一个错误,mypy没有识别出来,即在内部函数recur_dump() 中使用变量count ,应该声明nonlocal ,因为它被定义在范围之外。这个错误可以被Pylint和Flake8捕捉到,但mypy却忽略了它。
下面是完整的、经过纠正的代码,没有再出现错误。请注意,我们在第一行添加了神奇的注释"# type: ignore",以消除mypy发出的打字存根警告。
import h5py # type: ignore
def dumphdf5(filename: str) -> int:
"""Open a HDF5 file and print all the dataset and attributes stored
Args:
filename: The HDF5 filename
Returns:
Number of dataset found in the HDF5 file
"""
count: int = 0
def recur_dump(obj) -> None:
nonlocal count
print(f"{obj.name} ({type(obj).__name__})")
if obj.attrs.keys():
print("\tAttribs:")
for key in obj.attrs.keys():
print(f"\t\t{key}: {obj.attrs[key]}")
if isinstance(obj, h5py.Group):
# Group has key-value pairs
for key, value in obj.items():
recur_dump(value)
elif isinstance(obj, h5py.Dataset):
count += 1
print(obj[()])
with h5py.File(filename) as obj:
recur_dump(obj)
print(f"{count} dataset found")
return count
dumphdf5("my_model.h5")
总而言之,我们上面介绍的三个工具可以相互补充。你可以考虑运行所有这些工具来寻找你的代码中任何可能的错误,或者改善编码风格。每个工具都允许一些配置,可以从命令行或配置文件中进行配置,以满足你的需要(例如,多长的一行应该是值得警告的?
总结
在本教程中,你看到了一些常见的静态分析器如何帮助你写出更好的 Python 代码。具体来说,你学到了
- 三个工具的优点和缺点,Pylint、Flake8和mypy
- 如何定制这些工具的行为
- 如何理解这些分析器的投诉