【问题标题】:Python type hinting without cyclic imports没有循环导入的 Python 类型提示
【发布时间】:2017-02-06 00:40:31
【问题描述】:

我正试图将我的大班一分为二;好吧,基本上进入“主”类和一个带有附加功能的mixin,如下所示:

main.py文件:

import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...

mymixin.py文件:

class MyMixin(object):
    def func2(self: Main, xxx):  # <--- note the type hint
        ...

现在,虽然这工作得很好,但MyMixin.func2 中的类型提示当然不能工作。我无法导入main.py,因为我会得到一个循环导入并且没有提示,我的编辑器(PyCharm)无法分辨self 是什么。

我正在使用 Python 3.4,但如果那里有可用的解决方案,我愿意迁移到 3.5。

有什么方法可以将我的类拆分为两个文件并保留所有“连接”,以便我的 IDE 仍然为我提供自动完成功能以及它所带来的所有其他好处吗?

【问题讨论】:

  • 我认为您通常不需要注释 self 的类型,因为它总是会是当前类的子类(并且任何类型检查系统都应该能够计算出)自己出去)。 func2 是否试图调用 func1,而 MyMixin 中没有定义?也许应该是(作为abstractmethod,也许)?
  • 还请注意,通常更具体的类(例如您的 mixin)应该放在类定义中基类的左侧,即class Main(MyMixin, SomeBaseClass),以便来自更具体类的方法可以覆盖来自基类
  • 我不确定这些 cmets 有什么用处,因为它们与所提出的问题无关。 velis 没有要求进行代码审查。

标签: python python-3.5 python-3.4 type-hinting python-typing


【解决方案1】:

原来我最初的尝试也非常接近解决方案。这是我目前正在使用的:

# main.py
import mymixin.py

class Main(object, MyMixin):
    def func1(self, xxx):
        ...
# mymixin.py
if False:
    from main import Main

class MyMixin(object):
    def func2(self: 'Main', xxx):  # <--- note the type hint
        ...

注意 if False 语句中的导入永远不会被导入(但 IDE 无论如何都知道)并使用 Main 类作为字符串,因为它在运行时是未知的。

【讨论】:

  • 我希望这会导致有关死代码的警告。
  • @Phil:是的,当时我使用的是 Python 3.4。现在有打字了。TYPE_CHECKING
  • 看起来很蠢,但可以使用 PyCharm。有我的赞成票! :)
【解决方案2】:

从 Python 3.5 开始,将类分解为单独的文件很容易。

实际上可以在class ClassName: 块的inside 中使用import 语句,以便将方法导入类中。例如,

class_def.py:

class C:
    from _methods1 import a
    from _methods2 import b

    def x(self):
        return self.a() + " " + self.b()

在我的例子中,

  • C.a() 将是一个返回字符串 hello 的方法
  • C.b() 将是一个返回 hello goodbye 的方法
  • C.x() 将因此返回 hello hello goodbye

要实现ab,请执行以下操作:

_methods1.py:

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from class_def import C

def a(self: C):
    return "hello"

解释:当类型检查器读取代码时,TYPE_CHECKINGTrue。由于类型检查器不需要执行代码,循环导入在 if TYPE_CHECKING: 块中发生时是可以的。 __future__ 导入启用 postponed annotations。这是可选的;没有它,您必须引用类型注释(即def a(self: "C"):)。

我们同样定义_methods2.py

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from class_def import C

def b(self: C):
    return self.a() + " goodbye"

在 VS Code 中,悬停时我可以看到从 self.a() 检测到的类型:

一切都按预期运行:

>>> from class_def import C
>>> c = C()
>>> c.x()
'hello hello goodbye'

关于旧 Python 版本的说明

对于小于等于 3.4 的 Python 版本,TYPE_CHECKING 未定义,因此此解决方案不起作用。

对于小于等于 3.6 的 Python 版本,未定义延迟注释。作为一种解决方法,请省略 from __future__ import annotations 并引用上面提到的类型声明。

【讨论】:

    【解决方案3】:

    我建议按照其他人的建议重构您的代码。

    我可以告诉你我最近遇到的一个循环错误:

    之前:

    # person.py
    from spell import Heal, Lightning
    
    class Person:
        def __init__(self):
            self.life = 100
    
    class Jedi(Person):
        def heal(self, other: Person):
            Heal(self, other)
    
    class Sith(Person):
        def lightning(self, other: Person):
            Lightning(self, other)
    
    # spell.py
    from person import Person, Jedi, Sith
    
    class Spell:
        def __init__(self, caster: Person, target: Person):
            self.caster: Person = caster
            self.target: Person = target
    
    class Heal(Spell):
        def __init__(self, caster: Jedi, target: Person):
            super().__init__(caster, target)
            target.life += 10
    
    class Lightning(Spell):
        def __init__(self, caster: Sith, target: Person):
            super().__init__(caster, target)
            target.life -= 10
    
    # main.py
    from person import Jedi, Sith
    

    一步一步:

    # main starts to import person
    from person import Jedi, Sith
    
    # main did not reach end of person but ...
    # person starts to import spell
    from spell import Heal, Lightning
    
    # Remember: main is still importing person
    # spell starts to import person
    from person import Person, Jedi, Sith
    

    控制台:

    ImportError: cannot import name 'Person' from partially initialized module
    'person' (most likely due to a circular import)
    

    一个脚本/模块只能由一个且只有一个脚本导入。

    之后:

    # person.py
    class Person:
        def __init__(self):
            self.life = 100
    
    # spell.py
    from person import Person
    
    class Spell:
        def __init__(self, caster: Person, target: Person):
            self.caster: Person = caster
            self.target: Person = target
    
    # jedi.py
    from person import Person
    from spell import Spell
    
    class Jedi(Person):
        def heal(self, other: Person):
            Heal(self, other)
    
    class Heal(Spell):
        def __init__(self, caster: Jedi, target: Person):
            super().__init__(caster, target)
            target.life += 10
    
    # sith.py
    from person import Person
    from spell import Spell
    
    class Sith(Person):
        def lightning(self, other: Person):
            Lightning(self, other)
    
    class Lightning(Spell):
        def __init__(self, caster: Sith, target: Person):
            super().__init__(caster, target)
            target.life -= 10
    
    # main.py
    from jedi import Jedi
    from sith import Sith
    
    jedi = Jedi()
    print(jedi.life)
    Sith().lightning(jedi)
    print(jedi.life)
    

    执行行的顺序:

    from jedi import Jedi  # start read of jedi.py
    from person import Person  # start AND finish read of person.py
    from spell import Spell  # start read of spell.py
    from person import Person  # start AND finish read of person.py
    # finish read of spell.py
    
    # idem for sith.py
    

    控制台:

    100
    90
    

    文件组合是关键 希望它会有所帮助:D

    【讨论】:

    • 我只想指出,问题不在于将多个类拆分为多个文件。这是关于将单个类拆分为多个文件。也许我可以将这个类重构为多个类,但在这种情况下我不想这样做。一切实际上都属于那里。但是很难维持 >1000 行源,所以我按照一些任意标准进行拆分。
    【解决方案4】:

    对于那些在只为类型检查导入类时遇到循环导入问题的人:您可能希望使用Forward Reference(PEP 484 - 类型提示):

    当类型提示包含尚未定义的名称时,该定义可以表示为字符串文字,稍后再解析。

    所以而不是:

    class Tree:
        def __init__(self, left: Tree, right: Tree):
            self.left = left
            self.right = right
    

    你会的:

    class Tree:
        def __init__(self, left: 'Tree', right: 'Tree'):
            self.left = left
            self.right = right
    

    【讨论】:

    • 可能是 PyCharm。你用的是最新版本吗?你试过File -&gt; Invalidate Caches吗?
    • 谢谢。对不起,我已经删除了我的评论。它曾提到这可行,但 PyCharm 抱怨。我使用Velis 建议的 if False hack 解决了问题。使缓存无效并没有解决它。这可能是 PyCharm 的问题。
    • @JacobLee 除了if False:,您还可以使用from typing import TYPE_CHECKINGif TYPE_CHECKING:
    • 如果类型驻留在另一个模块中,这不起作用(至少pycharm不理解它)。如果字符串可以是完全限定的路径,那就太好了。
    【解决方案5】:

    恐怕没有一种非常优雅的方式来处理导入周期。您的选择是重新设计代码以消除循环依赖,或者如果不可行,请执行以下操作:

    # some_file.py
    
    from typing import TYPE_CHECKING
    if TYPE_CHECKING:
        from main import Main
    
    class MyObject(object):
        def func2(self, some_param: 'Main'):
            ...
    

    TYPE_CHECKING 常量在运行时始终为 False,因此不会评估导入,但 mypy(和其他类型检查工具)会评估该块的内容。

    我们还需要将Main 类型注释变成一个字符串,有效地向前声明它,因为Main 符号在运行时不可用。

    如果您使用的是 Python 3.7+,我们至少可以通过利用 PEP 563 跳过提供显式字符串注释:

    # some_file.py
    
    from __future__ import annotations
    from typing import TYPE_CHECKING
    if TYPE_CHECKING:
        from main import Main
    
    class MyObject(object):
        # Hooray, cleaner annotations!
        def func2(self, some_param: Main):
            ...
    

    from __future__ import annotations 导入将使 all 类型提示成为字符串并跳过评估它们。这有助于使我们的代码更符合人体工程学。

    综上所述,在 mypy 中使用 mixins 可能需要比目前更多的结构。 Mypy recommends an approach 这基本上就是deceze 所描述的——创建一个你的MainMyMixin 类都继承的ABC。如果你最终需要做类似的事情来让 Pycharm 的检查器满意,我不会感到惊讶。

    【讨论】:

    • 谢谢。我当前的 python 3.4 没有 typing,但 PyCharm 对 if False: 也很满意。
    • 唯一的问题是它不能将 MyObject 识别为 Django models.Model,因此会抱怨在 __init__ 之外定义的实例属性
    • 这里是 typing. TYPE_CHECKING 的相应 pep:python.org/dev/peps/pep-0484/#runtime-or-type-checking
    【解决方案6】:

    我认为完美的方法应该是在一个文件中导入所有类和依赖项(如__init__.py),然后在所有其他文件中导入from __init__ import *

    在这种情况下你是

    1. 避免多次引用这些文件和类,并且
    2. 也只需要在每个其他文件中添加一行,并且
    3. 第三个是 pycharm,它知道您可能使用的所有类。

    【讨论】:

    • 这意味着你正在加载每一个地方,如果你有一个非常重的库,这意味着每次导入你都需要加载整个库。 + 参考会运行得非常慢。
    • > 这意味着您正在到处加载所有内容。 >>>> 如果您有许多“init.py”或其他文件,并且避免使用import *,则绝对不会,您仍然可以利用这种简单的方法
    【解决方案7】:

    更大的问题是你的类型一开始就不健全。 MyMixin 做了一个硬编码假设,即它将被混合到 Main 中,而它可以混合到任意数量的其他类中,在这种情况下它可能会中断。如果您的 mixin 被硬编码为混合到一个特定的类中,那么您最好将方法直接写入该类,而不是将它们分开。

    要正确输入,MyMixin 应该针对 interface 或 Python 用语中的抽象类进行编码:

    import abc
    
    
    class MixinDependencyInterface(abc.ABC):
        @abc.abstractmethod
        def foo(self):
            pass
    
    
    class MyMixin:
        def func2(self: MixinDependencyInterface, xxx):
            self.foo()  # ← mixin only depends on the interface
    
    
    class Main(MixinDependencyInterface, MyMixin):
        def foo(self):
            print('bar')
    

    【讨论】:

    • 好吧,我并不是说我的解决方案很棒。这正是我试图做的,以使代码更易于管理。您的建议可能会通过,但这实际上意味着在我的 specific 案例中将整个 Main 类移动到接口。
    猜你喜欢
    • 2020-04-15
    • 2020-11-23
    • 2019-04-12
    • 2019-05-13
    • 2012-10-13
    • 2013-12-02
    • 2014-04-06
    • 1970-01-01
    相关资源
    最近更新 更多