Python3类型安全type hint
type hint大纲
-
什么是类型安全以及我们为什么需要类型安全
-
如何使用Python3的type hint
-
老项目如何逐步引入type hint
-
Type hint在运行时的应用
疑问
type hint在编译时会被去掉吧?
是的,Python的类型提示(Type Hints)只是一种语法糖,它们不会影响Python代码的运行。类型提示在运行时并不会进行类型检查,也不会影响代码的性能。它们主要是用来帮助程序员理解函数期望的输入和输出类型,以及提供给静态类型检查工具和IDE使用,以帮助找出潜在的错误。
类型标注能让我的程序运行得更快吗?
不会,Python的类型标注(Type Hints)并不会让你的程序运行得更快。类型标注在Python中主要是用于静态类型检查,以及提高代码的可读性和可维护性。它们在运行时并不会进行任何类型检查,也不会改变Python的动态类型特性,因此不会影响程序的运行速度。如果你想提高Python程序的运行速度,可以考虑使用如Cython、PyPy等工具,或者优化你的算法和数据结构。
为什么新的语言大部分都选择了类型在变量名的后面?
//Rust:
fn greeting(name: &str) -> String {
}
//Go:
func greeting(name string) string {
}
//Typescript:
function greeting(name: string): string {
}
python3的type check
Literal 在Python3中,字面量(Literal)是指在代码中直接使用的特定值。字面量可以是数字字面量,字符串字面量,布尔字面量,特殊字面量,或者容器字面量
Autocomplete 自动补全
添加type hint的位置
• 函数/方法签名
• 变量初始化
name: str = "Python3"
name = "Python3" # type checker know it’s a str
def greeting(name: str) -> str
return 'Hello ' + name
多种类型(Union)
from typing import Union
def accept_task(task_id: int) -> None:
task_type: Union[str, int]
if is_side_task(task_id):
task_type = "Side Task"
else:
task_type = 1
可选import(Optional)
from typing import Optional
def accept_task(task_id: int) -> None:
task_type: Optional[str] #这两种可选写法都ok
task_type: str | None #这两种可选写法都ok
if is_side_task(task_id):
task_type = "Side Task"
else:
task_type = None
条件import(TYPE_CHECKING)
原来的import存在以下问题:
from data.config.monster import MonsterConfig
def spawn_monster(monster: MonsterConfig) -> None:
...
可能需要避免import的情况:
• 会造成循环import
• Import有side-effect或者特定的时机要求
• Import耗时太长
增加条件后的import
from __future__ import annotations
import typing
if typing.TYPE_CHECKING:
from data.config.monster import MonsterConfig
def spawn_monster(monster: MonsterConfig) -> None:
...
前向引用(前向声明)
类用字符串来代替?或者导入annotations
NewType
在Python中,类型别名是一个方便的方式,用于为复杂的类型标注提供一个简单的名称。你可以使用 typing.TypeVar
或 typing.NewType
创建类型别名。
例如,如果你有一个复杂的类型,如 List[Tuple[str, str, int]]
,你可以创建一个类型别名来简化它:
from typing import List, Tuple, TypeVar
PersonInfo = List[Tuple[str, str, int]]
def get_people_info() -> PersonInfo:
return [('Alice', 'Engineer', 30), ('Bob', 'Doctor', 40)]
在这个例子中,PersonInfo
是一个类型别名,代表 List[Tuple[str, str, int]]
类型。这样可以使代码更易读,更易维护。
Callable
在Python中,typing
模块提供了Callable
,这是一个类型提示,用于表示可调用的类型,比如函数或方法。
下面是一个使用Callable
的例子:
from typing import Callable
def apply_func(x: int, func: Callable[[int], int]) -> int:
return func(x)
def double(x: int) -> int:
return 2 * x
print(apply_func(5, double)) # 输出:10
在这个例子中,apply_func
函数接受一个整数x
和一个函数func
作为参数。func
的类型被注解为Callable[[int], int]
,这表示它是一个接受一个整数参数并返回一个整数的函数。double
函数就是这样一个函数,所以我们可以将它作为apply_func
的参数。
Protocol
由于python函数中的参数无类型,或者说我们不关心对象的类型只关心它能做什么,所以可以通过隐式或某个规则来绕过typing的检查?
在Python 3.8及以上版本中,typing
模块提供了Protocol
类,它是一种特殊的类,用于定义结构化的类型协议。如果一个类满足了Protocol
的定义,那么它就被认为是实现了该协议,无论它是否显式地继承了Protocol
。
下面是一个使用Protocol
的例子:
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None:
...
def close_resource(resource: SupportsClose) -> None:
resource.close()
class Resource:
def close(self) -> None:
print("Resource closed")
resource = Resource()
close_resource(resource) # 输出:Resource closed
在这个例子中,SupportsClose
是一个Protocol
,它定义了一个close
方法。close_resource
函数接受一个SupportsClose
类型的参数。尽管Resource
类并没有显式地继承SupportsClose
,但是它实现了close
方法,所以它被认为是实现了SupportsClose
协议,可以作为close_resource
的参数。
泛型Generic
在Python 3中,typing
模块提供了Generic
类,用于定义泛型类型。泛型类型是指在定义时不指定具体类型,而在实例化时才确定具体类型的类型。
下面是一个使用Generic
的例子:
from typing import Generic, TypeVar
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self.items = []
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> T:
return self.items.pop()
# 使用时指定具体类型
stack = Stack[int]()
stack.push(1)
print(stack.pop()) # 输出:1
在这个例子中,Stack
是一个泛型类,它接受一个类型参数T
。在实例化Stack
时,我们需要指定T
的具体类型。例如,Stack[int]()
创建了一个只接受int
类型元素的栈。
使用泛型可以提高代码的复用性,使得我们可以用一套代码来处理多种类型的数据。同时,它也可以提高类型安全性,因为我们可以在编译时检查类型的正确性。
协变与逆变Covariant & Contravariant
变成c++的写法了?这个章节的内容非常多
Mypy Plugin
目前使用最多的pright,由微软主导是vscode自带的
mypy使用的较少
https://mypy.readthedocs.io/en/stable/extending_mypy.html
• 缺少文档
• 和其他type checker不兼容
Overload
如果项目比较特殊,非要使用mypy的话,通过overload来实现
pyi文件
只包含函数和变量类型“声明”的Python文件,Pyi文件的优先级高于py文件
使用场景:
• C/C++ extension模块的类型标注
• Python2/Python3兼容代码的类型标注
• Import耗时较长的模块的类型标注
• 为原本没有类型标注的第三方库添加类型标注
放置位置:
• 位于py文件同级目录
• 位于单独的stub目录(通过参数传递给type checker)
老项目引入 type checking
兼容老代码
迭代老代码
逐步将老的代码文件从exclude中移除
• 更新或为第三方库添加type hint
• 移除disable_error_code
• 单独跳过特定的第三方库
[[tool.mypy.overrides]]
module = "some_legacy_third_party"
follow_imports = skip
禁止未标注的新代码
运行时应用(Runtime API )
类型标注Annotation
import typing
typing.get_type_hints
typing.get_origin
assert get_origin(str) is None
assert get_origin(Dict[str, int]) is dict
assert get_origin(Union[int, str]) is Union
P = ParamSpec('P')
assert get_origin(P.args) is P
assert get_origin(P.kwargs) is P
typing.get_args
assert get_args(int) == ()
assert get_args(Dict[int, str]) == (int, str)
assert get_args(Union[int, str]) == (int, str)
PEP 563
PEP 563’s default change is clearly too disruptive to downstream users
and third-party libraries to happen right now. We can’t risk breaking even
a small subset of the FastAPI/pydantic users, not to mention other uses of
evaluated type annotations that we’re not aware of yet
PEP 563 的默认更改显然对下游用户和第三方库的破坏性太大,目前无法实现。我们不能冒险破坏 FastAPI/pydantic 用户的一小部分,更不用说我们尚不知道的评估类型注释的其他用途
Pydantic
定义每个class中字段的类型,强制指定类型和字段
FastAPI
调用者就相当于在使用强类型语言
Runtime API
Bevy Style ECS API
bevy是一个使用ecs模式的游戏引擎
Mypyc
Cython 3.0
使用cython在运行时热更替换C代码
总结
Type hint能够帮助我们提早发现程序中的类型错误
• 我们可以逐步分阶段在项目中引入type hint
• 我们可以在运行时合理的利用type hint
• Type hint可以帮助我们编译出性能更高的程序
• Python的type hint还在快速的发展中,要用动态的眼光去看代它