Python 使用静态类型标注时的循环导入问题

2021-01-27 07:00:59 +08:00
 LeeReamond

一般来说,包设计中做抽象时,个人习惯把基类抽出来单独放一个文件,我看很多其他人也喜欢这么做,比如做成这样

--鸟类大全\
    |___ base.py
    |___ 鸟人 1.py
    |___ 鸟人 2.py
    |___ 工具箱.py 

大概这种感觉。

最近在做 type hints 时遇到一个循环导入的问题,即比如我有一个 class BaseBird:存在于base.py中,而class BirdmanOne(BaseBird)存在于鸟人 1.py中,显然鸟人 1.py 需要from .base import BaseBird

正常情况下没问题,在老版本里也一直是这么处理的。但是现在如果想引入类型提示特性的话,如果BaseBird中的某个方法,或者base.py中的其他函数、方法的输入,是存在于其他文件中的子类,这种情况下没办法直接从子类导入这种类,因为会变成循环引用。

有办法解决吗?

3591 次点击
所在节点    Python
22 条回复
hareandlion
2021-01-27 08:05:44 +08:00
base.py 中没有抽象所有的接口实现?那写个声明了所有方法的 mixin 供所有子类继承是不是好点?
abersheeran
2021-01-27 08:47:53 +08:00
是的,这个问题我在写 Index.py 的时候也遇到了。我的解决方案是用 PEP563 。
LeeReamond
2021-01-27 08:58:06 +08:00
@hareandlion 只是举了一个简单例子,实际还有比如子类之间互相引用等等情况,实际业务中完全解耦几乎不可能,
LeeReamond
2021-01-27 08:58:42 +08:00
@abersheeran 我看了一下 pep563 没抓住重点他想表达什么,大佬能概述一下有什么作用吗
Kilerd
2021-01-27 08:59:05 +08:00
有。

from typing import TYPE_CHECKING


if TYPE_CHECKING:
from .base import BaseBird



https://docs.python.org/3/library/typing.html#typing.TYPE_CHECKING
LeeReamond
2021-01-27 09:01:08 +08:00
@abersheeran 手机上看的,不太方便,看起来似乎像是引入一个被动变量跳过编译检查?然后在 runtime 中类型提示已经被去除了,所以不会报错?
LeeReamond
2021-01-27 09:08:18 +08:00
abersheeran
2021-01-27 09:13:41 +08:00
@LeeReamond 你这么写当然会报错。目前的 CPython,type hint 可是会在运行时计算的。这和宣称的不符。才有了我上面所说的 PEP563 。3.7 以上版本可用 future 开启,3.10 默认开启。
676529483
2021-01-27 09:33:29 +08:00
原来-> BaseBird 改成字符串->"BaseBird"
no1xsyzy
2021-01-27 09:41:46 +08:00
也可以转移到 .pyi 里面去,1. 对运行时隐形; 2. 不担忧循环引用。
不好的地方是 pydantic 这种依赖运行时 hint 的就不行
popil1987
2021-01-27 09:52:38 +08:00
你得考虑下设计问题了,先用 typing any 通过
如果非得循环引用就得改变引入顺序,比如
a.py
from .b import B
class A:
def test(self, b:B)->None:
pass
b.py
class B:
pass
from .a import A
class C:
def test(self, a:A):
pass
要是 A 需要 B,B 需要 A 那就没办法了,改设计吧,可以多用 mixin,少继承。
so1n
2021-01-27 10:24:31 +08:00
@LeeReamond 第 10 行 def func(self , input:B.Bird):改为 def func(self , input:"B.Bird"): 即可
LeeReamond
2021-01-27 17:49:21 +08:00
@popil1987 只是这么抽象在人类逻辑上比较清楚,可能对程序来说不清楚,我不太懂设计的问题,mixin 是什么意思,百度搜了一下没搜到
LeeReamond
2021-01-27 18:05:39 +08:00
@so1n
@no1xsyzy
@676529483
@abersheeran

试了一下感觉目前这些方案都不行。我对静态类型的期望有两个,其一是在开发的时候可以使用 IDE 直接寻址到对象的目标位置,如果用双引号的写法的话无法实现,其二是我希望能够在 runtime 使用 inspect.trace 追踪到对象,这样可以在自动测试中依据这个特性自动检查所有函数和方法的输入输出是否符合要求,让程序更可靠,大概类似于编译中的静态类型检查吧。这个需求用双引号的方式也无法实现,用`from __future__ import annotations`也无法实现。

目前来看只能获取到对象的字符串名称,而不能直接获取对象,而进行 isinstance 判断必须要输入对象,所以似乎无解。还有一种方式是通过某种方式设计重载,并且在重载后使用 eval()将字符串转换为类,但一来实现成本较高,二来 eval 总归让人不爽。
so1n
2021-01-27 18:22:58 +08:00
@LeeReamond 那就需要写个抽象类了..
LeeReamond
2021-01-27 18:51:06 +08:00
@so1n 没听懂,详细说一下?
no1xsyzy
2021-01-27 21:42:51 +08:00
@LeeReamond
方法 1,更好的 eval ( python>=3.7 ):
pydantic (<https://github.com/samuelcolvin/pydantic>)用了一个类似 eval 的东西:
/pydantic/main.py#L757-L765
这里通过 sys.modules[cls.__module__].__dict__ 拿到 globals,然后
/pydantic/typing.py#L51-L68
应对不同版本调用 ForwardRef 的保护方法进行解析(但仍然是个 eval,你完全可以写出 `a: 'type(1+2)' = 5`,而且类型检查能过)。

方法 2,typing.Protocol ( python>=3.8 )
采用 typing.Protocol,通过结构子类型替换名义子类型。运行时可以采用 .runtime_checkable
https://docs.python.org/zh-cn/3/library/typing.html#nominal-vs-structural-subtyping (这是我翻译的,就放中文文档了)
( Go 的 interface 就是结构子类型,至于 Ponylang 则同时实现了结构子类型( interface )和名义子类型( trait ))

方法 3,控制反转(无要求)
需要动代码结构。
先解释下 mixin 和 abc:
mixin 是一种设计模式,可以参考 Ponylang/rust 的 trait,把一些非关键依赖的方法从基类里剥离出去,最后依赖结构会形成一个 Diamond 。
抽象类是指标准库 abc 模块,把最基础的要求提取出来可以在类型里留洞。
LeeReamond
2021-01-27 22:09:36 +08:00
@no1xsyzy 感谢回复,有时间研究一下。关于 mixin 和抽象类,我不会 ponylang 和 rust,所以无法理解你提供的 mixin 的例子。抽象类这个功能对于代码复用不是很灾难么,因为本身继承就是希望可以复用基类的代码,如果每个子类必须重新实现一遍的话感觉很累赘啊
LeeReamond
2021-01-28 01:42:11 +08:00
@no1xsyzy 我测试了一下方法 1,我没太看懂 https://github.com/samuelcolvin/pydantic/blob/master/pydantic/typing.py#L51-L68 这一段怎么用 eval 实现的字符串到实例的转换,不是很理解为什么 from typing import _eval_type 之后,_eval_type 就变成了 ForwardRef 的方法,我自己这么调用没有成功,所以不是很理解 sys.modules[cls.__module__].__dict__拿到的 globals 怎么转化,我自己从中没法提取想要的信息,虽然 runtime 当中这句执行已经在导入所有库之后,但仍然只能拿到相对该文件的 globals
LeeReamond
2021-01-28 07:48:03 +08:00
@no1xsyzy 感谢,已经解决

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/748739

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX