繁体   English   中英

如何为任意数量的任意键/值对键入提示/类型检查字典(在运行时)?

[英]How to type-hint / type-check a dictionary (at runtime) for an arbitrary number of arbitrary key/value pairs?

我正在定义一个类,如下所示:

from numbers import Number
from typing import Dict

from typeguard import typechecked

Data = Dict[str, Number]

@typechecked
class Foo:
    def __init__(self, data: Data):
        self._data = dict(data)
    @property
    def data(self) -> Data:
        return self._data

我正在使用typeguard 我的目的是限制可以进入数据字典的类型。 显然, typeguard会检查整个字典,如果它被传递到一个函数或从一个函数返回。 如果直接“暴露”字典,检查类型就变成了字典的“责任”——这显然不起作用:

bar = Foo({'x': 2, 'y': 3}) # ok

bar = Foo({'x': 2, 'y': 3, 'z': 'not allowed'}) # error as expected

bar.data['z'] = 'should also be not allowed but still is ...' # no error, but should cause one

PEP 589引入了类型字典,但用于一组固定的键(类似于其他语言中的struct -like 构造)。 相比之下,对于灵活数量的任意键,我需要它。

我最好的坏主意是走“老派”:对dict子类化并重新实现 API 的每一位,通过这些 API 数据可以进(出)字典,并向它们添加类型检查:

@typechecked
class TypedDict(dict): # just a sketch
    def __init__(
        self,
        other: Union[Data, None] = None,
        **kwargs: Number,
    ):
        pass # TODO
    def __setitem__(self, key: str, value: Number):
        pass # TODO
    # TODO

是否有不需要“老派”方法的有效替代方案?

你的问题似乎有两个部分。


(1) 在运行时创建类型检查字典


正如@juanpa.arrivillaga 在评论中所说,这与类型检查有关,但似乎与类型提示无关。 然而,设计您自己的自定义类型检查数据结构是相当简单的。 您可以使用collections.UserDict这样做:

from collections import UserDict
from numbers import Number

class StrNumberDict(UserDict):
    def __setitem__(self, key, value):
        if not isinstance(key, str):
            raise TypeError(
                f'Invalid type for dictionary key: '
                f'expected "str", got "{type(key).__name__}"'
            )
        if not isinstance(value, Number):
            raise TypeError(
                f'Invalid type for dictionary value: '
                f'expected "Number", got "{type(value).__name__}"'
            )
        super().__setitem__(key, value)

使用中:

>>> d = StrNumberDict()
>>> d['foo'] = 5
>>> d[5] = 6
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<string>", line 5, in __setitem__
TypeError: Invalid type for dictionary key: expected "str", got "int"
>>> d['bar']='foo'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<string>", line 10, in __setitem__
TypeError: Invalid type for dictionary value: expected "str", got "str"

如果你想概括这种事情,你可以这样做:

from collections import UserDict

class TypeCheckedDict(UserDict):
    def __init__(self, key_type, value_type, initdict=None):
        self._key_type = key_type
        self._value_type = value_type
        super().__init__(initdict)

    def __setitem__(self, key, value):
        if not isinstance(key, self._key_type):
            raise TypeError(
                f'Invalid type for dictionary key: '
                f'expected "{self._key_type.__name__}", '
                f'got "{type(key).__name__}"'
            )
        if not isinstance(value, self._value_type):
            raise TypeError(
                f'Invalid type for dictionary value: '
                f'expected "{self._value_type.__name__}", '
                f'got "{type(value).__name__}"'
            )
        super().__setitem__(key, value)

使用中:

>>> from numbers import Number
>>> d = TypeCheckedDict(key_type=str, value_type=Number, initdict={'baz': 3.14})
>>> d['baz']
3.14
>>> d[5] = 5
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<string>", line 9, in __setitem__
TypeError: Invalid type for dictionary key: expected "str", got "int"
>>> d['foo'] = 'bar'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<string>", line 15, in __setitem__
TypeError: Invalid type for dictionary value: expected "Number", got "str"
>>> d['foo'] = 5
>>> d['foo']
5

请注意,您不需要对传递给super().__init__()的字典进行类型检查。 UserDict.__init__调用self.__setitem__ ,您已经覆盖了它,因此如果您将无效字典传递给TypeCheckedDict.__init__ ,您会发现引发异常的方式与您尝试添加无效键或字典构建后的值:

>>> from numbers import Number
>>> d = TypeCheckedDict(str, Number, {'foo': 'bar'})
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "<string>", line 5, in __init__
  line 985, in __init__
    self.update(dict)
  line 842, in update
    self[key] = other[key]
  File "<string>", line 16, in __setitem__
TypeError: Invalid type for dictionary value: expected "Number", got "str"

UserDict是专门为以这种方式轻松创建子类而设计的,这就是为什么在此实例中它是比dict更好的基类。

如果您想向TypeCheckedDict添加类型提示,您可以这样做:

from collections import UserDict
from collections.abc import Mapping, Hashable
from typing import TypeVar, Optional

K = TypeVar('K', bound=Hashable)
V = TypeVar('V')

class TypeCheckedDict(UserDict[K, V]):
    def __init__(
        self, 
        key_type: type[K], 
        value_type: type[V], 
        initdict: Optional[Mapping[K, V]] = None
    ) -> None:
        self._key_type = key_type
        self._value_type = value_type
        super().__init__(initdict)

    def __setitem__(self, key: K, value: V) -> None:
        if not isinstance(key, self._key_type):
            raise TypeError(
                f'Invalid type for dictionary key: '
                f'expected "{self._key_type.__name__}", '
                f'got "{type(key).__name__}"'
            )
        if not isinstance(value, self._value_type):
            raise TypeError(
                f'Invalid type for dictionary value: '
                f'expected "{self._value_type.__name__}", '
                f'got "{type(value).__name__}"'
            )
        super().__setitem__(key, value)

(以上通过 MyPy 。)

但是请注意,添加类型提示与此数据结构在运行时的工作方式完全无关。


(2) 类型提示字典“用于灵活数量的任意键”


我不太确定你的意思,但是如果你想让 MyPy 在你只想要数字值的字典中添加一个字符串值时引发错误, 你可以这样做

from typing import SupportsFloat

d: dict[str, SupportsFloat] = {}
d['a'] = 5  # passes MyPy 
d['b'] = 4.67 # passes MyPy
d[5] = 6 # fails MyPy
d['baz'] = 'foo' # fails Mypy 

如果你想要 MyPy 静态检查运行时检查,你最好的选择(在我看来)是使用上面TypeCheckedDict的类型提示版本:

d = TypeCheckedDict(str, SupportsFloat) # type: ignore[misc]
d['a'] = 5  # passes MyPy 
d['b'] = 4.67  # passes MyPy 
d[5] = 6  # fails Mypy 
d['baz'] = 'foo'  # fails Mypy

Mypy 不太高兴我们将抽象类型作为参数传递给TypeCheckedDict.__init__ ,因此您必须在实例化 dict 时添加# type: ignore[misc] (这对我来说就像是 MyPy 错误。)但是,除此之外, 它运行良好

(有关使用SupportsFloat提示数字类型的警告,请参阅我之前的回答。如果您使用的是 Python <= 3.8,请使用typing.Dict而不是dict进行类型提示。)

好的建议,但我观察到它比所有这些都简单得多。 @alex-waygood 有一些很好的解释,即使像我一样,你发现他的解决方案有点矫枉过正,但他的回答要正确得多。

class HomebrewTypeGuarding:
    """"Class mimics the primary interface of a dict, as an example
        of naive guarding using the __annotations__ collection that
        exists on any type-annotated object.
    """
    a: str
    b: int
    def __getitem__(self, key):
        return (
            self.__dict__[key] 
            if key in self.__dict__ 
            else None
        )
    def __setitem__(self, key, value):
        if (
            key in self.__annotations__
            and isinstance(value, self.__annotations__[key])
        ):
            self.__dict__[key] = value
        else:
            raise ValueError(
                "Incorrect type for value on {}:{} = {}".format(
                    key,
                    str(
                        self.__annotations__[key] 
                        if key in self.__annotations__
                        else None
                    ),
                    str(value)
                )
            )
        

这里的管道要简单得多,但请注意,我并不是说这是一个字典。 我的意图是

  1. 使用annotations集合简单地演示类型保护,以及
  2. 这样做的方式可以远远超出 dicts。

如果您真的只是在进行类型检查的 dict 之后,那么@alex-waygood 在那里进行了更详细的介绍,他的解决方案实际上是完整且正确的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM