我正在尝试编写一个具有接受额外值的附加构造方法的类。这些额外值的计算成本很高,并且会在程序结束时保存,因此
.initialize()
有效地充当注入,以避免在程序的后续运行中再次重新计算它们。
class TestClass:
init_value: str
secondary_value: int
@overload
@classmethod
def initialize(cls: type["TestClass"], init_value: str, **kwargs) -> "TestClass":
...
@overload
@classmethod
def initialize(cls: "TestClass", **kwargs) -> "TestClass":
# The erased type of self "TestClass" is not a supertype of its class "Type[TestClass]
...
@classmethod
def initialize(cls: type["TestClass"] | "TestClass", init_value: str | None = None, **kwargs) -> "TestClass":
if isinstance(cls, type):
instance = cls(init_value=init_value)
# Argument "init_value" to "TestClass" has incompatible type "Optional[str]"; expected "str"
else:
instance = cls
for extra_key, extra_value in kwargs.items():
setattr(instance, extra_key, extra_value)
return instance
def __init__(self, init_value: str) -> None:
self.init_value = init_value
instance1 = TestClass.initialize(init_value="test", secondary_value="test2")
instance2 = TestClass(init_value="test").initialize(secondary_value="test2")
# Missing positional argument "init_value" in call to "initialize" of "TestClass"
instance1.init_value
instance2.init_value
instance1.secondary_value
instance2.secondary_value
如何才能完成上述工作,以便
TestClass(init_value).initialize()
不需要将 init_value 传递给 .initialize()
,因为它已经在 __init__
中声明,而 TestClass.initialize()
则需要?
简而言之,如何根据是在实例上还是在类上调用来定义具有不同类型的类方法?
这些额外的值不能在
__init__
中声明,因为类的内部结构很复杂,在这里重复太长了。
这里有两个不同的问题:
让我们从简单的部分开始,暂时忘记 mypy。
当在实例对象上调用方法时(意味着它是在对象的类型上声明的),Python 知道这是一个方法调用,并在传递的参数前面加上对象本身:这是方法的常见用法。
当在类对象上调用方法时(意味着它是在对象本身上声明的),Python 在普通函数调用中看到它,并简单地转发传递的参数。因此,如果
foo
是 Bar
类的方法,并且 bar
是 Bar
实例,则以下代码片段严格等效:
bar.foo(args...)
Bar.foo(bar, args...)
这意味着要从类对象或其实例调用您的
initialize
方法,应该声明它:
def initialize(init_value, **kwargs)-> "TestClass":
if isinstance(init_value, str): # handle a str from the class object
instance = TestClass(init_value)
elif not isinstance(init_value, TestClass): # reject any other call...
raise TypeError("Argument of initialize must be str")
else: # handle a call from an instance object
instance = init_value
for extra_key, extra_value in kwargs.items():
setattr(instance, extra_key, extra_value)
return instance
此代码在运行时可以正常工作。
现在是 mypy 部分...
不幸的是,mypy 的目标是帮助 Python 程序员提供一致且易于维护的代码。为此,它提供了一个静态类型分析器来确保运行时参数与声明一致并且多个重载声明都一致。这里开始满足您的要求:
换句话说,无法进行重载声明...
所以我发现的唯一方法就是说实话:您在
TestClass
上定义了一个属性,它是一个函数,当它作为方法调用时,可以接收 TestClass
对象作为其第一个参数,或者接收字符串当它被类对象本身调用时
...
def initialize(init_value: Union['TestClass', str], **kwargs)-> "TestClass"::
if isinstance(init_value, str):
instance = TestClass(init_value)
...
您可以使用描述符根据访问位置返回两种不同的方法。这是一个实现:
from __future__ import annotations
from typing import Any, Callable, Generic, Optional, Type, Union, overload
from typing_extensions import Concatenate, ParamSpec, TypeVar
T = TypeVar('T')
P = ParamSpec('P')
R = TypeVar('R')
# Generic-bound typevars
PI = ParamSpec('PI') # Instance method params
RI = TypeVar('RI') # Instance method return
PC = ParamSpec('PC') # Class method params
RC = TypeVar('RC') # Class method return
class dualmethod(Generic[PI, RI, PC, RC]):
"""Decorate a method to have a classmethod variant."""
def __init__(
self,
fget: Callable[Concatenate[Any, PI], RI],
cget: Optional[Callable[Concatenate[Any, PC], RC]] = None,
) -> None:
self.fget = self.__wrapped__ = fget
self.cget = cget
def classmethod(
self,
cget: Union[Callable[Concatenate[Any, P], R], classmethod[Any, P, R]],
) -> dualmethod[PI, RI, P, R]:
if isinstance(cget, classmethod):
cget = cget.__func__
return dualmethod(self.fget, cget)
@overload
def __get__(self, obj: None, cls: Type[T]) -> Callable[PC, RC]:
...
@overload
def __get__(self, obj: T, cls: Type[T]) -> Callable[PI, RI]:
...
def __get__(
self, obj: Optional[T], cls: Type[T]
) -> Union[Callable[PI, RI], Callable[PC, RC]]:
if obj is None:
if self.cget is None:
# No classmethod available so return an unbound method, although this
# means our actual return type is `Callable[Concatenate[T, PI], RI]`.
# Since we can't specify an overload based on self.cget, the only fix is
# to split this class into two, the first for before the classmethod has
# been supplied, and the second for after.
# Before classmethod:
# __get__ -> Callable[PI, RI] | Callable[Concatenate[T, PI], RI]
# With classmethod:
# __get__ => Callable[PI, RI] | Callable[PC, RC]
return self.fget.__get__(obj, cls)
return self.cget.__get__(cls, cls) # Return classmethod
return self.fget.__get__(obj, cls) # Return instance method
class Host:
@dualmethod
def initialize(self, **kwargs) -> Host:
return self
@initialize.classmethod # type: ignore[no-redef]
@classmethod
def initialize(cls, init_var) -> Host:
return cls()
在测试时,我发现Mypy丢失了
self
/cls
类型的信息,并且似乎通过忽略它来处理重新定义,因此它根本无法识别类方法。 Pyright 没有这些问题,所以我已经提交了错误报告。