一般来说,包设计中做抽象时,个人习惯把基类抽出来单独放一个文件,我看很多其他人也喜欢这么做,比如做成这样
--鸟类大全\
|___ 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
中的其他函数、方法的输入,是存在于其他文件中的子类,这种情况下没办法直接从子类导入这种类,因为会变成循环引用。
有办法解决吗?
1
hareandlion 2021-01-27 08:05:44 +08:00 via Android
base.py 中没有抽象所有的接口实现?那写个声明了所有方法的 mixin 供所有子类继承是不是好点?
|
2
abersheeran 2021-01-27 08:47:53 +08:00 via Android
是的,这个问题我在写 Index.py 的时候也遇到了。我的解决方案是用 PEP563 。
|
3
LeeReamond OP @hareandlion 只是举了一个简单例子,实际还有比如子类之间互相引用等等情况,实际业务中完全解耦几乎不可能,
|
4
LeeReamond OP @abersheeran 我看了一下 pep563 没抓住重点他想表达什么,大佬能概述一下有什么作用吗
|
5
Kilerd 2021-01-27 08:59:05 +08:00 1
有。
from typing import TYPE_CHECKING if TYPE_CHECKING: from .base import BaseBird https://docs.python.org/3/library/typing.html#typing.TYPE_CHECKING |
6
LeeReamond OP @abersheeran 手机上看的,不太方便,看起来似乎像是引入一个被动变量跳过编译检查?然后在 runtime 中类型提示已经被去除了,所以不会报错?
|
7
LeeReamond OP |
8
abersheeran 2021-01-27 09:13:41 +08:00
@LeeReamond 你这么写当然会报错。目前的 CPython,type hint 可是会在运行时计算的。这和宣称的不符。才有了我上面所说的 PEP563 。3.7 以上版本可用 future 开启,3.10 默认开启。
|
9
676529483 2021-01-27 09:33:29 +08:00
原来-> BaseBird 改成字符串->"BaseBird"
|
10
no1xsyzy 2021-01-27 09:41:46 +08:00
也可以转移到 .pyi 里面去,1. 对运行时隐形; 2. 不担忧循环引用。
不好的地方是 pydantic 这种依赖运行时 hint 的就不行 |
11
popil1987 2021-01-27 09:52:38 +08:00
|
12
so1n 2021-01-27 10:24:31 +08:00
@LeeReamond 第 10 行 def func(self , input:B.Bird):改为 def func(self , input:"B.Bird"): 即可
|
13
LeeReamond OP @popil1987 只是这么抽象在人类逻辑上比较清楚,可能对程序来说不清楚,我不太懂设计的问题,mixin 是什么意思,百度搜了一下没搜到
|
14
LeeReamond OP @so1n
@no1xsyzy @676529483 @abersheeran 试了一下感觉目前这些方案都不行。我对静态类型的期望有两个,其一是在开发的时候可以使用 IDE 直接寻址到对象的目标位置,如果用双引号的写法的话无法实现,其二是我希望能够在 runtime 使用 inspect.trace 追踪到对象,这样可以在自动测试中依据这个特性自动检查所有函数和方法的输入输出是否符合要求,让程序更可靠,大概类似于编译中的静态类型检查吧。这个需求用双引号的方式也无法实现,用`from __future__ import annotations`也无法实现。 目前来看只能获取到对象的字符串名称,而不能直接获取对象,而进行 isinstance 判断必须要输入对象,所以似乎无解。还有一种方式是通过某种方式设计重载,并且在重载后使用 eval()将字符串转换为类,但一来实现成本较高,二来 eval 总归让人不爽。 |
15
so1n 2021-01-27 18:22:58 +08:00
@LeeReamond 那就需要写个抽象类了..
|
16
LeeReamond OP @so1n 没听懂,详细说一下?
|
17
no1xsyzy 2021-01-27 21:42:51 +08:00 1
@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 模块,把最基础的要求提取出来可以在类型里留洞。 |
18
LeeReamond OP @no1xsyzy 感谢回复,有时间研究一下。关于 mixin 和抽象类,我不会 ponylang 和 rust,所以无法理解你提供的 mixin 的例子。抽象类这个功能对于代码复用不是很灾难么,因为本身继承就是希望可以复用基类的代码,如果每个子类必须重新实现一遍的话感觉很累赘啊
|
19
LeeReamond OP @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
|
20
LeeReamond OP @no1xsyzy 感谢,已经解决
|
21
maocat 2021-01-29 09:25:07 +08:00
@LeeReamond 你好,你是怎么解决的呢
|
22
LeeReamond OP @maocat
笼统地说实际代码层没有修改。解决的是指确实可以通过 runtime 的方式获取实例,也就是指虽然在同一个包内无法循环导入,但在用户代码执行的时候实际上所有内容都已经导入到用户空间,可以通过字符串转换到对应的类实例,也就是说如果想要的话是可以在 runtime 强行获取实例并进行某种校验的。但是研究了一下觉得并没有那么强的需求,所以最后也没改什么。楼上提到 PEP563 的导入方式,我测试下 mypy 是支持解释的,不愿意用 mypy 的话也有一些自己实现的方法,意思是你用 pep563 的方式进行导入,用自动工具是可以与普通导入等同地进行校验测试的,功能上来说已经达到了,虽然易用性上未必好 |