如何子类化字典以支持泛型类型提示?

Posted

技术标签:

【中文标题】如何子类化字典以支持泛型类型提示?【英文标题】:How to subclass a dictionary so it supports generic type hints? 【发布时间】:2020-07-21 13:44:56 【问题描述】:

如何对字典进行子类化以使子类支持泛型类型提示?它需要在各个方面表现得像字典,并支持键和值的类型提示。子类将添加访问和操作字典数据的函数。例如,它将有一个 valueat(self, idx:int) 函数,该函数返回给定索引处的字典值。

它不需要OrderedDict 作为它的基类,但字典确实需要有一个可预测的顺序。由于OrderedDict 维护插入顺序并支持类型提示,因此它似乎是一个合理的起点。 这是我尝试过的:

from collections import OrderedDict

class ApplicationSpecificDict(OrderedDict[str, int]):
    ...

但是,它失败并出现以下错误: TypeError: 'type' object is not subscriptable

这在 Python 3.7+ 中不支持,还是我遗漏了什么?

【问题讨论】:

您已经可以使用typing.Dict[str, int] 键入提示常规字典。您还在寻找其他东西吗? 不确定您要做什么的具体细节,但根据您的要求,数据类可能是实现目标的更优雅的解决方案。它们处理类型、默认值,并具有用于更新的 replace() 方法。它们与字典的互操作也非常好,包括使用 **dict_val 进行初始化,如果您确实需要纯字典功能,则使用其 asdict() 方法将实例变量转换为字典。 @JohnS 谢谢。我在几个模块中使用dataclassdataclass 中缺少的是 dict 的行为(显然;但是可以通过实现 dict 方法来补充这一点,就像我在 TypedDict 中所做的那样)并且还缺乏对类型提示的支持。用户如何指定数据类的dict 数据成员的键/值数据类型? 数据类的重点是没有任何字段,只有一些定义明确的字段。 @user2357112 目标是让类的行为类似于字典类,但具有访问和操作与键和值的类型提示一致的字典条目的附加方法。 【参考方案1】:

typing 包提供了对应于collections.abccollections 中的非泛型类的泛型类。这些泛型类可以用作基类来创建用户定义的泛型类,例如自定义泛型字典。

collections.abc中类型对应的泛型类示例:

typing.AbstractSet(Sized, Collection[T_co]) typing.Container(Generic[T_co]) typing.Mapping(Sized, Collection[KT], Generic[VT_co]) typing.MutableMapping(Mapping[KT, VT]) typing.MutableSequence(Sequence[T]) typing.MutableSet(AbstractSet[T]) typing.Sequence(Reversible[T_co], Collection[T_co])

collections中类型对应的泛型类示例:

typing.DefaultDict(collections.defaultdict, MutableMapping[KT, VT]) typing.OrderedDict(collections.OrderedDict, MutableMapping[KT, VT]) typing.ChainMap(collections.ChainMap, MutableMapping[KT, VT]) typing.Counter(collections.Counter, Dict[T, int]) typing.Deque(deque, MutableSequence[T])

实现自定义通用字典

实现自定义通用字典有很多选项。但是,需要注意的是,除非用户定义的类显式继承自 MappingMutableMapping,否则像 mypy 这样的静态类型检查器不会将该类视为映射。

用户定义的通用字典示例

from collections import abc  # Used for isinstance check in `update()`.
from typing import Dict, Iterator, MutableMapping, TypeVar

KT = TypeVar('KT')
VT = TypeVar('VT')


class MyDict(MutableMapping[KT, VT]):

    def __init__(self, dictionary=None, /, **kwargs) -> None:
        self.data: Dict[KT, VT] = 
        if dictionary is not None:
            self.update(dictionary)
        if kwargs:
            self.update(kwargs)
    
    def __contains__(self, key: KT) -> bool:
        return key in self.data

    def __delitem__(self, key: KT) -> None:
        del self.data[key]

    def __getitem__(self, key: KT) -> VT:
        if key in self.data:
            return self.data[key]
        raise KeyError(key)

    def __len__(self) -> int:
        return len(self.data)

    def __iter__(self) -> Iterator[KT]:
        return iter(self.data)

    def __setitem__(self, key: KT, value: VT) -> None:
        self.data[key] = value
    
    @classmethod
    def fromkeys(cls, iterable: Iterable[KT], value: VT) -> "MyDict":
        """Create a new dictionary with keys from `iterable` and values set 
        to `value`.

        Args:
            iterable: A collection of keys.
            value: The default value. All of the values refer to just a single 
                instance, so it generally does not make sense for `value` to be a 
                mutable object such as an empty list. To get distinct values, use 
                a dict comprehension instead.

        Returns:
            A new instance of MyDict.
        """
        d = cls()
        for key in iterable:
            d[key] = value
        return d

    def update(self, other=(), /, **kwds) -> None:
        """Updates the dictionary from an iterable or mapping object."""
        if isinstance(other, abc.Mapping):
            for key in other:
                self.data[key] = other[key]
        elif hasattr(other, "keys"):
            for key in other.keys():
                self.data[key] = other[key]
        else:
            for key, value in other:
                self.data[key] = value
        for key, value in kwds.items():
            self.data[key] = value

【讨论】:

哇,多好的模板啊!!!问题:在__getitem__() 中,为什么要进行key in self.data 测试?难道不能直接return self.data[key],就像data__getitem__自己提升KeyError一样吗? @MestreLion __getitem__() 键检查是为了在出现KeyError 的情况下,错误源自MyDict 而不是嵌套的MyDict.data(这将不必要地暴露实现细节)。【参考方案2】:

我在this 上发布了您的问题可能是骗子,但我也将其包含在此处,因为我在谷歌搜索如何执行此操作时发现了这两个问题。

基本上,您需要使用输入Mapping generic 这是 dict 使用的通用注解,因此您可以定义其他类型,例如 MyDict[str, int]

如何:

import typing
from collections import OrderedDict

# these are generic type vars to tell mutable-mapping 
# to accept any type vars when creating a sub-type of your generic dict
_KT = typing.TypeVar("_KT") #  key type
_VT = typing.TypeVar("_VT") #  value type


# `typing.MutableMapping` requires you to implement certain functions like __getitem__
# You can get around this by just subclassing OrderedDict first.
# Note: The generic you're subclassing needs to come BEFORE
# the `typing.MutableMapping` subclass or accessing indices won't work.

class ApplicationSpecificDict(
        OrderedDict, 
        typing.MutableMapping[_KT, _VT]
):
    """Your special dict"""
    ...

# Now define the key, value types for sub-types of your dict
RequestDict = MyDict[str, typing.Tuple[str, str]]
ModelDict = MyDict[str, typing.Any]

现在使用您的子类型字典的自定义类型:

from my_project.custom_typing import ApplicationSpecificDict #  Import your custom type

def make_request() -> ApplicationSpecificDict:
    request = ApplicationSpecificDict()
    request["test"] = ("sierra", "117")
    return request

print(make_request())

将输出为 "test": ("sierra", "117")

【讨论】:

【参考方案3】:

简而言之,假设您有一个基类MyDictBase。要添加类型提示,请执行MyDictTyped = MyDictBase[str, int]

例如

class MyDictBase(dict):
    pass

MyDictTyped = MyDictBase[str, int]

【讨论】:

以上是关于如何子类化字典以支持泛型类型提示?的主要内容,如果未能解决你的问题,请参考以下文章

Java泛型

Java泛型

泛型类派生子类

实例化泛型类时如何避免指定冗余类型

JAVA中的泛型类是啥东西?

如何通过使用类型名称来实例化泛型类?