【问题标题】:How do I check if a value matches a type in python?如何检查值是否与python中的类型匹配?
【发布时间】:2019-08-25 11:39:00
【问题描述】:

假设我有一个 python 函数,它的单个参数是一个非平凡类型:

from typing import List, Dict
ArgType = List[Dict[str, int]]  # this could be any non-trivial type
def myfun(a: ArgType) -> None:
    ...

...然后我有一个从 JSON 源解压缩的数据结构:

import json
data = json.loads(...)

我的问题是:在将data 用作myfun() 的参数之前,如何检查在运行时 是否具有正确的类型以用作myfun() 的参数?

if not isCorrectType(data, ArgType):
    raise TypeError("data is not correct type")
else:
    myfun(data)

【问题讨论】:

  • 您只需在dataexcept (TypeError, ...) 上运行任何代码(或在该上下文中可能发生的任何其他异常)。 typing 用于类型提示,除此之外,Python 更多地依赖于 duck typing
  • mypy........?
  • @a_guest:你为不预先验证提出了一个很好的论据,但是我仍然想进行验证,以便我可以(希望)生成一个合理的错误消息而不是得到一个来自不相关模块深处的意外AttributeError: 'NoneType' has no attribute 'get'

标签: python type-hinting python-typing


【解决方案1】:

验证类型注释是一项不平凡的任务。 Python 不会自动执行此操作,并且编写自己的验证器很困难,因为typing 模块没有提供太多有用的接口。 (事实上​​,typing 模块的内部结构自从它在 python 3.5 中引入以来已经发生了很大的变化,以至于它真的是一场噩梦。)

这是一个类型验证器函数,取自我的一个个人项目(代码警告墙):

import inspect
import typing

__all__ = ['is_instance', 'is_subtype', 'python_type', 'is_generic', 'is_base_generic', 'is_qualified_generic']


if hasattr(typing, '_GenericAlias'):
    # python 3.7
    def _is_generic(cls):
        if isinstance(cls, typing._GenericAlias):
            return True

        if isinstance(cls, typing._SpecialForm):
            return cls not in {typing.Any}

        return False


    def _is_base_generic(cls):
        if isinstance(cls, typing._GenericAlias):
            if cls.__origin__ in {typing.Generic, typing._Protocol}:
                return False

            if isinstance(cls, typing._VariadicGenericAlias):
                return True

            return len(cls.__parameters__) > 0

        if isinstance(cls, typing._SpecialForm):
            return cls._name in {'ClassVar', 'Union', 'Optional'}

        return False


    def _get_base_generic(cls):
        # subclasses of Generic will have their _name set to None, but
        # their __origin__ will point to the base generic
        if cls._name is None:
            return cls.__origin__
        else:
            return getattr(typing, cls._name)


    def _get_python_type(cls):
        """
        Like `python_type`, but only works with `typing` classes.
        """
        return cls.__origin__


    def _get_name(cls):
        return cls._name
else:
    # python <3.7
    if hasattr(typing, '_Union'):
        # python 3.6
        def _is_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing._Union, typing._Optional, typing._ClassVar)):
                return True

            return False


        def _is_base_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing._Union)):
                return cls.__args__ in {None, ()}

            if isinstance(cls, typing._Optional):
                return True

            return False
    else:
        # python 3.5
        def _is_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing.UnionMeta, typing.OptionalMeta, typing.CallableMeta, typing.TupleMeta)):
                return True

            return False


        def _is_base_generic(cls):
            if isinstance(cls, typing.GenericMeta):
                return all(isinstance(arg, typing.TypeVar) for arg in cls.__parameters__)

            if isinstance(cls, typing.UnionMeta):
                return cls.__union_params__ is None

            if isinstance(cls, typing.TupleMeta):
                return cls.__tuple_params__ is None

            if isinstance(cls, typing.CallableMeta):
                return cls.__args__ is None

            if isinstance(cls, typing.OptionalMeta):
                return True

            return False


    def _get_base_generic(cls):
        try:
            return cls.__origin__
        except AttributeError:
            pass

        name = type(cls).__name__
        if not name.endswith('Meta'):
            raise NotImplementedError("Cannot determine base of {}".format(cls))

        name = name[:-4]
        return getattr(typing, name)


    def _get_python_type(cls):
        """
        Like `python_type`, but only works with `typing` classes.
        """
        # Many classes actually reference their corresponding abstract base class from the abc module
        # instead of their builtin variant (i.e. typing.List references MutableSequence instead of list).
        # We're interested in the builtin class (if any), so we'll traverse the MRO and look for it there.
        for typ in cls.mro():
            if typ.__module__ == 'builtins' and typ is not object:
                return typ

        try:
            return cls.__extra__
        except AttributeError:
            pass

        if is_qualified_generic(cls):
            cls = get_base_generic(cls)

        if cls is typing.Tuple:
            return tuple

        raise NotImplementedError("Cannot determine python type of {}".format(cls))


    def _get_name(cls):
        try:
            return cls.__name__
        except AttributeError:
            return type(cls).__name__[1:]


if hasattr(typing.List, '__args__'):
    # python 3.6+
    def _get_subtypes(cls):
        subtypes = cls.__args__

        if get_base_generic(cls) is typing.Callable:
            if len(subtypes) != 2 or subtypes[0] is not ...:
                subtypes = (subtypes[:-1], subtypes[-1])

        return subtypes
else:
    # python 3.5
    def _get_subtypes(cls):
        if isinstance(cls, typing.CallableMeta):
            if cls.__args__ is None:
                return ()

            return cls.__args__, cls.__result__

        for name in ['__parameters__', '__union_params__', '__tuple_params__']:
            try:
                subtypes = getattr(cls, name)
                break
            except AttributeError:
                pass
        else:
            raise NotImplementedError("Cannot extract subtypes from {}".format(cls))

        subtypes = [typ for typ in subtypes if not isinstance(typ, typing.TypeVar)]
        return subtypes


def is_generic(cls):
    """
    Detects any kind of generic, for example `List` or `List[int]`. This includes "special" types like
    Union and Tuple - anything that's subscriptable, basically.
    """
    return _is_generic(cls)


def is_base_generic(cls):
    """
    Detects generic base classes, for example `List` (but not `List[int]`)
    """
    return _is_base_generic(cls)


def is_qualified_generic(cls):
    """
    Detects generics with arguments, for example `List[int]` (but not `List`)
    """
    return is_generic(cls) and not is_base_generic(cls)


def get_base_generic(cls):
    if not is_qualified_generic(cls):
        raise TypeError('{} is not a qualified Generic and thus has no base'.format(cls))

    return _get_base_generic(cls)


def get_subtypes(cls):
    return _get_subtypes(cls)


def _instancecheck_iterable(iterable, type_args):
    if len(type_args) != 1:
        raise TypeError("Generic iterables must have exactly 1 type argument; found {}".format(type_args))

    type_ = type_args[0]
    return all(is_instance(val, type_) for val in iterable)


def _instancecheck_mapping(mapping, type_args):
    return _instancecheck_itemsview(mapping.items(), type_args)


def _instancecheck_itemsview(itemsview, type_args):
    if len(type_args) != 2:
        raise TypeError("Generic mappings must have exactly 2 type arguments; found {}".format(type_args))

    key_type, value_type = type_args
    return all(is_instance(key, key_type) and is_instance(val, value_type) for key, val in itemsview)


def _instancecheck_tuple(tup, type_args):
    if len(tup) != len(type_args):
        return False

    return all(is_instance(val, type_) for val, type_ in zip(tup, type_args))


_ORIGIN_TYPE_CHECKERS = {}
for class_path, check_func in {
                        # iterables
                        'typing.Container': _instancecheck_iterable,
                        'typing.Collection': _instancecheck_iterable,
                        'typing.AbstractSet': _instancecheck_iterable,
                        'typing.MutableSet': _instancecheck_iterable,
                        'typing.Sequence': _instancecheck_iterable,
                        'typing.MutableSequence': _instancecheck_iterable,
                        'typing.ByteString': _instancecheck_iterable,
                        'typing.Deque': _instancecheck_iterable,
                        'typing.List': _instancecheck_iterable,
                        'typing.Set': _instancecheck_iterable,
                        'typing.FrozenSet': _instancecheck_iterable,
                        'typing.KeysView': _instancecheck_iterable,
                        'typing.ValuesView': _instancecheck_iterable,
                        'typing.AsyncIterable': _instancecheck_iterable,

                        # mappings
                        'typing.Mapping': _instancecheck_mapping,
                        'typing.MutableMapping': _instancecheck_mapping,
                        'typing.MappingView': _instancecheck_mapping,
                        'typing.ItemsView': _instancecheck_itemsview,
                        'typing.Dict': _instancecheck_mapping,
                        'typing.DefaultDict': _instancecheck_mapping,
                        'typing.Counter': _instancecheck_mapping,
                        'typing.ChainMap': _instancecheck_mapping,

                        # other
                        'typing.Tuple': _instancecheck_tuple,
                    }.items():
    try:
        cls = eval(class_path)
    except AttributeError:
        continue

    _ORIGIN_TYPE_CHECKERS[cls] = check_func


def _instancecheck_callable(value, type_):
    if not callable(value):
        return False

    if is_base_generic(type_):
        return True

    param_types, ret_type = get_subtypes(type_)
    sig = inspect.signature(value)

    missing_annotations = []

    if param_types is not ...:
        if len(param_types) != len(sig.parameters):
            return False

        # FIXME: add support for TypeVars

        # if any of the existing annotations don't match the type, we'll return False.
        # Then, if any annotations are missing, we'll throw an exception.
        for param, expected_type in zip(sig.parameters.values(), param_types):
            param_type = param.annotation
            if param_type is inspect.Parameter.empty:
                missing_annotations.append(param)
                continue

            if not is_subtype(param_type, expected_type):
                return False

    if sig.return_annotation is inspect.Signature.empty:
        missing_annotations.append('return')
    else:
        if not is_subtype(sig.return_annotation, ret_type):
            return False

    if missing_annotations:
        raise ValueError("Missing annotations: {}".format(missing_annotations))

    return True


def _instancecheck_union(value, type_):
    types = get_subtypes(type_)
    return any(is_instance(value, typ) for typ in types)


def _instancecheck_type(value, type_):
    # if it's not a class, return False
    if not isinstance(value, type):
        return False

    if is_base_generic(type_):
        return True

    type_args = get_subtypes(type_)
    if len(type_args) != 1:
        raise TypeError("Type must have exactly 1 type argument; found {}".format(type_args))

    return is_subtype(value, type_args[0])


_SPECIAL_INSTANCE_CHECKERS = {
    'Union': _instancecheck_union,
    'Callable': _instancecheck_callable,
    'Type': _instancecheck_type,
    'Any': lambda v, t: True,
}


def is_instance(obj, type_):
    if type_.__module__ == 'typing':
        if is_qualified_generic(type_):
            base_generic = get_base_generic(type_)
        else:
            base_generic = type_
        name = _get_name(base_generic)

        try:
            validator = _SPECIAL_INSTANCE_CHECKERS[name]
        except KeyError:
            pass
        else:
            return validator(obj, type_)

    if is_base_generic(type_):
        python_type = _get_python_type(type_)
        return isinstance(obj, python_type)

    if is_qualified_generic(type_):
        python_type = _get_python_type(type_)
        if not isinstance(obj, python_type):
            return False

        base = get_base_generic(type_)
        try:
            validator = _ORIGIN_TYPE_CHECKERS[base]
        except KeyError:
            raise NotImplementedError("Cannot perform isinstance check for type {}".format(type_))

        type_args = get_subtypes(type_)
        return validator(obj, type_args)

    return isinstance(obj, type_)


def is_subtype(sub_type, super_type):
    if not is_generic(sub_type):
        python_super = python_type(super_type)
        return issubclass(sub_type, python_super)

    # at this point we know `sub_type` is a generic
    python_sub = python_type(sub_type)
    python_super = python_type(super_type)
    if not issubclass(python_sub, python_super):
        return False

    # at this point we know that `sub_type`'s base type is a subtype of `super_type`'s base type.
    # If `super_type` isn't qualified, then there's nothing more to do.
    if not is_generic(super_type) or is_base_generic(super_type):
        return True

    # at this point we know that `super_type` is a qualified generic... so if `sub_type` isn't
    # qualified, it can't be a subtype.
    if is_base_generic(sub_type):
        return False

    # at this point we know that both types are qualified generics, so we just have to
    # compare their sub-types.
    sub_args = get_subtypes(sub_type)
    super_args = get_subtypes(super_type)
    return all(is_subtype(sub_arg, super_arg) for sub_arg, super_arg in zip(sub_args, super_args))


def python_type(annotation):
    """
    Given a type annotation or a class as input, returns the corresponding python class.

    Examples:

    ::
        >>> python_type(typing.Dict)
        <class 'dict'>
        >>> python_type(typing.List[int])
        <class 'list'>
        >>> python_type(int)
        <class 'int'>
    """
    try:
        mro = annotation.mro()
    except AttributeError:
        # if it doesn't have an mro method, it must be a weird typing object
        return _get_python_type(annotation)

    if Type in mro:
        return annotation.python_type
    elif annotation.__module__ == 'typing':
        return _get_python_type(annotation)
    else:
        return annotation

演示:

>>> is_instance([{'x': 3}], List[Dict[str, int]])
True
>>> is_instance([{'x': 3}, {'y': 7.5}], List[Dict[str, int]])
False

(据我所知,这支持所有 python 版本,即使是 typing module backport 的版本。)

【讨论】:

  • 谢谢,这是我正在寻找的通用解决方案。
  • 我很想知道这段代码是哪个项目的一部分,为什么你认为你需要它?
  • @toomuchphp 老实说,我不确定我为什么要实现这个怪物。我不认为我曾经使用过它。 ¯\_(ツ)_/¯
  • @toomuchphp 我进行了重大升级。 (我意识到UnionAnyCallable 之类的东西不起作用。)
  • 很棒的小图书馆!在 3.8 中,typing._Protocol 只是 typing.Protocol
【解决方案2】:

虽然没有内置函数,但typeguard 带有一个方便的check_type() 函数,这很尴尬:

>>> from typeguard import check_type
>>> from typing import List
>>> check_type("foo", [1,2,"3"], List[int])
Traceback (most recent call last):
...
TypeError: type of foo[2] must be int; got str instead

type of foo[2] must be int; got str instead

更多信息请见:https://typeguard.readthedocs.io/en/latest/api.html#typeguard.check_type

【讨论】:

    【解决方案3】:

    首先,尽管我认为您知道但为了完整起见,打字库包含用于类型提示的类型。 IDE 使用这些类型提示来检查您的代码是否合理,还可以作为开发人员期望的类型的文档。

    要检查变量是否是某物的类型,我们必须使用isinstance 函数。令人惊奇的是,我们可以使用类型库函数的直接类型,例如。

    from typing import List
    
    value = []
    isinstance(value, List)
    

    但是,对于像List[Dict[str, int]] 这样的嵌套结构,我们不能直接使用它,因为你很有趣会得到一个 TypeError。你要做的是:

    1. 检查初始值是否为列表
    2. 检查列表中的每一项是否都是dict类型
    3. 检查每个 dict 的每个键是否实际上是一个字符串,以及每个值是否实际上是一个 int

    不幸的是,对于严格的检查,python 有点麻烦。但是,请注意,python 使用了鸭子类型:如果它像鸭子并且表现得像鸭子,那么它肯定是鸭子。

    【讨论】:

      【解决方案4】:

      处理此问题的常用方法是利用这样一个事实,即如果您传递给myfun 的任何对象不具备所需的功能,则会引发相应的异常(通常是TypeErrorAttributeError)。因此,您将执行以下操作:

      try:
          myfun(data)
      except (TypeError, AttributeError) as err:
          # Fallback for invalid types here.
      

      您在问题中指出,如果传递的对象没有适当的结构,但 Python 已经为您做到了,您将提出 TypeError。关键问题是你将如何处理这种情况。如果合适,您还可以将try / except 块移动到myfun。在 Python 中键入时,您通常依赖 duck typing:如果对象具有所需的功能,那么您不必太在意它是什么类型,只要它符合目的即可。

      考虑以下示例。我们只需将数据传递给函数,然后免费获得AttributeError(然后我们可以除外);无需手动类型检查:

      >>> def myfun(data):
      ...     for x in data:
      ...             print(x.items())
      ... 
      >>> data = json.loads('[[["a", 1], ["b", 2]], [["c", 3], ["d", 4]]]')
      >>> myfun(data)
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<stdin>", line 3, in myfun
      AttributeError: 'list' object has no attribute 'items'
      

      如果您担心产生的错误的有用性,您仍然可以先除外然后重新引发自定义异常(甚至更改异常的消息):

      try:
          myfun(data)
      except (TypeError, AttributeError) as err:
          raise TypeError('Data has incorrect structure') from err
      
      try:
          myfun(data)
      except (TypeError, AttributeError) as err:
          err.args = ('Data has incorrect structure',)
          raise
      

      使用第三方代码时,应始终检查文档以了解将引发的异常。例如numpy.inner 报告它会在某些情况下引发ValueError。使用该函数时,我们不需要自己执行任何检查,而是依赖于它会在需要时引发错误的事实。当使用第三方代码时,不清楚它在某些极端情况下的行为方式,i.m.o.硬编码相应的类型检查器(见下文)而不是使用适用于任何类型的通用解决方案更容易和更清晰。无论如何,这些情况应该很少见,并且留下相应的评论可以让您的开发人员意识到这种情况。

      typing 库用于类型提示,因此它不会在运行时检查类型。当然,您可以手动执行此操作,但比较麻烦:

      def type_checker(data):
          return (
              isinstance(data, list)
              and all(isinstance(x, dict) for x in list)
              and all(isinstance(k, str) and isinstance(v, int) for x in list for k, v in x.items())
          )
      

      这与适当的注释一起仍然是可接受的解决方案,并且可以在需要类似数据结构的地方重复使用。意图清晰,代码易于验证。

      【讨论】:

        【解决方案5】:

        您必须手动检查您的嵌套类型结构 - 类型提示不是强制的。

        最好使用 ABC(抽象元类) 进行这样的检查 - 这样用户就可以提供支持与默认字典/列表相同的访问的派生类:

        import collections.abc 
        
        def isCorrectType(data):
            if isinstance(data, collections.abc.Collection): 
                for d in data:
                    if isinstance(d,collections.abc.MutableMapping): 
                        for key in d:
                            if isinstance(key,str) and isinstance(d[key],int):
                                pass
                            else:
                                return False
                    else: 
                        return False
            else:
                return False
            return True
        

        输出:

        print ( isCorrectType( [ {"a":2} ] ))       # True
        print ( isCorrectType( [ {2:2} ] ))         # False   
        print ( isCorrectType( [ {"a":"a"} ] ))     # False   
        print ( isCorrectType( [ {"a":2},1 ] ))     # False   
        

        独库:

        相关:


        另一种方法是遵循"Ask forgiveness not permission" - explain 范式并以您想要的形式简单地使用您的数据,如果它不符合您的要求,则使用try:/except:。这更适合What is duck typing? - 并允许(类似于 ABC 检查)消费者为您提供来自 list/dict 的派生类,同时它仍然可以工作......

        【讨论】:

          【解决方案6】:

          如果您只想解析 json,则应该使用 pydantic

          但是,我遇到了同样的问题,我想检查 python 对象的类型,所以我创建了一个比其他答案更简单的解决方案,至少可以处理具有嵌套列表和字典的复杂类型。

          我用这个方法在https://gist.github.com/ramraj07/f537bf9f80b4133c65dd76c958d4c461创建了一个要点

          此方法的一些示例用法包括:

          from typing import List, Dict, Union, Type, Optional
          
          check_type('a', str)
          check_type({'a': 1}, Dict[str, int])
          check_type([{'a': [1.0]}, 'ten'], List[Union[Dict[str, List[float]], str]])
          check_type(None, Optional[str])
          check_type('abc', Optional[str])
          

          以下代码供参考:

          import typing
          
          def check_type(obj: typing.Any, type_to_check: typing.Any, _external=True) -> None:
          
              try:
                  if not hasattr(type_to_check, "_name"):
                      # base-case
                      if not isinstance(obj, type_to_check):
                          raise TypeError
                      return
                  # type_to_check is from typing library
                  type_name = type_to_check._name
          
                  if type_to_check is typing.Any:
                      pass
                  elif type_name in ("List", "Tuple"):
                      if (type_name == "List" and not isinstance(obj, list)) or (
                          type_name == "Tuple" and not isinstance(obj, tuple)
                      ):
                          raise TypeError
          
                      element_type = type_to_check.__args__[0]
                      for element in obj:
                          check_type(element, element_type, _external=False)
                  elif type_name == "Dict":
                      if not isinstance(obj, dict):
                          raise TypeError
                      if len(type_to_check.__args__) != 2:
                          raise NotImplementedError(
                              "check_type can only accept Dict typing with separate annotations for key and values"
                          )
                      key_type, value_type = type_to_check.__args__
                      for key, value in obj.items():
                          check_type(key, key_type, _external=False)
                          check_type(value, value_type, _external=False)
                  elif type_name is None and type_to_check.__origin__ is typing.Union:
                      type_options = type_to_check.__args__
                      no_option_matched = True
                      for type_option in type_options:
                          try:
                              check_type(obj, type_option, _external=False)
                              no_option_matched = False
                              break
                          except TypeError:
                              pass
                      if no_option_matched:
                          raise TypeError
                  else:
                      raise NotImplementedError(
                          f"check_type method currently does not support checking typing of form '{type_name}'"
                      )
          
              except TypeError:
                  if _external:
                      raise TypeError(
                          f"Object {repr(obj)} is of type {_construct_type_description(obj)} "
                          f"when {type_to_check} was expected"
                      )
                  raise TypeError()
          
          
          def _construct_type_description(obj) -> str:
              def get_types_in_iterable(iterable) -> str:
                  types = {_construct_type_description(element) for element in iterable}
                  return types.pop() if len(types) == 1 else f"Union[{','.join(types)}]"
          
              if isinstance(obj, list):
                  return f"List[{get_types_in_iterable(obj)}]"
              elif isinstance(obj, dict):
                  key_types = get_types_in_iterable(obj.keys())
                  val_types = get_types_in_iterable(obj.values())
                  return f"Dict[{key_types}, {val_types}]"
              else:
                  return type(obj).__name__
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2010-12-08
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-05-12
            • 2015-01-22
            相关资源
            最近更新 更多