带有类型提示的模块化类方法
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__
不同的全局范围。
另请注意,最佳答案从未被接受。此外,将import
s 放在类定义的中间是一种糟糕的编程习惯。
您对一个文件中的“大量代码”有何顾虑?适当的源代码控制应该使多人处理一个文件成为非问题。
我相信 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 的方法。用不能使用预先存在的实例的函数替换它根本不等效。
对不起,我认为这会有所帮助,但我最终误解了您的建议。以上是关于带有类型提示的模块化类方法的主要内容,如果未能解决你的问题,请参考以下文章
如何检查定义的方法?来自在类中使用的模块,其中包含模块后定义的方法