Python中如何解决Type Hints的问题?分享经验与解决方案

说实话 Python 循环 import 一直是个不是问题的问题,我们可以通过提取出两个模块共同的部分来规避这个问题。我也感觉代码里最好不要出现循环,如果出现,一定是设计的问题。

不过 PEP484 ( Type Hints )出来以后,循环 import 的问题在我代码里出现比较多了,因为需要注明变量(参数)类型,所以不得不将一些不需要 import 的类导入。

所以这里有一个相关讨论: https://github.com/python/typing/issues/105

Python 之父 Guido 参与了讨论并给出了一个临时通用的解决方案: https://hg.python.org/peps/rev/06fbe54fcfe1 ,就是用import foo来代替from foo import bar

PEP-0563 里给出了另一个解决方案:使用typing.TYPE_CHECKING。这个常量在编辑器检查变量类型的时候为 True,在代码实际运行的时候为 False。于是,我们可以用如下代码来导入声明类型时用到的类:

if typing.TYPE_CHECKING:
    from foo import bar

这个常量在 3.5.2 后加入。

不过这个方法又引入了新的问题:在代码运行时,实际上是没有导入 bar 的,那么作为 Type Hints 使用会出错:

def f(foo: bar): pass

我们必须把 bar 用引号包裹:

def f(foo: 'bar'): pass

image

PEP-0563 里也给出了相关的解决方案。在 Python 4 以后,函数的 annotations 将不再运行时被执行,所以也就不会报错了

在 Python3.7 下,我们可以使用from __future__ import annotations来体验这个 4 里的特性。

image

这几个方式结合在一起,就能完美解决我们遇到的问题了。

总的来说,这些新特性让我的代码更加 humanize,我也十分期待 3.7 的正式发布,我觉得 3.7 里异步 context 的部分还挺好用的~


Python中如何解决Type Hints的问题?分享经验与解决方案

24 回复

很快就 4 了吗 版本号飙起来


Type Hints(类型提示)在Python里是个好东西,能让代码更清晰,配合IDE和mypy这类工具能提前发现不少低级错误。但用起来确实会遇到一些坑,我分享一下常见的几个问题和我的处理方式。

1. 循环引用(Circular Imports) 这是最烦人的问题之一。当两个类互相引用时,比如User类里有个Post列表,Post类里有个User作者,直接import会报错。

解决方案:使用字符串字面量或者from __future__ import annotations

# 方法1:使用字符串
from __future__ import annotations  # Python 3.7+ 可以更优雅

class User:
    def __init__(self, name: str):
        self.name = name
        self.posts: list[Post] = []  # 直接使用类名,或者用字符串 "Post"

class Post:
    def __init__(self, title: str, author: User):  # 这里User已经可用了
        self.title = title
        self.author = author

2. 前向引用(Forward References) 和循环引用类似,但更通用。当你需要引用尚未定义的类时。

解决方案:同上,用字符串。from __future__ import annotations会让所有注解在运行时都自动存为字符串,基本一劳永逸。

3. 泛型(Generics)和容器类型 标注listdict里具体放什么类型。

解决方案:用typing模块,Python 3.9+直接用内置类型。

from typing import Dict, List, Optional  # Python 3.8及以前
# Python 3.9+ 可以这样写:
def process_items(items: list[str]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for item in items:
        counts[item] = counts.get(item, 0) + 1
    return counts

4. 标注“可能为None”的值 解决方案:用Optional[X],或者Python 3.10+的 | 语法。

from typing import Optional

def find_user(user_id: int) -> Optional["User"]:
    # ... 可能返回None
    pass

# Python 3.10+
def find_user(user_id: int) -> User | None:
    pass

5. 第三方库或动态类型没有存根(stubs) 解决方案:自己写.pyi存根文件,或者用typing.castAny来临时绕过。

from typing import Any, cast
import some_untyped_module

result = cast(int, some_untyped_module.funky_function())  # 告诉类型检查器,我相信它是int
data: Any = get_dynamic_data()  # 实在不行就Any,但尽量少用

6. 类型检查太严格,需要忽略某行 解决方案:用 # type: ignore 注释。

x = some_untyped_call()  # type: ignore

我的工作流建议:

  1. 统一代码风格:团队约定好用from __future__ import annotations
  2. 渐进式添加:老项目不用一次性全加上,新代码和重点模块先加。
  3. 善用工具:在CI里集成mypypyright,提交前跑一下。
  4. 理解本质:类型提示主要是给人和工具看的,运行时影响很小(除了get_type_hints)。

总结:核心就是处理好循环引用,善用字符串注解和typing模块。

感谢感谢,解决了我这里互相 import 的问题

其实不用 from … import … 就问题不大

感谢分享,
话说看到 Python4 还是虎躯一震

不用 type hint 完美解决

厉害了,最近刚转入 3 的使用和学习,马上就 4 出来了



4 应该还早吧,这里只是用到了一个未来的特性

这几个问题都是在 www.mypy.com 里面直接告诉你解决方案的。

你们都没有去看官方文档吗?


老师对不起,我们没有好好学习,今天学习了一个新的官方文档 www.mypy.com

讲真的,如果我的代码里要写很多这种奇奇怪怪对实际 IDE 体验(比如这个 bar 到底是哪个 bar,目前 PyCharm 自动完成就搞不定,甚至搞不定 metaclass 或 Proxy Wrapper )、代码可读性都没有提升的东西,还不如选择换一门静态类型的语言。

其实我更喜欢 Guido 提到的另一种暂未实现的解决方案

# type: import my_module
# type: from my_module import A

这种好处是可以把为了 hint import 的东西放在 hint 附近,并且和一般的 import 区分,可读性更高。

这样注释之后,代码中如何解决不让解释器执行没引入的 type 呢?
也是如帖子里的 from future import annotations 那样不执行吗?

嘛刚才 Guido 说不会走这条路了……所以也没什么必要讨论这个了。

类型提示导致循环引用这种情况,不应该上 lazy settings 么?

没懂,我看他们没提到这个,可以给点详细信息么

就是所有的常量、配置项都写在一个文件里,然后在任意位置都能 import 到这个文件里的内容,类似于 django 的 settings.py 和 flask 的 g

并不是常量,比如 A 类里某个方法接受或返回一个 B 类对象,B 类里一个方法接受或返回 A 类对象。你可能还没遇到过这种情况,如果仅是配置的话就简单多了。
既然 python 官方需要有专门的方案来解决这个问题,说明这个问题是一个广泛问题,并不能通过你说的这种方式来解决。你说的这种方式仅能处理一些简单的逻辑。

数据类型的定义不也是常量么?难道类定义在运行时还会动态改?

明白你的意思了。 这是个鸡生蛋、蛋生鸡的问题啊~

这种情况,我都是放弃 annotations, 在函数内部用 assert 来做类型判断。。。。。反正也只是给 ide 看的

恩,之前看到的时候就收藏了

回到顶部