Kodewick
第 31 课,共 31 课目录

类型标注

第 31 课,共 31 课 · 第 6 部分 · 进阶 · 约 20 分钟

Python 从来不问一个变量里装的是什么类型。短程序因此写得短,可程序一大,就容易忘了一个函数要的到底是什么:一个数、一个字符串,还是一个列表,列表里又是什么?类型标注把答案直接写进代码。它是写给人、编辑器和检查工具看的说明。这是整套课的最后一课,讲怎么写标注,以及它能做什么、不做什么。

第一个类型标注

参数的标注写在参数名后面,用冒号隔开。返回值的类型写在 -> 后面,在 def 那一行末尾的冒号之前。有默认值的,默认值写在标注后面。

main.py
1def greet(name: str) -> str:2    return f"晚上好,{name}!"3 4def lamps_needed(houses: int, per_house: int = 2) -> int:5    return houses * per_house6 7print(greet("Wick"))8print(lamps_needed(5))
输出
晚上好,Wick!
10

把第二个念出来:「lamps_needed 接受 int 类型的 houses 和 int 类型的 per_house,不给 per_house 就是 2,返回一个 int。」变量也能标注,同样用冒号:

main.py
1lamp_count: int = 122town: str = "像素小镇"3oil_left: float = 3.54is_night: bool = True5print(lamp_count, town, oil_left, is_night)
输出
12 像素小镇 3.5 True

大多数变量用不着:lamp_count = 12 谁都看得出是 int。看不出来的地方才需要,比如一个空列表、空字典,后面才往里放东西。

Python 不强制检查

几乎每个人都会吃一惊:Python 运行代码时根本不看标注。传错了类型,也没有谁拦着:

main.py
1def lamps_needed(houses: int, per_house: int = 2) -> int:2    return houses * per_house3 4print(lamps_needed("ab"))5print(lamps_needed(2.5))6count: int = 57count = "五"8print(count)
输出
abab
5.0
五

"ab" * 2 就是 "abab",所以调用照样成功,只是结果谁都不想要。标注没有引发报错:它不是规定,而是承诺,这里也没有人去查承诺有没有做到。查承诺是 mypy 这类工具的事(后面会讲)。

先猜一猜

这段会显示什么?

1def half(n: int) -> int:2    return n / 23 4print(half(5))

容器的标注

列表要说明里面装的是什么:list[int] 是一个装 int 的列表。字典要写两个类型,一个给键,一个给值。

标注意思例子
list[int]装 int 的列表[3, 8, 2]
dict[str, int]键是字符串、值是 int 的字典{"磨坊": 5}
tuple[str, int]先是一个 str、再是一个 int 的元组("阿达", 12)
set[str]装字符串的集合{"铁", "金"}
main.py
1def total_ore(loads: list[int]) -> int:2    return sum(loads)3 4def count_words(words: list[str]) -> dict[str, int]:5    counts: dict[str, int] = {}6    for word in words:7        counts[word] = counts.get(word, 0) + 18    return counts9 10print(total_ore([3, 8, 2]))11print(count_words(["铁", "金", "铁"]))
输出
13
{'铁': 2, '金': 1}

这里变量的标注就有用了:光写 counts = {},看不出以后要往里放什么。老代码里会看到 List[int]、Dict[str, int],是从 typing 模块导入的;Python 3.9 以前只能这么写,意思一样。

可能为空的值

很多函数返回一个值或者 None,比如一次可能什么都找不到的查找。这写成 int | None:「一个 int,或者 None」。

main.py
1def find_lamp(oil: dict[str, int], name: str) -> int | None:2    if name in oil:3        return oil[name]4    return None5 6oil = {"磨坊": 5, "桥头": 0}7print(find_lamp(oil, "磨坊"))8print(find_lamp(oil, "城门"))
输出
5
None

typing 模块里有同一个意思的老写法 Optional[int],很多代码里都会见到。默认值是 None 的参数也这样标:

main.py
1from typing import Optional2 3def light(name: str, hour: Optional[int] = None) -> str:4    if hour is None:5        return f"{name}:现在就点"6    return f"{name}:{hour} 点再点"7 8print(light("磨坊"))9print(light("桥头", 21))
输出
磨坊:现在就点
桥头:21 点再点
先猜一猜

会怎么样?

1def find_lamp(oil: dict[str, int], name: str) -> int | None:2    if name in oil:3        return oil[name]4    return None5 6print(find_lamp({"磨坊": 5}, "城门") + 1)

查看标注

Python 不检查标注,但会把它们留着。每个函数都有一个字典 __annotations__,一条标注一项;返回值的类型在 "return" 下面。

main.py
1def lamps_needed(houses: int, per_house: int = 2) -> int:2    return houses * per_house3 4print(lamps_needed.__annotations__)5print(lamps_needed.__annotations__["return"])
输出
{'houses': <class 'int'>, 'per_house': <class 'int'>, 'return': <class 'int'>}
<class 'int'>

int 这样的普通类型显示成 <class 'int'>,也就是这个类本身。容器的标注照你写的样子保存,list[str] 就是 list[str],str | None 也一样。没标注的参数没有这一项,-> None 存的就是 None:

main.py
1def area(width, height):2    return width * height3 4def greet(names: list[str], title: str | None = None) -> None:5    print(f"你好,{names}")6 7print(area.__annotations__)8print(greet.__annotations__)
输出
{}
{'names': list[str], 'title': str | None, 'return': None}

自己很少会去读 __annotations__,但工具会读:@dataclass 就是这样找到类的字段的,有些库也靠它替你检查数据。

类里的标注

方法和普通函数一样标注。self 不用标;__init__ 什么都不返回,标 -> None。在 dataclass(第 28 课)里,标注不是可有可无的:有标注的那一行才算一个字段。

main.py
1from dataclasses import dataclass2 3class Lamp:4    def __init__(self, name: str, oil: int = 0) -> None:5        self.name = name6        self.oil = oil7 8    def refill(self, amount: int) -> None:9        self.oil += amount10 11@dataclass12class Cart:13    ore: str14    weight: int15 16lamp = Lamp("磨坊")17lamp.refill(4)18print(lamp.name, lamp.oil)19print(Cart("铁", 12))20print(Lamp.refill.__annotations__)
输出
磨坊 4
Cart(ore='铁', weight=12)
{'amount': <class 'int'>, 'return': None}

检查标注,什么时候用

想让标注真的被检查,就在运行之前用类型检查工具过一遍代码。最有名的是 mypy,在自己的电脑上用 pip 安装(浏览器里跑不了)。拿这个文件来说:

lamps.py
1def lamps_needed(houses: int, per_house: int = 2) -> int:2    return houses * per_house3 4print(lamps_needed("五"))
Terminal
$ pip install mypy
$ mypy lamps.py
lamps.py:4: error: Argument 1 to "lamps_needed" has incompatible type "str"; expected "int"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

mypy 什么都没运行就找到了错误:第 4 行的第 1 个实参是 str,可要的是 int。VS Code、PyCharm 这些编辑器也读标注:边写边在类型不对的地方画线,打一个点就提示合适的方法。

什么时候值得写标注?

  • 别人要调用的函数,或者一个月后你自己还要回来看的函数:标注是一份过时了也会被发现的说明。
  • 大一点的程序,配上检查工具:一整类错误,比如忘了处理 None,在发生之前就能找出来。
  • dataclass,它离不开标注。
  • 短脚本和小试验用不着,就像这套课里的大多数例子。标注是可选的,有用的地方加上就好。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

给面积加标注

给 area 加上类型标注:两个参数都是 int,结果也是 int。最后一行就会显示它的标注。

要显示
{'width': <class 'int'>, 'height': <class 'int'>, 'return': <class 'int'>}
exercise1.py
1def area(width, height):2    return width * height3 4print(area.__annotations__)
输出
点「运行」或按 Ctrl+Enter
练习 2

矿石在哪一车

把 find_cart 写完:返回 ore 在 carts 里的位置,不在里面就返回 None。

要显示
1
None
exercise2.py
1def find_cart(carts: list[str], ore: str) -> int | None:2    pass3 4print(find_cart(["铁", "金", "铜"], "金"))5print(find_cart(["铁", "金", "铜"], "银"))
输出
点「运行」或按 Ctrl+Enter
练习 3

给方法加标注

给 refill 加上类型标注:amount 是 int,方法什么都不返回。最后一行就会显示它的标注。

要显示
{'amount': <class 'int'>, 'return': None}
exercise3.py
1class Lamp:2    def __init__(self) -> None:3        self.oil = 04 5    def refill(self, amount):6        self.oil += amount7 8print(Lamp.refill.__annotations__)
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 名字: 类型 标注参数或变量;-> 类型 标注函数返回什么。
  • Python 运行时不检查标注:它是写给人、编辑器和 mypy 这类检查工具看的承诺。
  • 容器要说明装的是什么:list[int]、dict[str, int]、tuple[str, int]、set[str]。
  • int | None(或者 Optional[int])表示这个值可能没有。
  • 函数.__annotations__ 里存着标注;dataclass 靠它找到自己的字段。

这是最后一课了。现在你已经能写真正的程序:挑一个自己的想法,比如待办清单、一个小测验,或者帮 Wick 记录每盏灯的油,到 Python 编程台 里一点一点做出来。卡住了就回来翻需要的那一课,这里的每个例子都还能运行、能单步。