简体   繁体   English

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

[英]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.你的问题似乎有两个部分。


(1) Creating a type-checked dictionary at runtime (1) 在运行时创建类型检查字典


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.但是请注意,添加类型提示与此数据结构在运行时的工作方式完全无关。


(2) Type-hinting dictionaries "for a flexible number of arbitrary keys" (2) 类型提示字典“用于灵活数量的任意键”


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我的意图是

  1. To simply demonstrate type-guarding using the annotations collection, and使用annotations集合简单地演示类型保护,以及
  2. To do so in a way that can extend much further beyond dicts.这样做的方式可以远远超出 dicts。

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.

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