简介
本文介绍项目迁移工具mypy, 使用 mypy,开发者可以享受静态类型的好处,如减少调试时间、增强代码可读性和易维护性。示例展示了如何在不同场景下应用 mypy,包括对字典、类和泛型的类型检查。
在处理字典时,添加类型注解能明确变量类型,提高代码清晰度。在类的示例中,展示了如何为方法添加类型注解,以增强类的安全性。泛型允许创建可复用的类型安全容器,如 Stack 类,它可以用于不同类型的元素。
mypy 支持 Python 的泛型,允许用户定义泛型类,如 Stack[T]
,并能够捕获类型错误。在 Python 3.8 之前,内置类型不直接支持泛型实例化,需要使用 typing
模块。
1 mypy 类型检查
Mypy 是 Python 的可选静态类型检查器,旨在结合动态(或“鸭子”)类型和静态类型的优点。
Mypy 将 Python 的表达能力和便利性与强大的类型系统和编译时类型检查相结合。
Mypy 类型检查标准 Python 程序;使用任何 Python VM 运行它们,基本上没有运行时开销。
将现有代码迁移到静态类型,一次一个函数。您可以在程序、模块或表达式中自由混合静态类型和动态类型。无需放弃动态类型——在有意义的时候使用静态类型。通常只需添加函数签名即可为您提供静态类型的代码。Mypy 可以推断其他变量的类型。
2 类型检测
编译时类型检查有不少优点,尤其在大型项目中。 mypy支持对python项目做如下操作:
- 静态类型使得通过更少的调试更容易找到错误。
- 通常更容易维护。
- 类型声明充当机器检查文档。静态类型使您的代码更易于理解和修改,而不会引入错误。
- 将程序从动态类型迁移到静态 类型
您可以使用动态类型开发程序并在代码成熟后添加静态类型,或者将现有的 Python 代码迁移到静态类型。
简单例子:一个hello word 程序,展示mypy如何使用的
mypy main.py --python-version 3.6 mypy main.py (venvbackend) # mypy resource.py --python-version 3.5 resource.py:15: error: Variable annotation syntax is only supported in Python 3.6 and greater Found 1 error in 1 file (errors prevented further checking) (venvbackend) root@fk:/data/code/hello_world# mypy resource.py --python-version 3.6 resource.py:30: note: Revealed type is "builtins.int" (venvbackend) root@fk:/data/code/hello_world# cat resource.py from typing import Dict, Optional
代码如下:
## python3.x
def square(x:float, y:float) -> float:
return x * y
# typing module was added in pyton3.5
def log(s:str, *, filename:Optional[str] = None) -> None:
...
# add in python3.6
x: int = 5
from typing import NamedTuple
class NT(NamedTuple):
x: int
y: int
## added in python3.9
# old way
def print_items(dct:Dict[str, str]) -> None:...
# def print_items[dct:dict[str,str]) -> None: ...
reveal_type(NT(1,2).y) # mypy 检测提示不匹配的地方 resource.py:30: note: Revealed type is "builtins.int"
import dataclasses
@dataclasses.dataclass
class Tiem(object):
a: str
b: int
reveal_type(Tiem("n",12).a)
统计字典中词频
在此示例中,我们为变量 d 添加了一个显式类型声明,因为它在局部上下文中并不明显。
Mypy检查动态类型字典
Display the frequencies of words in a file.
import sys
import reif not sys.argv[1:]:
raise RuntimeError('Usage: wordfreq FILE')
d = {}
with open(sys.argv[1]) as f:
for s in f: for word in re.sub('\W', ' ', s).split(): d[word] = d.get(word, 0) + 1
Use list comprehension
l = [(freq, word) for word, freq in d.items()]
for freq, word in sorted(l):
print('%-6d %s' % (freq, word))
Mypy检查静态类型字典
如下程序,显示一个文件中的词频
import sys
import re
from typing import Dict
if not sys.argv[1:]:
raise RuntimeError('Usage: wordfreq FILE')
d = {} # type: Dict[str, int]
with open(sys.argv[1]) as f:
for s in f:
for word in re.sub('\W', ' ', s).split():
d[word] = d.get(word, 0) + 1
l = [(freq, word) for word, freq in d.items()]
for freq, word in sorted(l):
print('%-6d %s' % (freq, word))
简单类型
在本例中,我们选择使用整数来表示余额。例如,这在游戏中会很好,但在其他应用程序中,不同的类型会更有意义。
Mypy检查带有动态类型的类型
class BankAccount: def __init__(self, initial_balance=0): self.balance = initial_balance def deposit(self, amount): self.balance += amount def withdraw(self, amount): self.balance -= amount def overdrawn(self): return self.balance < 0 my_account = BankAccount(15) my_account.withdraw(5) print(my_account.balance)
Mypy检查带有静态类型的类
class BankAccount: def __init__(self, initial_balance: int = 0) -> None: self.balance = initial_balance def deposit(self, amount: int) -> None: self.balance += amount def withdraw(self, amount: int) -> None: self.balance -= amount def overdrawn(self) -> bool: return self.balance < 0 my_account = BankAccount(15) my_account.withdraw(5) print(my_account.balance)
带生成器的素数筛
Mypy 检查带有动态类型,一个从2到无穷大的素数筛生产器。 可以永远生成素数
import itertools
def iter_primes():numbers = itertools.count(2) while True: # Get the first number from the iterator # (always a prime) prime = next(numbers) yield prime # This code iteratively builds up a chain # of filters... numbers = filter(prime.__rmod__, numbers)
for p in iter_primes():
if p > 1000: break print(p)
静态类型的 Mypy检查支持
静态类型的一个从2到无穷大的素数筛生产器。
import itertools
from typing import Iterator
def iter_primes() -> Iterator[int]:
numbers = itertools.count(2)
while True:
prime = next(numbers)
yield prime
numbers = filter(prime.__rmod__, numbers)
for p in iter_primes():
if p > 1000:
break
print(p)
3 检查泛型:在python中 使用 mypy
泛型
先介绍如何定义自己的泛型类,这些泛型类采用一个或多个类型参数,类似于内置类型,例如list[X]. 用户定义的泛型是一个中等高级的特性,你可以在不使用它们的情况下走得更远——请随意跳过本节,稍后再回来。
定义泛型类
内置集合类是泛型类。泛型有一个或多个类型参数,可以是任意类型。例如,具有类型参数and ,并且具有类型参数。
dict[int, str]intstrlist[int]int
程序还可以定义新的通用类。这是一个非常简单的泛型类,代表一个堆栈:
以下堆栈类创建一个类型为 T 的空列表
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self.items: list[T] = []
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> T:
return self.items.pop()
def empty(self) -> bool:
return not self.items
该类Stack可用于表示任何类型的堆栈: Stack[int]、等。Stack[tuple[int, str]]
使用Stack类似于内置容器类型:
创建一个空堆栈实例,最报错 类型错误
stack = Stack[int]()
stack.push(2)
stack.pop()
stack.push('x') # 类型错误
类型推断也适用于用户定义的泛型类型: Stack[int] 的 参数类型推断
def process(stack: Stack[int]) -> None: ...
process(Stack())
内部通用类
您可能想知道在运行Stack时索引时会发生什么 。
Stack索引返回一个通用别名 , 它在实例化Stack时返回原始类的实例:print(Stack) __main__.Stack print(Stack[int]) __main__.Stack[int] print(Stack[int]().__class__) __main__.Stack
泛型别名可以被实例化或子类化,类似于真实的类,但上面的例子说明了类型变量在运行时被擦除。
泛型Stack实例只是普通的 Python 对象,除了重载索引运算符的元类之外,它们没有额外的运行时开销或由于泛型而产生的魔力。
需要注意,在 Python 3.8 及更低版本中,内置类型 list和dict其他类型不支持索引。
这就是在模块中使用别名List等 的原因 。索引这些别名会为您提供一个通用别名,类似于通过在更新的 Python 版本中直接索引目标类而构造的通用别名:Dicttyping
3.8版本及以下版本如下:
from typing import List List[int] typing.List[int]
请注意,中的通用别名typing不支持构造实例:
from typing import List List[int]() Traceback (most recent call last): ... TypeError: Type List cannot be instantiated; use list() instead
用户定义的泛型类typing 可以用作另一个类的基类,包括泛型和非泛型。例如:
from typing import Generic, TypeVar, Mapping, Iterator KT = TypeVar('KT') VT = TypeVar('VT') class MyMap(Mapping[KT, VT]): # This is a generic subclass of Mapping def __getitem__(self, k: KT) -> VT: ... # Implementations omitted def __iter__(self) -> Iterator[KT]: ... def __len__(self) -> int: ... items: MyMap[str, int] # Okay class StrDict(dict[str, str]): # This is a non-generic subclass of dict def __str__(self) -> str: return f'StrDict({super().__str__()})' data: StrDict[int, int] # Error! StrDict is not generic data2: StrDict # OK class Receiver(Generic[T]): def accept(self, value: T) -> None: ... class AdvancedReceiver(Receiver[T]): ...
如果您希望 mypy 将用户定义的类视为映射(以及 Sequence序列等),则必须添加显式基类。
这是因为 mypy 不对 这些 ABC 使用结构子类型Iterable,这与使用结构子类型的更简单的协议不同。
Generic如果有其他包含类型变量的基类,则可以从基类中省略,例如 在上面的示例中。
如果你包含在基础中,那么它应该列出其他基础中存在的所有类型变量(或者更多,如果需要的话)。
类型变量的顺序由以下规则定义:Mapping[KT, VT]Generic[...]如果Generic[...]存在,则变量的顺序总是由它们在Generic[...] 中的顺序决定。
如果没有,则所有类型变量都按字典顺序收集(即按第一次出现)。
类型别名不定义新类型。对于泛型类型别名,这意味着用于别名定义的类型变量的变化不适用于别名。
参数化的通用别名被简单地视为原始类型,并替换了相应的类型变量。
4 小结
总之,mypy 提升了 Python 代码的健壮性,尤其是对于大型项目,它提供了类似强类型语言的额外保障,同时保持了 Python 的灵活性。 各语言发展最后都越来越相似。
参考文档