Typings是什么
Python 从版本3.5开始支持了typing,也就是类型提示(type hints),算是为开发者解决了一个很头疼的问题。"类型提示",顾名思义就是为开发者提示参数或者返回值的类型。小伙伴们看一下下面的代码:
-
def calculate_distance(a: Point, b: Point) -> float:
-
# 实现过程省略
这个函数非常易懂,它计算两个点之间的距离,返回值类型为float, 而两个参数类型为Point类型。
常见误区
有的小伙伴会误将这个类型提示跟类型检查混为一谈,需要注意的是:虽然Python加入了type hints,但是Python依然是一门动态语言,从根本上跟C++, JAVA这样的强类型静态语言不同,这也注定了Python不可能在运行时做类型检查。理论上讲,Python也能够做运行时类型检查,但是这个代价也太高了,会讲本来就运行慢的毛病让人更加诟病不已。type hints的目的是为了在开发期间,而不是运行期间,提供给开发者检查类型的能力,这个类型的检查主要靠工具例如各种linter,各种IDE以及IDE的插件,例如VS Code, PyCharm等等都对type hints类型提示有良好的支持。
常见用法和例子(来自mypy文档)
变量的类型
-
# This is how you declare the type of a variable
-
age: int = 1
-
-
# You don't need to initialize a variable to annotate it
-
a: int # Ok (no value at runtime until assigned)
-
-
# Doing so can be useful in conditional branches
-
child: bool
-
if age < 18:
-
child = True
-
else:
-
child = False
'
运行
简单类型的变量,例如int, str, bool, float等等,类型提示的写法如上所示。
内建类型
-
x: int = 1
-
x: float = 1.0
-
x: bool = True
-
x: str = "test"
-
x: bytes = b"test"
'
运行
对于内建类型,可以直接用类型的名字作为类型注解。注意这里当我们声明变量的同时为变量赋值的时候,类型系统能够根据初始赋值推导出变量类型。例如 x: int = 1这行代码可以简写为x = 1, 但是为了保持代码的可读性和一致性,我还是建议显式注明变量的类型。
集合类型
集合类型其实也属于Python语言的内建类型,但是因为有其特殊性,这里单独说一说。对于Python 3.9及以上,我们使用list/set/dict/tuple等类型名字就可以了,注意这些类型名称都是小写字母开头的。
-
# Python 3.9+
-
-
x: list[int] = [1]
-
x: set[int] = {6, 7}
-
-
# 对于字典类型,我们需要指定键和值的类型
-
x: dict[str, float] = {"field": 2.0}
-
-
# 对于元组类型,如果元组是固定大小,我们指定所有元素的类型
-
x: tuple[int, str, float] = (3, "yes", 7.5)
-
-
# 对于可变大小的元组类型,我们使用单一元素类型,其他的使用省略符号
-
x: tuple[int, ...] = (1, 2, 3)
'
运行
而如果你使用的是比较早的Python 3版本,那么你需要导入typing包来使用对应的几个类型注解。typing包中定义的类型都是大写字母开头的。
-
# Python 3.8或者更低版本
-
from typing import List, Set, Dict, Tuple
-
-
x: List[int] = [1]
-
x: Set[int] = {6, 7}
-
x: Dict[str, float] = {"field": 2.0}
-
x: Tuple[int, str, float] = (3, "yes", 7.5)
-
x: Tuple[int, ...] = (1, 2, 3)
'
运行
另外有两个常见的类型是Union和Optional
Union和Optional
Union用在变量或返回值有多种可选类型的情况下;
-
# Python 3.10+ 中使用操作符 “|”来表示有多个候选类型的情况
-
x: list[int | str] = [3, 5, "test", "fun"]
-
-
-
from typing import Union, Optional
-
-
# 在Python 3.10之前的版本中使用Union来注解
-
x: list[Union[int, str]] = [3, 5, "test", "fun"]
-
'
运行
而Optional,用于处理空值None的情况。
-
# Optional[X] 等同于 X | None 或者 Union[X, None]
-
x: Optional[str] = "something" if some_condition() else None
-
if x is not None:
-
print(())
-
类型别名(Alias)
-
Url = str
-
-
def retry(url: Url, retry_count: int) -> None: ...
'
运行
函数(Functions)
对于函数的定义中的类型提示,没有什么特别的地方。
-
def stringify(num: int) -> str:
-
return str(num)
-
-
-
def plus(num1: int, num2: int) -> int:
-
return num1 + num2
-
-
-
# 注意参数缺省值的写法。另外如果函数没有返回值,需要注解为None
-
def show(value: str, excitement: int = 10) -> None:
-
print(value + "!" * excitement)
'
运行
需要瞄一眼的地方就是位置参数和关键字参数,它们的类型注解写法如下。它表示每个参数的类型都是str类型。
-
def call(self, *args: str, **kwargs: str) -> str:
-
request = make_request(*args, **kwargs)
-
return self.do_api_query(request)
'
运行
下面看看Python中函数的典型用法,有趣一点的那种。
-
x: Callable[[int, float], float] = f
-
def register(callback: Callable[[str], int]) -> None: ...
回调函数使用场景特别多,需要从typing包中导入Callable来做注解。
-
-
def gen(n: int) -> Iterator[int]:
-
i = 0
-
while i < n:
-
yield i
-
i += 1
大家都知道生成子其实就是函数。它的注解如上面代码所示,返回值是一个迭代器。注意这里也需要导入typing包。
自定义的类
-
class BankAccount:
-
-
def __init__(self, account_name: str, initial_balance: int = 0) -> None:
-
# 实例变量的类型可以自动推导得出,所以不必要手动注解。当然你也可以那么做。
-
# based on the types of the parameters.
-
self.account_name = account_name
-
= initial_balance
-
-
# 实例方法中的self不需要进行类型注解
-
def deposit(self, amount: int) -> None:
-
+= amount
-
-
def withdraw(self, amount: int) -> None:
-
-= amount
-
-
-
# 自定义的类型可以用于注解其他的函数或者类型
-
account: BankAccount = BankAccount("Alice", 400)
-
-
def transfer(src: BankAccount, dst: BankAccount, amount: int) -> None:
-
(amount)
-
(amount)
'
运行
这样经过了注解的类,是不是读起来更清爽?再也不用猜来猜去猜不出类型了。
-
-
class AuditedBankAccount(BankAccount):
-
# 实例变量的类型注解
-
audit_log: list[str]
-
-
def __init__(self, account_name: str, initial_balance: int = 0) -> None:
-
super().__init__(account_name, initial_balance)
-
# 在构造函数中注解实例变量的类型
-
self.audit_log: list[str] = []
-
-
def deposit(self, amount: int) -> None:
-
self.audit_log.append(f"Deposited {amount}")
-
+= amount
-
-
def withdraw(self, amount: int) -> None:
-
self.audit_log.append(f"Withdrew {amount}")
-
-= amount
-
-
audited = AuditedBankAccount("Bob", 300)
-
transfer(audited, account, 100)
派生类的实例被transfer函数使用时,类型检查也同样会发生。
注意,类变量的注解比较特殊,需要用到ClassVar这个类型。
-
# You can use the ClassVar annotation to declare a class variable
-
class Car:
-
seats: ClassVar[int] = 4
-
passengers: ClassVar[list[str]]
如果你的自定义类比较灵活,例如使用了自定义的属性,那么你需要想明白这些属性可以是哪些类型。
-
-
# 如果你定义的类中包含有动态属性,那么会好玩一些~~
-
class A:
-
# 这里期望属性的值为int类型。如果你不希望限定类型,可以使用"value: Any"
-
def __setattr__(self, name: str, value: int) -> None: ...
-
-
# 这里的返回值类型其实就是属性的类型,已经被限定为int了。
-
def __getattr__(self, name: str) -> int: ...
-
-
a = A()
-
= 42 # Works
-
= 'Ex-parrot' # 恭喜!类型检查会失败
'
运行
写到这里,其实还有很多没有涵盖。一些高级的用法(或者古怪一点的地方),我们下一期再讨论吧,记得关注我哦。
扩展阅读
对于Python类型系统感兴趣的小伙伴们,可以进一步查阅下列资料,也欢迎和我交流。
PEP 484 – Type Hints |
mypy - Optional Static Typing for Python
mypy 1.11.1 documentation