类型标注
Python 从来不问一个变量里装的是什么类型。短程序因此写得短,可程序一大,就容易忘了一个函数要的到底是什么:一个数、一个字符串,还是一个列表,列表里又是什么?类型标注把答案直接写进代码。它是写给人、编辑器和检查工具看的说明。这是整套课的最后一课,讲怎么写标注,以及它能做什么、不做什么。
第一个类型标注
参数的标注写在参数名后面,用冒号隔开。返回值的类型写在 -> 后面,在 def 那一行末尾的冒号之前。有默认值的,默认值写在标注后面。
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。」变量也能标注,同样用冒号:
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 运行代码时根本不看标注。传错了类型,也没有谁拦着:
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))
/ 的结果总是 float,所以 half 返回 2.5。-> int 承诺返回 int,但 Python 不检查。换成 mypy 这样的检查工具,就会指出这个承诺没做到。
容器的标注
列表要说明里面装的是什么:list[int] 是一个装 int 的列表。字典要写两个类型,一个给键,一个给值。
| 标注 | 意思 | 例子 |
|---|---|---|
list[int] | 装 int 的列表 | [3, 8, 2] |
dict[str, int] | 键是字符串、值是 int 的字典 | {"磨坊": 5} |
tuple[str, int] | 先是一个 str、再是一个 int 的元组 | ("阿达", 12) |
set[str] | 装字符串的集合 | {"铁", "金"} |
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」。
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 的参数也这样标:
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)
find_lamp 返回 None,None + 1 是 TypeError。标注里的 | None 是提醒调用的人:先看看是不是 None。mypy 不用运行程序就会指出这一行。
查看标注
Python 不检查标注,但会把它们留着。每个函数都有一个字典 __annotations__,一条标注一项;返回值的类型在 "return" 下面。
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:
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 课)里,标注不是可有可无的:有标注的那一行才算一个字段。
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 安装(浏览器里跑不了)。拿这个文件来说:
1def lamps_needed(houses: int, per_house: int = 2) -> int:2 return houses * per_house3 4print(lamps_needed("五"))
$ 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,它离不开标注。
- 短脚本和小试验用不着,就像这套课里的大多数例子。标注是可选的,有用的地方加上就好。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
给面积加标注
✓给 area 加上类型标注:两个参数都是 int,结果也是 int。最后一行就会显示它的标注。
{'width': <class 'int'>, 'height': <class 'int'>, 'return': <class 'int'>}1def area(width, height):2 return width * height3 4print(area.__annotations__)
矿石在哪一车
✓把 find_cart 写完:返回 ore 在 carts 里的位置,不在里面就返回 None。
1 None
1def find_cart(carts: list[str], ore: str) -> int | None:2 pass3 4print(find_cart(["铁", "金", "铜"], "金"))5print(find_cart(["铁", "金", "铜"], "银"))
给方法加标注
✓给 refill 加上类型标注:amount 是 int,方法什么都不返回。最后一行就会显示它的标注。
{'amount': <class 'int'>, 'return': None}1class Lamp:2 def __init__(self) -> None:3 self.oil = 04 5 def refill(self, amount):6 self.oil += amount7 8print(Lamp.refill.__annotations__)
这一课学会了
名字: 类型标注参数或变量;-> 类型标注函数返回什么。- Python 运行时不检查标注:它是写给人、编辑器和 mypy 这类检查工具看的承诺。
- 容器要说明装的是什么:
list[int]、dict[str, int]、tuple[str, int]、set[str]。 int | None(或者Optional[int])表示这个值可能没有。函数.__annotations__里存着标注;dataclass 靠它找到自己的字段。
这是最后一课了。现在你已经能写真正的程序:挑一个自己的想法,比如待办清单、一个小测验,或者帮 Wick 记录每盏灯的油,到 Python 编程台 里一点一点做出来。卡住了就回来翻需要的那一课,这里的每个例子都还能运行、能单步。