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()