【问题标题】:How to validate structure (or schema) of dictionary in Python?如何在 Python 中验证字典的结构(或模式)?
【发布时间】:2018-01-30 10:19:42
【问题描述】:

我有一本包含配置信息的字典:

my_conf = {
    'version': 1,

    'info': {
        'conf_one': 2.5,
        'conf_two': 'foo',
        'conf_three': False,
        'optional_conf': 'bar'
    }
}

我想检查字典是否符合我需要的结构。

我正在寻找这样的东西:

conf_structure = {
    'version': int,

    'info': {
        'conf_one': float,
        'conf_two': str,
        'conf_three': bool
    }
}

is_ok = check_structure(conf_structure, my_conf)

是否有任何解决方案或任何库可以使实现check_structure 更容易?

【问题讨论】:

标签: python validation dictionary schema config


【解决方案1】:

您可以使用schema (PyPi Link)

schema 是一个用于验证 Python 数据结构的库,例如从配置文件、表单、外部服务或命令行解析获得的数据结构,从 JSON/YAML(或其他)转换为Python 数据类型。

from schema import Schema, And, Use, Optional, SchemaError

def check(conf_schema, conf):
    try:
        conf_schema.validate(conf)
        return True
    except SchemaError:
        return False

conf_schema = Schema({
    'version': And(Use(int)),
    'info': {
        'conf_one': And(Use(float)),
        'conf_two': And(Use(str)),
        'conf_three': And(Use(bool)),
        Optional('optional_conf'): And(Use(str))
    }
})

conf = {
    'version': 1,
    'info': {
        'conf_one': 2.5,
        'conf_two': 'foo',
        'conf_three': False,
        'optional_conf': 'bar'
    }
}

print(check(conf_schema, conf))

【讨论】:

  • 看起来很棒!谢谢:)
  • 这是来自文档的复制粘贴。这究竟对OP有什么帮助?你能提供一个具体的例子来说明如何吗?这并不比目前的仅链接答案好多少。
  • 你好。一个问题!如果有一个清单呢?例如,如果“信息”字段有一个字段“更改”,它是一个列表并且可以有 0 个或更多元素?架构会是什么样子?
  • 警告:conf_schema.validate(conf) 如果设法将给定类型转换为正确类型,则不会抛出错误! (表达式将返回给定字典的更正版本。)
【解决方案2】:

不使用库,你也可以像这样定义一个简单的递归函数:

def check_structure(struct, conf):
    if isinstance(struct, dict) and isinstance(conf, dict):
        # struct is a dict of types or other dicts
        return all(k in conf and check_structure(struct[k], conf[k]) for k in struct)
    if isinstance(struct, list) and isinstance(conf, list):
        # struct is list in the form [type or dict]
        return all(check_structure(struct[0], c) for c in conf)
    elif isinstance(struct, type):
        # struct is the type of conf
        return isinstance(conf, struct)
    else:
        # struct is neither a dict, nor list, not type
        return False

这假定配置可以具有不在您的结构中的键,如您的示例中所示。


更新:新版本还支持列表,例如喜欢'foo': [{'bar': int}]

【讨论】:

  • 我认为elif isinstance(struct, type):这一行应该改为isinstance(conf, type):
【解决方案3】:

对未来的建议:使用 Pydantic!

Pydantic 在运行时强制执行类型提示,并在数据无效时提供用户友好的错误。定义数据应该如何在纯粹的、规范的 python 中;用pydantic验证它,就这么简单:

from pydantic import BaseModel


class Info(BaseModel):
    conf_one: float
    conf_two: str
    conf_three: bool

    class Config:
        extra = 'forbid'


class ConfStructure(BaseModel):
    version: int
    info: Info

如果验证失败,pydantic 将引发错误并详细说明错误:

my_conf_wrong = {
    'version': 1,

    'info': {
        'conf_one': 2.5,
        'conf_two': 'foo',
        'conf_three': False,
        'optional_conf': 'bar'
    }
}

my_conf_right = {
    'version': 10,

    'info': {
        'conf_one': 14.5,
        'conf_two': 'something',
        'conf_three': False
    }
}

model = ConfStructure(**my_conf_right)
print(model.dict())
# {'version': 10, 'info': {'conf_one': 14.5, 'conf_two': 'something', 'conf_three': False}}

res = ConfStructure(**my_conf_wrong)
# pydantic.error_wrappers.ValidationError: 1 validation error for ConfStructure
#     info -> optional_conf
# extra fields not permitted (type=value_error.extra)

【讨论】:

    【解决方案4】:

    您可以使用递归构建结构:

    def get_type(value):
        if isinstance(value, dict):
            return {key: get_type(value[key]) for key in value}
        else:
            return str(type(value))
    

    然后将所需的结构与您的字典进行比较:

    get_type(current_conf) == get_type(required_conf)
    

    例子:

    required_conf = {
        'version': 1,
        'info': {
            'conf_one': 2.5,
            'conf_two': 'foo',
            'conf_three': False,
            'optional_conf': 'bar'
        }
    }
    
    get_type(required_conf)
    
    {'info': {'conf_two': "<type 'str'>", 'conf_one': "<type 'float'>", 'optional_conf': "<type 'str'>", 'conf_three': "<type 'bool'>"}, 'version': "<type 'int'>"}
    

    【讨论】:

      【解决方案5】:

      看起来dict-schema-validator 包完全符合您的需要:

      这是一个代表客户的简单模式:

      {
        "_id":          "ObjectId",
        "created":      "date",
        "is_active":    "bool",
        "fullname":     "string",
        "age":          ["int", "null"],
        "contact": {
          "phone":      "string",
          "email":      "string"
        },
        "cards": [{
          "type":       "string",
          "expires":    "date"
        }]
      }
      

      验证:

      from datetime import datetime
      import json
      from dict_schema_validator import validator
      
      
      with open('models/customer.json', 'r') as j:
          schema = json.loads(j.read())
      
      customer = {
          "_id":          123,
          "created":      datetime.now(),
          "is_active":    True,
          "fullname":     "Jorge York",
          "age":          32,
          "contact": {
              "phone":    "559-940-1435",
              "email":    "york@example.com",
              "skype":    "j.york123"
          },
          "cards": [
              {"type": "visa", "expires": "12/2029"},
              {"type": "visa"},
          ]
      }
      
      errors = validator.validate(schema, customer)
      for err in errors:
          print(err['msg'])
      

      输出:

      [*] "_id" has wrong type. Expected: "ObjectId", found: "int"
      [+] Extra field: "contact.skype" having type: "str"
      [*] "cards[0].expires" has wrong type. Expected: "date", found: "str"
      [-] Missing field: "cards[1].expires"
      

      【讨论】:

        【解决方案6】:

        您也可以使用dataclasses_json 库。这是我通常的做法

        from dataclasses import dataclass
        from dataclasses_json import dataclass_json, Undefined
        from dataclasses_json.undefined import UndefinedParameterError
        from typing import Optional
        
        
        #### define schema #######
        @dataclass_json(undefined=Undefined.RAISE)
        @dataclass
        class Info:
          conf_one: float
          # conf_two: str
          conf_three: bool
          optional_conf: Optional[str]
        
        @dataclass_json
        @dataclass
        class ConfStructure:
          version: int
          info: Info
        
        ####### test for compliance####
        try:
          ConfStructure.from_dict(my_conf).to_dict()
        except KeyError as e:
          print('theres a missing parameter')
        except UndefinedParameterError as e:
          print('extra parameters')
        
        
        

        【讨论】:

          【解决方案7】:

          您可以从https://pypi.org/project/dictify/ 使用dictify

          在此处阅读文档https://dictify.readthedocs.io/en/latest/index.html

          这是可以做到的。

          from dictify import Field, Model
          
          class Info(Model):
              conf_one = Field(required=True).instance(float)
              conf_two = Field(required=True).instance(str)
              conf_three = Field(required=True).instance(bool)
              optional_conf = Field().instance(str)
          
          class MyConf(Model):
              version = Field(required=True).instance(int)
              info = Field().model(Info)
          
          my_conf = MyConf() # Invalid without required fields
          
          # Valid
          my_conf = MyConf({
              'version': 1,
              'info': {
                  'conf_one': 2.5,
                  'conf_two': 'foo',
                  'conf_three': False,
                  'optional_conf': 'bar'
              }
          })
          
          my_conf['info']['conf_one'] = 'hi' # Invalid, won't be assinged
          

          【讨论】:

            【解决方案8】:

            @tobias_k 击败了我(可能在时间和质量上),但这里有另一个递归函数,可能对你(和我)来说更容易理解:

            def check_dict(my_dict, check_against):
                for k, v in check_against.items():
                    if isinstance(v, dict):
                        return check_dict(my_dict[k], v)
                    else:
                        if not isinstance(my_dict[k], v):
                            return False
                return True
            

            【讨论】:

              【解决方案9】:

              字典的本质是,如果它们在 python 中使用并且不导出为某些 JSON,则不需要设置字典的顺序。相反,查找键会返回值(因此是字典)。

              在任何一种情况下,这些函数都应该为您提供在您提供的示例中存在的嵌套级别所需的内容。

              #assuming identical order of keys is required
              
              def check_structure(conf_structure,my_conf):
                  if my_conf.keys() != conf_structure.keys():
                      return False
              
                  for key in my_conf.keys():
                      if type(my_conf[key]) == dict:
                          if my_conf[key].keys() != conf_structure[key].keys():
                              return False
              
                  return True
              
              #assuming identical order of keys is not required
              
              def check_structure(conf_structure,my_conf):
                  if sorted(my_conf.keys()) != sorted(conf_structure.keys()):
                      return False
              
                  for key in my_conf.keys():
                      if type(my_conf[key]) != dict:
                          return False
                      else:
                          if sorted(my_conf[key].keys()) != sorted(conf_structure[key].keys()):
                              return False
              
                  return True
              

              如果嵌套级别更高,则显然需要更改此解决方案(即,它被配置为评估具有某些值作为字典的字典的结构相似性,但不评估具有某些值的字典的字典,这些后面的字典也是字典)。

              【讨论】:

                【解决方案10】:

                有一个验证 JSON 文件的标准称为JSON Schema

                验证器已在 many languages 中实现,包括 Python。另请阅读documentation 了解更多详情。在下面的示例中,我将使用我熟悉的 Python 包 jsonschema (docs)。


                给定配置数据

                my_conf = {
                    'version': 1,
                    'info': {
                        'conf_one': 2.5,
                        'conf_two': 'foo',
                        'conf_three': False,
                        'optional_conf': 'bar',
                    },
                }
                

                以及相应的配置架构

                conf_structure = {
                    'type': 'object',
                    'properties': {
                        'version': {'type': 'integer'},
                        'info': {
                            'type': 'object',
                            'properties': {
                                'conf_one': {'type': 'number'},
                                'conf_two': {'type': 'string'},
                                'conf_three': {'type': 'boolean'},
                                'optional_conf': {'type': 'string'},
                            },
                            'required': ['conf_one', 'conf_two', 'conf_three'],
                        },
                    },
                }
                

                验证这些数据的实际代码就这么简单:

                import jsonschema
                
                jsonschema.validate(my_conf, schema=conf_structure)
                

                这种方法的一大优势是您可以将数据和模式存储为 JSON 格式的文件。

                【讨论】:

                  猜你喜欢
                  • 2011-08-26
                  • 2020-06-11
                  • 2019-08-05
                  • 2021-09-05
                  • 1970-01-01
                  • 1970-01-01
                  • 2023-02-09
                  • 2012-11-28
                  • 2011-12-07
                  相关资源
                  最近更新 更多