[英]How to type-hint / type-check a dictionary (at runtime) for an arbitrary number of arbitrary key/value pairs?
I am defining a class about as follows:我正在定义一个类,如下所示:
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
I am using typeguard
.我正在使用
typeguard
。 My intention is to restrict the types that can go into the data dictionary.我的目的是限制可以进入数据字典的类型。 Obviously,
typeguard
does check the entire dictionary if it is passed into a function or returned from one.显然,
typeguard
会检查整个字典,如果它被传递到一个函数或从一个函数返回。 If the dictionary is "exposed" directly, it becomes the dictionary's "responsibility" to check types - which does not work, obviously:如果直接“暴露”字典,检查类型就变成了字典的“责任”——这显然不起作用:
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 introduces typed dictionaries, but for a fixed set of keys (similar to struct
-like constructs in other languages). PEP 589引入了类型字典,但用于一组固定的键(类似于其他语言中的
struct
-like 构造)。 In contrast, I need this for a flexible number of arbitrary keys.相比之下,对于灵活数量的任意键,我需要它。
My best bad idea is to go "old-school": Sub-classing dict
and re-implementing every bit of API through which data can go in (and out) of the dictionary and adding type checks to them:我最好的坏主意是走“老派”:对
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
Is there a valid alternative that does not require the "old-school" approach?是否有不需要“老派”方法的有效替代方案?
There seem to be two parts to your question.你的问题似乎有两个部分。
As @juanpa.arrivillaga says in the comments , this has everything to do with type- checking , but doesn't seem to have anything to do with type- hinting . 正如@juanpa.arrivillaga 在评论中所说,这与类型检查有关,但似乎与类型提示无关。 However, it's fairly trivial to design your own custom type-checked data structure.
然而,设计您自己的自定义类型检查数据结构是相当简单的。 You can do it like this using
collections.UserDict
:您可以使用
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)
In usage:使用中:
>>> 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"
If you wanted to generalise this kind of thing, you could do it like this:如果你想概括这种事情,你可以这样做:
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)
In usage:使用中:
>>> 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
Note that you don't need to do type checks for the dictionary you pass to super().__init__()
.请注意,您不需要对传递给
super().__init__()
的字典进行类型检查。 UserDict.__init__
calls self.__setitem__
, which you've already overridden, so if you pass an invalid dictionary to TypeCheckedDict.__init__
, you'll find an exception is raised in just the same way as if you try to add an invalid key or value to the dictionary after it has been constructed: 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
is specifically designed for easy subclassing in this way, which is why it is a better base class in this instance than dict
. UserDict
是专门为以这种方式轻松创建子类而设计的,这就是为什么在此实例中它是比dict
更好的基类。
If you wanted to add type hints to TypeCheckedDict
, you'd do it like this:如果您想向
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)
(The above passes MyPy .) (以上通过 MyPy 。)
Note, however, that adding type hints has no relevance at all to how this data structure works at runtime.但是请注意,添加类型提示与此数据结构在运行时的工作方式完全无关。
I'm not quite sure what you mean by this, but if you want MyPy to raise an error if you add a string value to a dictionary you only want to have numeric values, you could do it like this :我不太确定你的意思,但是如果你想让 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
If you want MyPy static checks and runtime checks, your best bet (in my opinion) is to use the type-hinted version of TypeCheckedDict
above:如果你想要 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 isn't too happy about us passing an abstract type in as a parameter to TypeCheckedDict.__init__
, so you have to add a # type: ignore[misc]
when instantiating the dict. Mypy 不太高兴我们将抽象类型作为参数传递给
TypeCheckedDict.__init__
,因此您必须在实例化 dict 时添加# type: ignore[misc]
。 (That feels like a MyPy bug to me.) Other than that, however, it works fine . (这对我来说就像是 MyPy 错误。)但是,除此之外, 它运行良好。
(See my previous answer for caveats about using SupportsFloat
to hint numeric types. Use typing.Dict
instead of dict
for type-hinting if you're on Python <= 3.8.) (有关使用
SupportsFloat
提示数字类型的警告,请参阅我之前的回答。如果您使用的是 Python <= 3.8,请使用typing.Dict
而不是dict
进行类型提示。)
Good suggestions, but I observe that it gets much simpler than all that.好的建议,但我观察到它比所有这些都简单得多。 @alex-waygood has some great explanations, and even if like me, you find his solution to be a tad overkill, his answer is much more correct.
@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)
)
)
The plumbing here is much simpler, but do note I'm not claiming this is a dict.这里的管道要简单得多,但请注意,我并不是说这是一个字典。 My intention is
我的意图是
If you're really just after a type-checked dict, well, @alex-waygood goes into much greater detail there, and his solution is actually complete and correct.如果您真的只是在进行类型检查的 dict 之后,那么@alex-waygood 在那里进行了更详细的介绍,他的解决方案实际上是完整且正确的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.