Python中的typings

时间:2024-10-07 06:55:52

Typings是什么

Python 从版本3.5开始支持了typing,也就是类型提示(type hints),算是为开发者解决了一个很头疼的问题。"类型提示",顾名思义就是为开发者提示参数或者返回值的类型。小伙伴们看一下下面的代码:

  1. def calculate_distance(a: Point, b: Point) -> float:
  2. # 实现过程省略

这个函数非常易懂,它计算两个点之间的距离,返回值类型为float, 而两个参数类型为Point类型。

常见误区

有的小伙伴会误将这个类型提示跟类型检查混为一谈,需要注意的是:虽然Python加入了type hints,但是Python依然是一门动态语言,从根本上跟C++, JAVA这样的强类型静态语言不同,这也注定了Python不可能在运行时做类型检查。理论上讲,Python也能够做运行时类型检查,但是这个代价也太高了,会讲本来就运行慢的毛病让人更加诟病不已。type hints的目的是为了在开发期间,而不是运行期间,提供给开发者检查类型的能力,这个类型的检查主要靠工具例如各种linter,各种IDE以及IDE的插件,例如VS Code, PyCharm等等都对type hints类型提示有良好的支持。

常见用法和例子(来自mypy文档)

变量的类型

  1. # This is how you declare the type of a variable
  2. age: int = 1
  3. # You don't need to initialize a variable to annotate it
  4. a: int # Ok (no value at runtime until assigned)
  5. # Doing so can be useful in conditional branches
  6. child: bool
  7. if age < 18:
  8. child = True
  9. else:
  10. child = False
'
运行

简单类型的变量,例如int, str, bool, float等等,类型提示的写法如上所示。

内建类型

  1. x: int = 1
  2. x: float = 1.0
  3. x: bool = True
  4. x: str = "test"
  5. x: bytes = b"test"
'
运行

对于内建类型,可以直接用类型的名字作为类型注解。注意这里当我们声明变量的同时为变量赋值的时候,类型系统能够根据初始赋值推导出变量类型。例如 x: int = 1这行代码可以简写为x = 1, 但是为了保持代码的可读性和一致性,我还是建议显式注明变量的类型。

集合类型

集合类型其实也属于Python语言的内建类型,但是因为有其特殊性,这里单独说一说。对于Python 3.9及以上,我们使用list/set/dict/tuple等类型名字就可以了,注意这些类型名称都是小写字母开头的。

  1. # Python 3.9+
  2. x: list[int] = [1]
  3. x: set[int] = {6, 7}
  4. # 对于字典类型,我们需要指定键和值的类型
  5. x: dict[str, float] = {"field": 2.0}
  6. # 对于元组类型,如果元组是固定大小,我们指定所有元素的类型
  7. x: tuple[int, str, float] = (3, "yes", 7.5)
  8. # 对于可变大小的元组类型,我们使用单一元素类型,其他的使用省略符号
  9. x: tuple[int, ...] = (1, 2, 3)
'
运行

而如果你使用的是比较早的Python 3版本,那么你需要导入typing包来使用对应的几个类型注解。typing包中定义的类型都是大写字母开头的。

  1. # Python 3.8或者更低版本
  2. from typing import List, Set, Dict, Tuple
  3. x: List[int] = [1]
  4. x: Set[int] = {6, 7}
  5. x: Dict[str, float] = {"field": 2.0}
  6. x: Tuple[int, str, float] = (3, "yes", 7.5)
  7. x: Tuple[int, ...] = (1, 2, 3)
'
运行

另外有两个常见的类型是Union和Optional

Union和Optional

Union用在变量或返回值有多种可选类型的情况下;

  1. # Python 3.10+ 中使用操作符 “|”来表示有多个候选类型的情况
  2. x: list[int | str] = [3, 5, "test", "fun"]
  3. from typing import Union, Optional
  4. # 在Python 3.10之前的版本中使用Union来注解
  5. x: list[Union[int, str]] = [3, 5, "test", "fun"]
'
运行

而Optional,用于处理空值None的情况。

  1. # Optional[X] 等同于 X | None 或者 Union[X, None]
  2. x: Optional[str] = "something" if some_condition() else None
  3. if x is not None:
  4. print(())

类型别名(Alias)

  1. Url = str
  2. def retry(url: Url, retry_count: int) -> None: ...
'
运行

函数(Functions)

对于函数的定义中的类型提示,没有什么特别的地方。

  1. def stringify(num: int) -> str:
  2. return str(num)
  3. def plus(num1: int, num2: int) -> int:
  4. return num1 + num2
  5. # 注意参数缺省值的写法。另外如果函数没有返回值,需要注解为None
  6. def show(value: str, excitement: int = 10) -> None:
  7. print(value + "!" * excitement)
'
运行

需要瞄一眼的地方就是位置参数和关键字参数,它们的类型注解写法如下。它表示每个参数的类型都是str类型。

  1. def call(self, *args: str, **kwargs: str) -> str:
  2. request = make_request(*args, **kwargs)
  3. return self.do_api_query(request)
'
运行

下面看看Python中函数的典型用法,有趣一点的那种。

  1. x: Callable[[int, float], float] = f
  2. def register(callback: Callable[[str], int]) -> None: ...

回调函数使用场景特别多,需要从typing包中导入Callable来做注解。

  1. def gen(n: int) -> Iterator[int]:
  2. i = 0
  3. while i < n:
  4. yield i
  5. i += 1

大家都知道生成子其实就是函数。它的注解如上面代码所示,返回值是一个迭代器。注意这里也需要导入typing包。

自定义的类

  1. class BankAccount:
  2. def __init__(self, account_name: str, initial_balance: int = 0) -> None:
  3. # 实例变量的类型可以自动推导得出,所以不必要手动注解。当然你也可以那么做。
  4. # based on the types of the parameters.
  5. self.account_name = account_name
  6. = initial_balance
  7. # 实例方法中的self不需要进行类型注解
  8. def deposit(self, amount: int) -> None:
  9. += amount
  10. def withdraw(self, amount: int) -> None:
  11. -= amount
  12. # 自定义的类型可以用于注解其他的函数或者类型
  13. account: BankAccount = BankAccount("Alice", 400)
  14. def transfer(src: BankAccount, dst: BankAccount, amount: int) -> None:
  15. (amount)
  16. (amount)
'
运行

这样经过了注解的类,是不是读起来更清爽?再也不用猜来猜去猜不出类型了。

  1. class AuditedBankAccount(BankAccount):
  2. # 实例变量的类型注解
  3. audit_log: list[str]
  4. def __init__(self, account_name: str, initial_balance: int = 0) -> None:
  5. super().__init__(account_name, initial_balance)
  6. # 在构造函数中注解实例变量的类型
  7. self.audit_log: list[str] = []
  8. def deposit(self, amount: int) -> None:
  9. self.audit_log.append(f"Deposited {amount}")
  10. += amount
  11. def withdraw(self, amount: int) -> None:
  12. self.audit_log.append(f"Withdrew {amount}")
  13. -= amount
  14. audited = AuditedBankAccount("Bob", 300)
  15. transfer(audited, account, 100)

派生类的实例被transfer函数使用时,类型检查也同样会发生。

注意,类变量的注解比较特殊,需要用到ClassVar这个类型。

  1. # You can use the ClassVar annotation to declare a class variable
  2. class Car:
  3. seats: ClassVar[int] = 4
  4. passengers: ClassVar[list[str]]

如果你的自定义类比较灵活,例如使用了自定义的属性,那么你需要想明白这些属性可以是哪些类型。

  1. # 如果你定义的类中包含有动态属性,那么会好玩一些~~
  2. class A:
  3. # 这里期望属性的值为int类型。如果你不希望限定类型,可以使用"value: Any"
  4. def __setattr__(self, name: str, value: int) -> None: ...
  5. # 这里的返回值类型其实就是属性的类型,已经被限定为int了。
  6. def __getattr__(self, name: str) -> int: ...
  7. a = A()
  8. = 42 # Works
  9. = 'Ex-parrot' # 恭喜!类型检查会失败
'
运行

写到这里,其实还有很多没有涵盖。一些高级的用法(或者古怪一点的地方),我们下一期再讨论吧,记得关注我哦。

扩展阅读

对于Python类型系统感兴趣的小伙伴们,可以进一步查阅下列资料,也欢迎和我交流。

PEP 484 – Type Hints |

mypy - Optional Static Typing for Python

mypy 1.11.1 documentation