Python3类型安全type hint

type hint大纲

  1. 什么是类型安全以及我们为什么需要类型安全

  2. 如何使用Python3的type hint

  3. 老项目如何逐步引入type hint

  4. 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.TypeVartyping.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还在快速的发展中,要用动态的眼光去看代它

posted @ 2024-12-15 16:36  赵青青  阅读(18)  评论(0编辑  收藏  举报