[英]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
是否有不需要“老派”方法的有效替代方案?
你的问题似乎有两个部分。
正如@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 。)
但是请注意,添加类型提示与此数据结构在运行时的工作方式完全无关。
我不太确定你的意思,但是如果你想让 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)
)
)
这里的管道要简单得多,但请注意,我并不是说这是一个字典。 我的意图是
如果您真的只是在进行类型检查的 dict 之后,那么@alex-waygood 在那里进行了更详细的介绍,他的解决方案实际上是完整且正确的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.