Type Hint in Python¶
Keywords: Python, typing, type hint, type hints, typehint, typehints, mypy
Avoid Cyclic Import for Type Hint¶
Challenge
在大型项目的代码中, 通常模块众多, 互有依赖. 所以避免 “循环导入” 是常识, 并且有很多设计模式可以避免这一点. 但一旦想要给所有的 API 涉及到的参数添加 Type Hint, 这一点就变得几乎无法做到.
Solution
typing.TYPE_CHECKING
是 Type Hint 核心模块 typing
中的一个魔法常量. 在正常 runtime, 这个常量的值永远是 False
. 所以你可以把需要用来做 Type Hint 的 import 放在 if TYPE_CHECKING:
的下面, 这样 IDE 和静态分析软件就可以读取这些信息并提供基于 Type Hint 的功能. 而 Runtime 则完全不受影响. 这得益于 Python 是动态语言的特性, 如果代码不被执行, 即使实际上有错也没有关系. 而静态分析软件则可以在运行检查之前提前 import typing
然后用 monkey patch 将其改为 typing.TYPE_CHECKING = True
即可.
这里有一个小问题, 如果用于 Type Hint 的 type 在被标注时没有被定义, 则需要用 'yourClassHere'
将其标注起来. 这样很不方便. 所以在 Python3.7+ 引入了 from __future__ import annotations
, 这使得 type hint 都会被标注为字符串保存在 __annotations__
系统变量中. 这使得所有的 type hint annotation 都不会在 runtime 中被 evaluate, 从而避免了 'yourClassHere'
这样的语法. 这也是很多开源库都选择只支持 3.7+. 如果你的库要支持 3.6, 那么请不要使用 from __future__ import annotations
这一功能.
# -*- coding: utf-8 -*-
# content of a.py
# this only works in Python3.7+
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from avoid_cyclic_import_b import B
class A:
def __init__(self, b: 'B'):
self.b = b
def method(self, b: B):
return b
# -*- coding: utf-8 -*-
# content of b.py
# this only works in Python3.7+
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from avoid_cyclic_import_a import A
class B:
def __init__(self, a: 'A'):
self.a = a
def method(self, a: A):
return a
Ref:
Distinguish Class Attribute and Instance Attribute Type Hint¶
# -*- coding: utf-8 -*-
"""
**问题**
在下面的例子中我们用标准库里的 dataclass 定义了一个 class.
作为 class attribute 的 Person.name, 实际上是一个 dataclasses.Field 对象
而作为 instance attribute 的 person.name, 是一个 str 对象
而 IDE 只能根据你在 class 定义中 type hint 推断应该是一个什么类型.
这个问题在几乎所有的 ORM 框架中都存在. 通常我们会手动指定 type hint 的类型为
instance attribute 的类型. 因为毕竟我们用 instance 比较多. 但是还是有很多时候我们需要
将 class / instance attribute 做区分用来做不同的事情. 那么我们如何让 IDE 知道
他们其实是有不同的 type hint 呢?
**解决方案**
1. 在你的 class 定义中, 使用 instance attribute 的类型
2. 在 class 定义之后, 使用 Class.attribute: ${type} 的语法指定 class attribute 的类型
"""
import dataclasses
@dataclasses.dataclass
class Person:
name: str = dataclasses.field()
Person.name: dataclasses.Field
person = Person(name="alice")
Person.name.
person.name.
def main():
Person.name.
person.name.
One Method Rtype Depends On Other Method Rtype¶
# -*- coding: utf-8 -*-
"""
我们在设计框架类型的库的时候, 有时候会为一个接口提供一个底层函数, 让用户自己实现自定义的细节,
然后在顶层函数对其进行封装, 增加一些例如, 验证, 打印日志等通用逻辑. 而顶层函数的返回值其实
是和底层函数的返回值是一摸一样的. 但是, 这里实现底层函数的时候的返回类型可能是任何自定义的
类型. 我们如何让顶层函数自动根据底层函数的返回类型, 发现自己实际的返回类型呢?
这个话题实际上就是编程中的泛型.
这里我们来看两个例子. 我们有一个反序列化的类. 他有两个函数, 一个是从字符串中解析数据,
一个是从字节中解析数据. 但是解析出来的数据是什么由用户自己定义. 用户需要继承这个类, 然后
实现对应的方法.
"""
from abc import ABC, abstractmethod
from typing import Generic, TypeVar
# 首先我们来定义两个类型变量, 分别供 parse_string
T1 = TypeVar("T1")
T2 = TypeVar("T2")
# 这里的 Generic 则是一切的关键, 它是一个 "泛型" 构造器, 定义了一个类里面与泛型有关的变量
# 然后让这个类继承这个 ``Generic[T1, T2, ...]`` 构造器构造出来的父类
# 从而让这个类的子类能够在继承的时候, 通过修改 T1, T2 的值改变里面 TypeHint 的推断
class A(
ABC, # 这个是说 A 是一个抽象类.
# 说明了 A 的子类都会涉及到 2 个类型, 分别用于
# parse_string 和 parse_bytes 两个函数的返回类型
Generic[T1, T2],
):
@abstractmethod
def _parse_string(self, value: str) -> T1:
raise NotImplementedError
def parse_string(self, value: str) -> T1: # 目前他们都是变量, 具体是什么类型不知道
try:
return self._parse_string(value)
except Exception as e:
print(f"Failed to parse from string, error: {e}")
@abstractmethod
def _parse_bytes(self, value: bytes) -> T2:
raise NotImplementedError
def parse_bytes(self, value: bytes) -> T2:
try:
return self._parse_bytes(value)
except Exception as e:
print(f"Failed to parse from bytes, error: {e}")
class Data1:
def is_data_1(self):
return True
class Data2:
def is_data_2(self):
return True
# 这里用我们用 Data1, Data2 替换了 T1, T2, 也就是告诉了 parse_string 和 parse_bytes
# 将要返回什么类型了.
class B(A[Data1, Data2]):
def _parse_string(self, value: str):
return Data1()
def _parse_bytes(self, value: str):
return Data2()
b = B()
b.parse_string("s").is_data_1() # 你输入 b.parse_string("s"). 就能看到 is_data_1 的提示了
b.parse_bytes(b"b").is_data_2()
# -*- coding: utf-8 -*-
import typing as T
class DataA:
def is_data_a(self):
pass
class DataB:
def is_data_b(self):
pass
A = T.TypeVar("A")
B = T.TypeVar("B")
# ------------------------------------------------------------------------------
# 这样做是可以的, 一次性定义两个 TypeVar 的具体类型
# ------------------------------------------------------------------------------
# class ModelA(
# T.Generic[A, B]
# ):
# def method1_lower(self) -> A:
# raise NotImplementedError
#
# def method1(self):
# return self.method1_lower()
#
# def method2_lower(self) -> B:
# raise NotImplementedError
#
# def method2(self):
# return self.method2_lower()
#
#
# class ModelB(
# ModelA[DataA, DataB]
# ):
# def method1_lower(self):
# return DataA()
#
# def method2_lower(self):
# return DataB()
#
#
# ModelB().method1().is_data_a()
# ModelB().method2().is_data_b()
# ------------------------------------------------------------------------------
# 如果你要用到多重继承, 一次只定义一个 TypeVar, 那么你要把两个方法分开, 分两次进行
# ------------------------------------------------------------------------------
# class BaseModelA(T.Generic[A]):
# def method1_lower(self) -> A:
# raise NotImplementedError
#
# def method1(self):
# return self.method1_lower()
#
#
# class BaseModelB(T.Generic[B]):
# def method2_lower(self) -> B:
# raise NotImplementedError
#
# def method2(self):
# return self.method2_lower()
#
#
# class ModelA(BaseModelA[DataA]):
# def method1_lower(self):
# return DataA()
#
#
# class ModelB(ModelA, BaseModelB[DataB]):
# def method2_lower(self):
# return DataB()
#
#
# ModelB().method1_lower().is_data_a()
# ModelB().method2_lower().is_data_b()
# ------------------------------------------------------------------------------
# 如果两个错综复杂要合并在一起
# ------------------------------------------------------------------------------
class ModelA(T.Generic[A]):
def method1_lower(self) -> A:
raise NotImplementedError
def method1(self):
return self.method1_lower()
class ModelB(T.Generic[B]):
def method2_lower(self) -> B:
raise NotImplementedError
def method2(self):
return self.method2_lower()
class ModelC(
ModelA,
ModelB,
T.Generic[A, B],
):
def method1_lower(self):
return DataA()
def method2_lower(self):
return DataB()
def method3_lower(self) -> T.Union[A, B]:
raise NotImplementedError
def method3(self):
return self.method3_lower()
class ModelD(
ModelC[DataA, DataB],
):
def method3_lower(self):
return DataA()
ModelD().method1().is_data_a()
ModelD().method2().is_data_b()
ModelD().method3().is_data_a()