Skip to content

类型提示

Python允许在代码中标注类型提示。在Python3.6之后,类型提示已经基本可用,但本页中的部分代码需要在更高的版本中运行,建议使用最新版本。

同时Python是一门弱类型语言,运行时不会因为实际类型与标注类型不一致而报错。标注类型的主要目的是帮助开发者自己或团队间的协作,以及方便IDE进行类型检查。

此外,注释不等同于声明,这一点与C语言有显著不同。在Python中,声明name: str之后,name依然是未定义的状态,而不像C语言中会有一个默认值(或者随机值)。

严格的类型检查

Python本身的弱类型特点让本页教程中的类型提示显得似乎有些多余。

这对于编程语言的初学者来说也许是一件好事,但并非所有程序员都觉得弱类型是更好的选择。

好在,你可以通过一些方法来在静态检查时配合类型提示发现潜在的类型错误。例如,使用mypy,这是一个第三方的类型检查库,需要首先通过pip install mypy安装。然后,在命令行中运行mypy <文件名>.py即可执行静态检查。

库?

库/模块,即packagemodule,是Python中常用的代码封装形式。

如果你对这不是很了解,可以暂时忽略,专注于本页的内容即可。

类型提示自然是类型的提示,需要你首先了解Python中的那些类型。如果你不记得了,回去看基本类型哦。

基本注释

python
name: str = "MangoFanFanw"
age: int = 18
money: float = 6.66
languages: tuple = ("Python", "C")
friends: list = ["QAQ", "AWA", "PWP"]
gpa: dict = {"Math": 2, "English": 3, "PE": -6}

上面分别是Python中的字符串、整形、浮点数、元组、列表、字典类型。实际上这里的类型提示是没有必要的,因为IDE可以根据等号后面的类型推断等号前面的类型。

那么在什么情况下我们需要这样的注释呢?

python
for string in stringList:
    string: str
    print(string.split(","))

例如处理stringList,学过小学二年级的程序员都能看出来这是一个由字符串组成的列表,那么从中提取的每一个string都应该是字符串,从而允许我们进行split()字符串切割操作。

但是IDE看不出来,怎么办呢?欸!加上类型提示string: str就好嘞。

在实际运行中,如果阴差阳错,stringList中混入了一个整形,Python会在pring(string.split(","))报错,而非string: str,这说明类型提示终归是提示,也证明了Python是一门弱类型语言。

另外,你也可以将变量指定为你定义的类型,例如:

python
class Cat:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
    def miaow(self):
        print(f"{self.name}: Miaow Miaow!!")
        
# 假设这里有很多很多代码

for cat in catList:
    cat: Cat
    cat.miaow()

函数相关提示

python
def add(x: int, y: int) -> int:
    return x+y

上方的add函数接收xy两个参数,返回一个变量,三个都是整形类型。这样的定义可以帮助IDE为你提供更好的函数签名辅助。

如果函数不返回值,则可以写为-> None:

函数签名

函数签名似乎是来自C++的概念,是指函数的名称及其参数类型的组合,用于区分不同的函数。函数签名不包括返回类型,也不包括参数的名字。

同样,如果你使用print(add("Mango", "FanFan")),会得到MangoFanFan而不是一个TypeError,这说明函数签名也不会在运行时进行检查。

同样,可以使用你定义的其他类来作为注释。

传入函数

你完全可以将函数当作对象处理,自然也可以将函数作为参数传入另一函数。你可以使用callable提示表示传入的对象是一个函数,或者说可调用,毕竟函数就是典型的可调用对象。

python
def add(x: int, y: int) -> int:
    return x+y

def sub(x: int, y: int) -> int:
    return x-y

def calc(x: int, y: int, action: callable) -> int:
    return action(x, y)

print(calc(10, 20, add))
print(calc(10, 20, sub))
python
30
-10

你也可以更详细地标注传入的可调用对象(即函数)的传入类型和返回类型,此时需要将callable换成typing.Callable,以下是示例:

python
from typing import Callable


def add(x: int, y: int) -> int:
    return x+y

def sub(x: int, y: int) -> int:
    return x-y

def calc(x: int, y: int, action: Callable[[int, int], int]) -> int:
    return action(x, y)

print(calc(10, 20, add))
print(calc(10, 20, sub))

Callable[[], ]中,内层的中括号中是传入值的类型,有多少写多少;外层中括号的第二个参数是返回值的类型。以上代码的打印结果相同。

你知道吗?

将函数作为参数传入函数,实际上就是Python中的装饰器的底层原理。

装饰器将在后面介绍哦。

复杂提示

多种类型

你可以在类型提示中表明一个对象同时有可能是多种类型,例如下面的类型提示表明变量username有可能是字符串类型或整形类型。

python
username: str | int

如果你的Python版本不支持使用|,你可以将|替换成or

复杂数据结构

对于字典、列表和元组之类类型的变量,你也可以描述其内部数据结构:

python
user_info: dict[str: str | int]
likes: list[str]
best_friends: tuple[str, str]

对于字典这种键值对结构,以冒号分隔键和值的类型,上面的第一行代码表明user_info的每一个值都是字符串或整形。列表likes中的每一个变量都是字符串,元组best_friends中有两个字符串变量。注意在类型注释中只使用方括号[],而不是对字典用{}、对元组用()

如果你的Python版本不支持形如dict[...]这样的类型提示,你可以将其替换成Dict[...],然后记得在之前from typing import Dictlist等依次类推。

类型别名

对于反复出现的同一类型提示,你可以按照如下方式复用提示以减少重复工作。

python
InfoDict = dict[str: str | int]

user_info: InfoDict
like_info: InfoDict
friends: list[InfoDict]

在上面的代码中,IndoDictdict[str: str | int]的类型别名。

自定义新类型

如果你对代码规范性有更高的要求,或者需要更加严谨的代码,可以自定义新类型。首先需要从typing中导入NewType

python
from typing import NewType  

PlayerHP = NewType('PlayerHP', int)  
PlayerAge = NewType('PlayerAge', int)  

a = 100
b = PlayerHP(100)
c = PlayerAge(100)

print(type(a) == type(b))
print(type(a) == type(c))
print(type(b) == type(c))

class Player:
    def __init__(self, hp: PlayerHP, age: PlayerAge): 
        self.hp = hp
        self.age = age

    def __str__(self):
        return f'Player(hp={self.hp}, age={self.age})'

p1 = Player(PlayerHP(100), PlayerAge(20))
p2 = Player(100, 20)
print(p1)
print(p2)

上面的代码首先定义了PlayerHPPlayerAge两种自定义类型,它们都是整形类型的子类型,因此100PlayerHP(100)PlayerAge(100)三个对象在Python层面是相等的。上面代码的运行结果如下:

python
True
True
True
Player(hp=100, age=20)
Player(hp=100, age=20)

但是使用mypy检查此文件,会得到以下错误:

python
xxx.py:23: error: Argument 1 to "Player" has incompatible type "int"; expected "PlayerHP"  [arg-type]
xxx.py:23: error: Argument 2 to "Player" has incompatible type "int"; expected "PlayerAge"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

可见,mypy根据代码第15行的类型提示,检查了p1p2在实例化时的传入值,并且发现第23行p2 = Player(100, 20)的传入值类型与提示不符。

注意,mypy发现的类型错误并不影响Python代码的正常运行。

本节中使用到了Python 面向对象魔术方法 的知识,关于这些的详细教程在后面哦。

泛型

再次使用我们刚才用到的例子,定义一个add函数如下:

python
def add(x: int | str, y: int | str) -> int | str:
    return x+y

print(add(1, 2))
print(add('a', 'b'))

运行得到3ab,上面的类型提示也似乎不存在问题,当我们传入两个整形时就会返回整形,传入两个字符串就会返回字符串。

但是从语义上仍然有一些不明朗,如果你运行一下mypy就会发现,如此简单的代码确报了两个错:

python
xxx.py:2: error: Unsupported operand types for + ("int" and "str")  [operator]
xxx.py:2: error: Unsupported operand types for + ("str" and "int")  [operator]
xxx.py:2: note: Both left and right operands are unions
Found 2 errors in 1 file (checked 1 source file)

用人话说就是,现在的类型提示是允许传入一个整形与一个字符串的,而这会导致return x+y报错TypeError

因此我们引入了泛型的概念。首先需要从typing中导入TypeVar

python
from typing import TypeVar  

T = TypeVar('T', int, str)  

def add(x: int | str, y: int | str) -> int | str:  
def add(x: T, y: T) -> T:  
    return x+y

print(add(1, 2))
print(add('a', 'b'))

泛型T可以是整形或字符串,但不能同时是整形和字符串,因此这限制了调用add函数时的传参类型,也更明确地指出了返回值的类型。

TypeVar()可以只接收第一个参数,这样的话就是任何类型,但依然不能同时是多个类型。

运行mypy,可以发现现在已经不会报错。

贡献者

页面历史