如何检查值是否与python中的类型匹配?

问题描述 投票:1回答:4

假设我有一个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) 的参数?

typing
python typing
4个回答
1
投票

验证类型注释是一项非常重要的任务。 Python不会自动执行,编写自己的验证器很困难,因为typing模块没有提供太多有用的接口。 (事实上​​,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 模块的内部结构自从在python 3.5中引入以来已经发生了很大的变化,它实际上是一个与之合作的噩梦。)

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

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

示范:

typing module backport

(据我所知,这支持所有python版本,甚至是使用isinstance的<3.5版本。)


1
投票

首先,即使我认为你知道但是为了完整性,输入库包含类型提示的类型。 IDE使用这些类型提示来检查您的代码是否有点理智,并且还可以作为开发人员期望的类型的文档。

要检查变量是否是某种类型的东西,我们必须使用from typing import List value = [] isinstance(value, List) 函数。令人惊讶的是,我们可以使用直接类型的输入库函数,例如。

List[Dict[str, int]]

但是,对于像myfun这样的嵌套结构,我们不能直接使用它,因为你足够有趣得到一个TypeError。你要做的是:

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

不幸的是,对于严格检查python有点麻烦。但是,要注意python使用鸭子打字:如果它像鸭子一样,表现得像鸭子,那么它绝对是一只鸭子。


1
投票

处理这个问题的常用方法是利用这样一个事实:如果传递给TypeError的任何对象没有所需的功能,则会引发相应的异常(通常是AttributeErrortry: myfun(data) except (TypeError, AttributeError) as err: # Fallback for invalid types here. )。所以你会做以下事情:

TypeError

你在问题中指出如果传递的对象没有适当的结构你会提出一个try / except但是Python已经为你做了这个。关键问题是如何处理这种情况。如果合适的话,你也可以将myfun块移动到duck typing。当你在Python中输入时,你通常依赖于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报告说它会在某些情况下提高typing。使用该功能时,我们不需要自己执行任何检查,但依赖于在需要时会引发错误的事实。当使用第三方代码时,不清楚它在某些角落情况下的表现如何,i.m.o。只需硬编码相应的类型检查器(见下文),而不是使用适用于任何类型的通用解决方案,这样更容易,更清晰。无论如何这些情况应该是罕见的,并留下相应的评论使您的开发人员了解情况。

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()) ) 库用于类型提示,因此它不会在运行时检查类型。当然你可以手动完成,但它相当麻烦:

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

这与适当的评论一起仍然是可接受的解决方案,并且在需要类似数据结构的情况下可重复使用。意图很明确,代码很容易验证。


0
投票

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

像这样检查最好使用ABC(抽象元类) - 因此用户可以提供支持与默认dict / lists相同访问的派生类:

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

输出:

ABC - abstract meta classes

质地:

有关:


另一种方式是遵循try:/except:范式,并以你想要的形式使用你的数据,并且如果它不符合你想要的那样,就可以使用What is duck typing?。这更适合qazxswpoi - 并且允许(类似于ABC检查)消费者为你提供list / dict的派生类,同时它仍然可以工作......

© www.soinside.com 2019 - 2024. All rights reserved.