带有类型提示的模块化类方法

Posted

技术标签:

【中文标题】带有类型提示的模块化类方法【英文标题】:Modular class methods with type hints 【发布时间】:2021-10-20 12:03:40 【问题描述】:

在这个问题中 How can I separate functions of class into multiple files? 最佳答案建议使用

from method_file import method

在类定义中,在单独的文件中定义类方法。但是,对于这样的课程

my_number.py

class MyNumber:
    def __init__(self):
        self.x = 5

    from my_method import my_method

my_method.py

def my_method(self):
    print(self.x)

IDE 不清楚self 指的是MyNumber 对象。因此代码完成(例如self.x)在my_method 中不可用。 self 的类型提示可以解决这个问题,即

my_method.py

from my_number import MyNumber

def my_method(self: MyNumber):
    print(self.x)

但这会导致循环导入。

对于这种情况是否有任何解决方法或最佳实践?

【问题讨论】:

想知道您的 IDE 在这种情况下将如何处理它,但 def my_method(self: "MyNumber"): 可能会工作。 我认为最好的做法是不要像这样跨多个模块拆分类的定义。例如,MyNumber.my_method 将看到与 MyNumber.__init__ 不同的全局范围。 另请注意,最佳答案从未被接受。此外,将imports 放在类定义的中间是一种糟糕的编程习惯。 您对一个文件中的“大量代码”有何顾虑?适当的源代码控制应该使多人处理一个文件成为非问题。 我相信 OP 的实际问题可以改写为“使用类型提示 + 类型检查器时如何避免循环导入”。当然“重写你的代码以避免循环导入”是一个解决方案,但我认为有一个替代方案是有价值的。讨论在运行时将函数绑定到类是否有意义似乎与此无关,因为循环导入 + 类型提示的相同问题可能出现在不同的上下文中。 【参考方案1】:

有一种方法可以结合 __future__ 导入以在运行时忽略类型注释,并使用 if TYPE_CHECKING 子句仅从 IDE 的角度“导入”代码,以便代码完成可用。

例子:

my_number.py

class MyNumber:
    def __init__(self):
        self.x = 5

    from my_method import my_method

my_method.py

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from my_number import MyNumber

def my_method(self: MyNumber):
    print(self.x)

使用from __future__ import annotations,我们推迟了对类型提示的评估——换句话说,即使我们实际上没有导入MyNumber,我们也可以输入提示my_method。这个行为是planned to be the default in Python 3.10,但它被推迟了,所以我们现在需要这个导入。

现在,根据您的编辑器/IDE,您仍会收到警告,抱怨 MyNumber 未定义,其方法/属性可能不会显示在自动完成中。这就是TYPE_CHECKING 发挥作用的地方:这只是一个常量False 值,这意味着我们的子句是:

if False:
    from my_number import MyNumber

换句话说,我们在“欺骗”IDE 以为我们正在导入MyNumber,但实际上该行永远不会执行。因此,我们完全避免了循环导入。

这可能会让人觉得有点 hacky,但它确实有效 :-) TYPE_CHECKING 常量的全部意义在于允许类型检查器完成它们的工作,而不是在运行时实际导入代码,并且清楚地这样做方式(Python 之禅:应该有一种——最好只有一种——明显的方式)。

这种方法在 PyCharm 中一直对我有用,但不确定其他 IDE/编辑器。

【讨论】:

如果您使用的是annotations,则不需要导入。 MyNumber 被视为文字字符串,而不是要评估的表达式。 (或者这仅仅是为了 IDE 的利益?) @chepner 但随后(在大多数 IDE 中)MyNumber 的代码完成在 my_method.py 中不可用,这正是 OP 所抱怨的。 @chepner 类型检查器仍然必须知道 MyNumber 是在哪个命名空间中定义的。否则,如果不得不猜测,那就是模棱两可了。 对;我忽略了 IDE 自动完成方面(因为它不是我通常使用的东西)。【参考方案2】:

对于这种情况是否有任何解决方法或最佳实践?

最好的做法是不要这样做。如果方法实现特定于某个类,则它应该是类定义的一部分。


如果方法不是特定于类的,则应在所有个有效类型中定义它。协议适合表达这一点:

from typing import Protocol, Any

class HasX(Protocol):
    x: Any  # might need a TypeVar for complex cases

def my_method(self: HasX):
    print(self.x)

如果一个方法扩展了一个独立于其定义的类,则不应对其进行修补。使用functools.singledispatch外部定义单个调度函数,这在逻辑上类似于方法:

from functools import singledispatch
from my_number import MyNumber

# not imported into MyNumber
@singledispatch
def my_method(self):
    raise NotImplementedError(f"no dispatch for type(self") 

@my_method.register
def _(self: MyNumber):
    print(self.x)

【讨论】:

【参考方案3】:

通常,self 关键字用于表示类的一个实例。输入提示自我是没有意义的。其次,如果您的函数不是 是一个 MyNumber 对象的类的方法,则不能通过 self 访问实例变量 x。

我会建议两个选项来完成你想要的。您可以接受 MyNumber 对象作为 my_method() 函数的参数,也可以创建一个新类并继承 MyNumber 类。确保文件在同一目录下,否则更新文件 2 中的导入语句。

选项#1

class MyNumber:
    def __init__(self):
        self.x = 5

def my_method(my_number: MyNumber):
    print(my_number.x)

my_method(MyNumber())

选项#2

#my_number.py
class MyNumber:
    def __init__(self):
        self.x = 5

#my_method.py
from my_number import MyNumber

class MyMethod(MyNumber):
    def __init__(self):
        super().__init__()
        
    def my_method(self):
        print(self.x)

MyMethod().my_method()

【讨论】:

这个答案似乎具有误导性。问题中显示的代码确实定义了MyNumber的方法,可以通过self访问x。虽然不常见,但在许多可行的情况下应该注释 self,以引用或限制 self 的类型。 什么意思?问题中没有方法 MyNumber ......有一个名为 MyNumber 的类,它存在于我的答案中 问题中的代码定义了一个方法 MyNumber,即MyNumber.my_method【参考方案4】:

我认为您在 python 中遇到了有关面向对象概念的问题。您的“my_method”函数不需要“self:MyNumber”作为参数,实际上,您需要创建 MyNumber 类的对象,因此该类将具有一个属性,即“x”,因为您定义了MyNumber 类的构造函数中的“x”。它看起来像这样:

#my_number.py
class MyNumber:
  def __init__(self):
    self.x = 5

#my_method.py
from my_number import MyNumber
def my_method():
  mm = MyNumber()
  print(mm.x)

【讨论】:

感谢您的回答。您的建议将是一种功能更强大的编程方法,在这种情况下可能确实更合适。然而,我的问题是关于模块化类定义,即将my_method(self) 定义为不同文件中的类方法。 这个答案似乎具有误导性。问题中显示的代码确实定义了一个像往常一样传递 self 的方法。用不能使用预先存在的实例的函数替换它根本不等效。 对不起,我认为这会有所帮助,但我最终误解了您的建议。

以上是关于带有类型提示的模块化类方法的主要内容,如果未能解决你的问题,请参考以下文章

如何检查定义的方法?来自在类中使用的模块,其中包含模块后定义的方法

TypeScript - 使用带有单元测试的模块或类?

maven打包时报错:找不到符号,errors提示:符号:类 xxx位置:程序包 xxx.xxx

导入模块的 Python 类型提示

带有验证器的 Perl 工作流模块

定义类+类实例化+属性+构造函数+匿名类型var+堆与栈+GC回收机制+值类型与引用类型