ChatGPT解决这个技术问题 Extra ChatGPT

没有循环导入的 Python 类型提示

我正试图将我的大班分成两部分;好吧,基本上进入“主”类和带有附加功能的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) 以便来自更具体类的方法可以覆盖来自基类的方法
我不确定这些评论有什么用,因为它们与所提出的问题无关。 velis 并没有要求进行代码审查。

M
Michael0x2a

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

# 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
这很好用!您可以对 mixin 进行 linting/类型检查,而无需在运行时进行循环导入。谢谢!
T
Tomasz Bartkowiak

对于仅在为类型检查导入类时遇到循环导入问题的人:您可能希望使用 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 -> Invalidate Caches吗?
谢谢。对不起,我已经删除了我的评论。它曾提到这可行,但 PyCharm 抱怨。我使用 Velis 建议的 if False hack 解决了问题。使缓存无效并没有解决它。这可能是 PyCharm 的问题。
@JacobLee 除了 if False:,您还可以使用 from typing import TYPE_CHECKINGif TYPE_CHECKING:
如果类型驻留在另一个模块中,这将不起作用(至少 pycharm 不理解它)。如果字符串可以是完全限定的路径,那就太好了。
该解决方案在 VSCode 中运行良好!谢谢!!
d
deceze

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

要使用合理的输入正确执行此操作,应针对 interface 或 Python 用语中的抽象类对 MyMixin 进行编码:

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

好吧,我并不是说我的解决方案很棒。这正是我试图做的,以使代码更易于管理。您的建议可能会通过,但这实际上意味着在我的特定情况下将整个 Main 类移动到接口。
我认为这是唯一正确的解决方案。而且由于 OP 希望 MainMyMixin 分别在文件 main.py 和 mymixin.py 中分开,我想这必然意味着创建第三个文件 api.py 持有 MixinDependencyInterface,不是吗?
@velis typing.Protocol 可以代替 abc.ABC 使用,因为您实际上不需要子类化它来注册它。这是提供您计划使用的接口的正确方法,而 abc.ABC 更适合您提供部分完成的实现,即您实际上想要子类化它。
S
Shane Bishop

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

# 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 语句中的 import 永远不会被导入(但 IDE 无论如何都知道它)并使用 Main 类作为字符串,因为它在运行时不知道。


我希望这会导致有关死代码的警告。
@Phil:是的,当时我使用的是 Python 3.4。现在有打字了。TYPE_CHECKING
看起来很愚蠢,但可以与 PyCharm 一起使用。有我的赞成票! :)
B
Ben Mares

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

在我的例子中,

Ca() 将是一个返回字符串 hello 的方法

Cb() 将是一个返回 hello goodbye 的方法

Cx() 将因此返回 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"

https://i.stack.imgur.com/FquCl.png

一切都按预期运行:

>>> 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 并引用上面提到的类型声明。


S
Simply Beautiful Art

与其强迫自己参与 typing.TYPE_CHECKING 恶作剧,还有一种避免循环类型提示的简单方法:不要使用 from 导入,而使用 from __future__ import annotations 或字符串注释。

# foo.py
from __future__ import annotations
import bar


class Foo:
    bar: bar.Bar
# bar.py
import foo


class Bar:
    foo: "foo.Foo"

这种导入方式是“惰性求值”的,而使用 from foo import Foo 会强制 Python 运行整个 foo 模块,以便在导入行立即获得 Foo 的最终值。如果您还需要在运行时使用它,这非常有用,例如,如果需要在函数/方法中使用 foo.Foobar.Bar,因为您的函数/方法应该只调用一次 foo.Foo 并且 bar.Bar 可以使用。


V
Valentin Fabianski

正如其他一些人所建议的那样,我建议重构您的代码。

我可以向您展示我最近遇到的一个循环错误:

前:

# 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 的线源,所以我按照一些任意标准进行拆分。
A
AmirHossein

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

在这种情况下,您是

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


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