无需循环导入的 Python 类型提示

2025-01-16 08:37:00
admin
原创
163
摘要:问题描述:我正在尝试将我的大班级分成两部分;基本上分成“主”班级和具有附加功能的混合班级,如下所示:main.py文件:import mymixin.py class Main(object, MyMixin): def func1(self, xxx): ... mymixin.p...

问题描述:

我正在尝试将我的大班级分成两部分;基本上分成“主”班级和具有附加功能的混合班级,如下所示:

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 仍然可以为我提供自动完成功能以及了解类型的所有其他好东西?


解决方案 1:

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

# 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将使所有类型提示都变成字符串并跳过对它们的求值。这可以帮助我们的代码更加符合人体工程学。

尽管如此,使用 mixin 和 mypy 可能需要比您目前拥有的更多的结构。Mypy推荐的方法基本上就是deceze所描述的那样——创建一个您的MainMyMixin类都继承的 ABC。如果您最终需要做类似的事情才能让 Pycharm 的检查器满意,我不会感到惊讶。

解决方案 2:

对于在仅为类型检查而导入类时遇到循环导入困难的人来说:您可能需要使用前向引用(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

解决方案 3:

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

为了以合理的类型正确地执行此操作,MyMixin应该针对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')

解决方案 4:

与其强迫自己参与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.Barfoo.Foo`bar.Bar

解决方案 5:

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

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

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__导入启用了延迟注释。这是可选的;如果没有它,您必须引用类型注释(即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"

self.a()在 VS Code 中,我可以看到悬停时
检测到的类型:在此处输入图片描述

一切按预期进行:

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

有关旧 Python 版本的说明

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

对于 Python 版本 ≤ 3.6,未定义延期注解。解决方法是省略from __future__ import annotations并引用上述类型声明。

解决方案 6:

事实证明,我最初的尝试也非常接近解决方案。这是我目前正在使用的:

# 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
        ...

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

解决方案 7:

我建议重构你的代码,就像其他人建议的那样。

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

前:

# 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

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   3911  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   2721  
  本文介绍了以下10款项目管理软件工具:禅道项目管理软件、Freshdesk、ClickUp、nTask、Hubstaff、Plutio、Productive、Targa、Bonsai、Wrike。在当今快速变化的商业环境中,项目管理已成为企业成功的关键因素之一。然而,许多企业在项目管理过程中面临着诸多痛点,如任务分配不...
项目管理系统   61  
  本文介绍了以下10款项目管理软件工具:禅道项目管理软件、Monday、TeamGantt、Filestage、Chanty、Visor、Smartsheet、Productive、Quire、Planview。在当今快速变化的商业环境中,项目管理已成为企业成功的关键因素之一。然而,许多项目经理和团队在管理复杂项目时,常...
开源项目管理工具   61  
  本文介绍了以下10款项目管理软件工具:禅道项目管理软件、Smartsheet、GanttPRO、Backlog、Visor、ResourceGuru、Productive、Xebrio、Hive、Quire。在当今快节奏的商业环境中,项目管理已成为企业成功的关键因素之一。然而,许多企业在选择项目管理工具时常常面临困惑:...
项目管理系统   54  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用