我正在创建一个半单例类Foo
,它可以有(也可以是半单例)子类,构造函数有一个参数,我们称之为slug
,并且每个(子)类对于slug
的每个值最多只能有一个示例。
假设我有一个Foo
的子类Bar
,下面是一个调用的例子:
Foo("a slug")
-〉返回使用键(Foo, "a slug")
保存的Foo
的新示例。Foo("some new slug")
-〉返回一个新的示例Foo
,用键(Foo, "some new slug")
保存。Foo("a slug")
-〉我们有相同的类和步骤1中的slug
,所以这将返回步骤1中返回的相同示例。Bar("a slug")
-〉我们有和以前一样的slug,但是一个不同的类,所以这返回一个新的Bar
示例,用键(Bar, "a slug")
保存。Bar("a slug")
-〉这将返回与我们在步骤4中获得的Bar
相同的示例。
我知道如何实现这一点:类字典将type
和str
的元组关联到示例,覆盖__new__
等。简单的东西。
我的问题是这本词典怎样打注解?
我试着做的事情是这样的:
FooSubtype = TypeVar("FooSubtype", bound="Foo")
class Foo:
_instances: Final[dict[tuple[Type[FooSubtype], str], FooSubtype]] = dict()
因此,其思想是"无论键的第一个元素是什么类型(将其"赋值"给FooSubtype
类型变量),值都需要是相同类型的示例"。
这在Type variable "FooSubtype" is unbound
中失败了,我有点明白为什么了。
如果我这样拆分它,也会得到同样的错误:
FooSubtype = TypeVar("FooSubtype", bound="Foo")
InstancesKeyType: TypeAlias = tuple[Type[FooSubtype], str]
class Foo:
_instances: Final[dict[InstancesKeyType, FooSubtype]] = dict()
错误指向本例中的最后一行,这意味着问题出在值类型上,而不是键类型。mypy
也建议使用Generic
,但我不知道在这个特定的示例中如何使用,因为值的类型应该以某种方式与键的类型相关,而不是单独的泛型类型。
这是可行的:
class Foo:
_instances: Final[dict[tuple[Type["Foo"], str], "Foo"]] = dict()
但是它允许_instance[(Bar1, "x")]
是Bar2
类型(这里Bar1
和Bar2
是Foo
的不同子类),这不是一个大问题,我可以这样保留它,但是我想知道是否有更好(更严格)的方法。
1条答案
按热度按时间hc8w905p1#
这是一个非常好的问题。首先我看了一下,说"不,你根本不能",因为你不能表达dict键和值之间的任何关系。然而,后来我意识到你的建议几乎是可以实现的。
首先,让我们定义一个描述所需行为的协议:
它是如何工作的?它定义了一个具有项访问权限的任意数据结构,例如
cache_dict[(Foo1, 'foo')]
解析为Foo1
类型。(或collections.abc.MutableMapping
),但类型稍有不同。Dunder参数名几乎等同于仅定位参数(使用/
)。如果您需要其他方法(例如get
或pop
),也可以将它们添加到此定义中(您可能希望使用overload
)。您几乎肯定需要__contains__
,它应该与__delitem__
具有相同的签名。那么现在
哇,我们正确地推断出了值类型!我们将
dict
转换为所需的类型,因为我们的类型与dict
定义不同。它还有一个问题:你能做的
因为
_T
在这里正好解析为Foo
,但是这个问题是完全不可避免的:即使我们有一些infer
关键字或Infer
特殊形式拼写def __setitem__(self, __key: _KeyT[Infer[_T]], __value: _T) -> None
,它不会正常工作:注意,我们没有使用上面的任何类型转换,所以这段代码是类型安全的,但肯定与我们的意图相冲突。
因此,我们可能不得不接受
__setitem__
的不严格性,但至少从项访问中获得适当的类型。最后,类在
_T
中不是泛型的,因为否则所有的值都将被推断为声明的类型,而不是函数作用域(您可以尝试使用Protocol[_T]
作为基类,并观察发生了什么,这对于更深入地理解mypy
类型推断方法是非常好的)。下面是playground的完整代码链接。
此外,您可以子类化
MutableMapping[_KeyT['Foo'], 'Foo']
以获得更多的方法,而不是手动定义它们。它将开箱即用地处理__delitem__
和__contains__
,但是__setitem__
和__getitem__
仍然需要您的实现。下面是
MutableMapping
和get
的替代解决方案(因为get
的实现既复杂又有趣)(playground):注意,我们现在不使用
Protocol
(因为它还需要MutableMapping
作为协议),而是使用抽象方法。诡计,不要用它!
当我在写这个答案的时候,我发现了一个
mypy
bug,你可以用一种非常有趣的方式在这里滥用它,我们是从这样的东西开始的,对吗?现在,让我们将
__setitem__
签名更改为一个非常奇怪的东西。**警告:这是一个bug,不要依赖这个行为!**如果我们将__default
输入为_T | _Q
,我们会神奇地得到"正确的"类型,并严格缩小到第一个参数的类型。现在:
这是完全错误的,因为
_Q
联合部分可以解析为任何东西,并且实际上没有使用(此外,它根本不可能是类型变量,因为它在定义中只使用过一次)。此外,当右侧不是
Foo
子类时,这允许另一个无效赋值:我将很快报告这一点,并将问题与此问题联系起来。