【问题标题】:How to elegantly check if a dictionary has a given structure?如何优雅地检查字典是否具有给定的结构?
【发布时间】:2017-10-04 21:28:22
【问题描述】:

我有一本结构如下的字典:

D = {
   'rows': 11,
   'cols': 13,
   (i, j): {
              'meta': 'random string',
              'walls': {
                  'E': True,
                  'O': False,
                  'N': True,
                  'S': True
              }
           }
}
# i ranging in {0 .. D['rows']-1}
# j ranging in {0 .. D['cols']-1}

我想编写一个将任意对象作为参数并检查它是否具有该结构的函数。这是我写的:

def well_formed(L):
    if type(L) != dict:
        return False
    if 'rows' not in L:
        return False
    if 'cols' not in L:
        return False

    nr, nc = L['rows'], L['cols']

    # I should also check the int-ness of nr and nc ...

    if len(L) != nr*nc + 2:
        return False

    for i in range(nr):
        for j in range(nc):
            if not ((i, j) in L
                and 'meta' in L[i, j]
                and  'walls' in L[i, j]
                and type(L[i, j]['meta']) == str
                and type(L[i, j]['walls'])  == dict
                and     'E' in L[i, j]['walls']
                and     'N' in L[i, j]['walls']
                and     'O' in L[i, j]['walls']
                and     'S' in L[i, j]['walls']
                and type(L[i, j]['walls']['E']) == bool
                and type(L[i, j]['walls']['N']) == bool
                and type(L[i, j]['walls']['O']) == bool
                and type(L[i, j]['walls']['S']) == bool):
                return False

    return True

虽然它有效,但我一点也不喜欢它。有没有 Pythonic 的方式来做到这一点?

我只能使用标准库。

【问题讨论】:

  • 我会考虑为您的字典写一个JSON Schema,然后使用Python module 来验证它。
  • 我认为引发异常是一个更好的主意,只返回真或假,因为它在这里完成了许多测试。可能更容易知道为什么 dict 没有通过测试
  • @tzaman 将该字典转换为 JSON 会很棘手,因为其中一个键是元组,而 JSON 仅本机支持字符串作为对象键。您必须将元组编码为字符串,这可能会产生歧义(例如,如果被检查的对象具有与您用于元组的格式相同的字符串键),我认为这是无效的,但是 JSON除了编码为字符串的有效元组之外,验证器无法分辨它。
  • 查看相关问题。 stackoverflow.com/q/6011881/7724457
  • 在比较类型时至少使用is而不是==

标签: python dictionary


【解决方案1】:

如果你使用这样的东西:

def get_deep_keys(d, depth=2):
    """Gets a representation of all dictionary keys to a set depth.

    If a (sub)dictionary contains all non-dictionary values, a list of keys
    will be returned.

    If a dictionary contains a mix of types, a dictionary of dicts/lists/types
    will be returned.
    """
    if isinstance(d, dict):
        if depth > 0 and any(isinstance(v, dict) for v in d.values()):
            return {k: get_deep_keys(v, depth=depth - 1) for k, v in d.items()}
        else:
            return set(d.keys())

    else:
        return type(d)

那么你可以这样做:

assert get_deep_keys(D[i, j]) == {
    'meta': str, 'walls': {'E', 'N', 'O', 'S'}}

i, j 的循环内。这很容易修改为返回底层元素的类型:

def get_deep_keys(d, depth=2):
    """Gets a representation of all dictionary keys to a set depth, with types.
    """
    if isinstance(d, dict) and depth > 0:
        return {k: get_deep_keys(v, depth=depth - 1) for k, v in d.items()}

    return type(d)


get_deep_keys(D)
 # {'meta': str, 'walls': {'E': bool, 'O': bool, 'N': bool, 'S': bool}}

【讨论】:

    【解决方案2】:

    您可以像这样编写验证(来自 Scala 提取器的想法)。优点是验证器结构类似于要测试的结构。

    一个缺点是许多函数调用可能会使其速度变慢。

    class Mapping:
        def __init__(self, **kwargs):
            self.key_values = [KeyValue(k, v) for k, v in kwargs.items()]
    
        def validate(self, to_validate):
            if not isinstance(to_validate, dict):
                return False
    
            for validator in self.key_values:
                if not validator.validate(to_validate):
                    return False
            return True
    
    
    class KeyValue:
        def __init__(self, key, value):
            self.key = key
            self.value = value
    
        def validate(self, to_validate):
            return self.key in to_validate and self.value.validate(to_validate[self.key])
    
    
    class Boolean:
        def validate(self, to_validate):
            return isinstance(to_validate, bool)
    
    
    class Integer:
        def validate(self, to_validate):
            return isinstance(to_validate, int)
    
    
    class String:
        def validate(self, to_validate):
            return isinstance(to_validate, str)
    
    
    class CustomValidator:
        def validate(self, to_validate):
            if not Mapping(rows=Integer(), cols=Integer()).validate(to_validate):
                return False
            element_validator = Mapping(meta=String(), walls=Mapping(**{k: Boolean() for k in "EONS"}))
            for i in range(to_validate['rows']):
                for j in range(to_validate['cols']):
                    if not KeyValue((i, j), element_validator).validate(to_validate):
                        return False
            return True
    
    
    d = {
        'rows': 11,
        'cols': 13,
    }
    d.update({(i, j): {
        'meta': 'random string',
        'walls': {
            'E': True,
            'O': False,
            'N': True,
            'S': True
        }
    } for i in range(11) for j in range(13)})
    
    assert CustomValidator().validate(d)
    

    与覆盖 isinstance 相同(使用 Python 3.5 测试)

    class IsInstanceCustomMeta(type):
        def __instancecheck__(self, instance):
            return self.validate(instance)
    
    def create_custom_isinstance_class(f):
        class IsInstanceCustomClass(metaclass=IsInstanceCustomMeta):
            validate = f
        return IsInstanceCustomClass
    
    def Mapping(**kwargs):
        key_values = [KeyValue(k, v) for k, v in kwargs.items()]
    
        def validate(to_validate):
            if not isinstance(to_validate, dict):
                return False
    
            for validator in key_values:
                if not isinstance(to_validate, validator):
                    return False
            return True
    
        return create_custom_isinstance_class(validate)
    
    def KeyValue(key, value):
        return create_custom_isinstance_class(lambda to_validate: key in to_validate and isinstance(to_validate[key], value))
    
    def my_format_validation(to_validate):
        if not isinstance(to_validate, Mapping(rows=int, cols=int)):
            return False
        element_validator = Mapping(meta=str, walls=Mapping(**{k: bool for k in "EONS"}))
        for i in range(to_validate['rows']):
            for j in range(to_validate['cols']):
                if not isinstance(to_validate, KeyValue((i, j), element_validator)):
                    return False
        return True
    
    MyFormat = create_custom_isinstance_class(my_format_validation)
    
    d = {
        'rows': 11,
        'cols': 13,
    }
    d.update({(i, j): {
        'meta': 'random string',
        'walls': {
            'E': True,
            'O': False,
            'N': True,
            'S': True
        }
    } for i in range(11) for j in range(13)})
    
    assert isinstance(d, MyFormat)
    

    【讨论】:

      【解决方案3】:
      from itertools import product
      
      def isvalid(d):
          try:
              for key in product(range(d['rows']), range(d['cols'])):
                  sub = d[key]
                  assert (isinstance(sub['meta'], str) and
                          all(isinstance(sub['walls'][c], bool)
                              for c in 'EONS'))
          except (KeyError, TypeError, AssertionError):
              return False
          return True
      

      如果 Python 2 兼容性很重要,或者有必要断言没有额外的键,请告诉我。

      【讨论】:

        【解决方案4】:

        在 Python 中,所涉及类型的确切标识不如值的行为方式重要。对于这样一个对象的定义使用,这个对象就足够了吗?这意味着L 不必是dict,它只是支持__getitem__L[(i,j)]['meta'] 不一定是str,它只需要通过str(L[(i,j)]['meta']) 支持转换 到字符串;等等

        鉴于这种放松,我会简单地尝试捕获尝试此类操作所引发的任何错误,并在发生任何错误时返回 False。例如,

        def well_formed(L):
            try:
                nr = L['rows']
                nc = L['cols']
            except KeyError:
                return False
        
            try:
                for i in range(nr):
                    for j in range(nc):
                        str(L[(i,j)]['meta'])
                        walls = L[(i,j)]['walls']
                        for direction in walls:
                            # Necessary?
                            if direction not in "ENOS":
                                return False
                            if walls[direction] not in (True, False):
                                return False
            except KeyError:
                return False
        
            return True
        

        鉴于 any 对象具有布尔值,尝试bool(walls[direction]) 似乎毫无意义;相反,如果准确地将TrueFalse 作为一个值不是硬性要求,那么我将完全对该值进行任何测试。同样,额外的墙可能是也可能不是问题,并且不必明确测试。

        【讨论】:

        • 为什么要两个 try 块而不是尝试一切?
        • 说实话,我不确定。我尝试了很多不同的方法,我认为前两个检查在他们自己的try 语句中是有道理的。在这里看到的版本,不是那么多。
        • 我不同意你关于str() 的观点。首先,一切都支持str(),就像一切都支持bool()。其次,如果该值不是字符串,则表明 (1) 生成 dict 的代码可能有错误,并且 (2) 使用 dict 的代码可能需要知道如何处理字符串和非字符串值。跨度>
        【解决方案5】:

        如果您的格式更简单,我会同意其他答案/cmets 使用现有的架构验证库,例如 schemavoluptuous。但是,鉴于您必须使用元组键检查字典的特定情况,并且这些元组的值取决于您的字典其他成员的值,我认为您最好编写自己的验证器而不是试图哄骗模式以适合您的格式。

        【讨论】:

          【解决方案6】:

          首先,我认为更“Pythonic”的事情可能是请求宽恕而不是许可 - 检查何时需要属性,数据结构是否具有该属性。

          但另一方面,如果您被要求创建一些东西来检查它的格式是否正确,那也无济于事。 :)

          因此,如果您需要检查,可以使用 the schema library 之类的东西来定义您的数据结构应该是什么样子,然后根据该架构检查其他数据结构。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2019-11-24
            • 2014-09-05
            • 1970-01-01
            • 2019-07-01
            • 2011-10-14
            • 2015-11-03
            • 1970-01-01
            • 2016-04-29
            相关资源
            最近更新 更多