本文是Python第2部分2020年出现日历,第16天。
类型提示是在Python 3.5中引入的,现在,即使在最初是动态类型化语言的Python中,也通常在代码中写入类型信息。
这次,我将介绍
什么是pydantic?
由于最近流行的Python网络框架FastAPI中也使用了它,因此许多人可能知道它的存在。
实际上,当我第一次使用Fast API时,我也了解了
-
提供运行时类型信息
- 返回不正确数据的用户友好错误
很多人说这就是他们所需要的。
在此之后,我将使用一个示例进行说明。
官方资源
GitHub:samuelcolvin / pydantic:使用Python类型提示进行数据解析和验证
官方文档:pydantic
示例
首先,让我们考虑一个不使用
使用
1 2 3 4 5 6 7 | from dataclasses import dataclass, field from typing import Optional @dataclass class NonPydanticUser: name: str age: int |
让我们创建这个
在此示例中,两个字段
它保存类中定义的数据类型。
1 2 3 4 5 6 7 | Ichiro = NonPydanticUser(name="Ichiro", age=19) print(Ichiro) #> NonPydanticUser(name='Ichiro', age=19) print(type(Ichiro.name)) #> <class 'str'> print(type(Ichiro.age)) #> <class 'int'> |
让我们创建另一个实例。
1 2 3 4 5 6 7 | Samatoki = NonPydanticUser(name="Samatoki", age="25") print(Samatoki) #> NonPydanticUser(name='Samatoki', age='25') print(type(Samatoki.name)) #> <class 'str'> print(type(Samatoki.age)) #> <class 'str'> |
在此示例中,
不会抛出诸如
您可以再次看到类型注释提供的类型信息仅在编码时有效。
当然,如果使用
另一方面,使用
1 2 3 4 5 | from pydantic import BaseModel class User(BaseModel): name: str age: int |
乍一看,它类似于使用
但是有明显的区别。
首先,让我们使用常规字段值创建一个实例。
1 2 3 4 5 6 7 | Ramuda = User(name="Ramuda", age=24) print(Ramuda) #> name='Ramuda' age=24 print(type(Ramuda.name)) #> <class 'str'> print(type(Ramuda.age)) #> <class 'int'> |
仅凭这一点您无法真正分辨出区别。
接下来,给
1 2 3 4 5 6 | Jakurai = User(name="Jakurai", age="35") #> name='Jakurai' age=35 print(type(Jakurai.name)) #> <class 'str'> print(type(Jakurai.age)) #> <class 'int'> |
顺便说一句,如果给
1 2 3 4 | Sasara = User(name="Sasara", age="ホンマか?") #> ValidationError: 1 validation error for User #> age #> value is not a valid integer (type=type_error.integer) |
引发了
即使未特别执行验证,我仍检测到无效值。
以这种方式使用
pydantic推荐给这样的人! !!
- 我想尽可能地省略简单的验证
- 从Go,TypeScript和Swift等类型严格的语言进入Python并希望关心Python中的类型的人
- 任何想要编写健壮代码的人
- 任何想要被绑在模具上的人
pydantic基本知识
我将使用官方示例的以下代码进行基本说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | from datetime import datetime from typing import List, Optional from pydantic import BaseModel class User(BaseModel): id: int name = 'John Doe' signup_ts: Optional[datetime] = None friends: List[int] = [] external_data = { 'id': '123', 'signup_ts': '2019-06-01 12:22', 'friends': [1, 2, '3'], } user = User(**external_data) print(user.id) #> 123 print(repr(user.signup_ts)) #> datetime.datetime(2019, 6, 1, 12, 22) print(user.friends) #> [1, 2, 3] print(user.dict()) """ { 'id': 123, 'signup_ts': datetime.datetime(2019, 6, 1, 12, 22), 'friends': [1, 2, 3], 'name': 'John Doe', } """ |
继承基类
在此类定义中,定义了四个字段:
每个字段都有不同的描述。根据文档,它具有以下含义。
-
id (int )...如果仅声明了类型提示,则它将是必填字段。如果在实例化时给出类型为str ,bytes ,float 的值,则将其强制转换为int 。如果给出任何其他数据类型(dict ,list 等)的值,则将引发异常。 -
从默认值
name (str )...John Doe ,可以推断name 为str 类型。另外,name 不是必需字段,因为声明了默认值。 -
signup_ts :(datetime ,可选)...允许None 的datetime 类型。另外,sign_up 不是必需字段,因为声明了默认值。可以将类型为int 的UNIX时间戳(例如1608076800.0)或表示日期和时间的类型为str 的字符串作为参数。 -
friends :(List[int] )...使用Python的内置键入系统。另外,由于声明了默认值,因此它不是必填字段。与id 一样,"123" 和"45" 也将转换为int 类型。
我提到过,如果您在实例化继承
让我们使用以下代码在
1 2 3 4 5 6 | from pydantic import ValidationError try: User(signup_ts='broken', friends=[1, 2, 'not number']) except ValidationError as e: print(e.json()) |
此代码的
您可以看到每个字段中发生了什么样的不一致。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | [ { "loc": [ "id" ], "msg": "field required", "type": "value_error.missing" }, { "loc": [ "signup_ts" ], "msg": "invalid datetime format", "type": "value_error.datetime" }, { "loc": [ "friends", 2 ], "msg": "value is not a valid integer", "type": "type_error.integer" } ] |
贴士
仅本文不能介绍所有的pydantic,但是从现在开始,我想介绍可以立即使用的元素。
栏位类型
有许多支持
这里是其中的一些。
标准库类型
当然,您可以使用原始数据类型,例如
它还支持诸如
以下是使用
1 2 3 4 5 6 7 8 9 10 11 12 | from pydantic import BaseModel from ipaddress import IPv4Address class IPNode(BaseModel): address: IPv4Address client = IPNode(address="192.168.0.12") srv = IPNode(address="hoge") #> ValidationError: 1 validation error for IPNode #> address #> value is not a valid IPv4 address (type=value_error.ipv4address) |
网址
1 2 3 4 5 6 7 8 9 10 11 | from pydantic import BaseModel, HttpUrl, AnyUrl class Backend(BaseModel): url: HttpUrl bd1 = Backend(url="https://example.com") bd2 = Backend(url="file://hogehoge") #> ValidationError: 1 validation error for Backend #> url #> URL scheme not permitted (type=value_error.url.scheme; allowed_schemes={'https', 'http'}) |
秘密类型
您还可以处理不想输出到输出的信息,例如日志。
例如,您可以使用
1 2 3 4 5 6 7 8 | from pydantic import BaseModel, SecretStr class Password(BaseModel): value: SecretStr p1 = Password(value="hogehogehoge") print(p1.value) #> ********** |
电子邮件Str
一种可以处理电子邮件地址的类型。
但是,要使用它,您需要与
让我们在上一节中使用此
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | from pydantic import BaseModel, EmailStr, SecretStr, Field class User(BaseModel): email: EmailStr password: SecretStr = Field(min_length=8, max_length=16) # OK Juto = User(email="[email protected]", password="hogehogehoge") print(Juto) #> email='[email protected]' password=SecretStr('**********') # NG, emailがメールアドレスのフォーマットになっていない Rio = User(email="rio", password="hogehogehogehoge") #> ValidationError: 1 validation error for User #> value is not a valid email address (type=value_error.email) # NG, passwordの文字数が16文字を越えている Gentaro = User(email="[email protected]", password="hogehogehogehogehoge") #> ValidationError: 1 validation error for User #> password #> ensure this value has at most 16 characters (type=value_error.any_str.max_length; limit_value=16) # NG, passwordの文字数が8文字未満である Daisu = User(email="[email protected]", password="hoge") #> ValidationError: 1 validation error for User #> password #> ensure this value has at least 8 characters (type=value_error.any_str.min_length; limit_value=8) |
约束类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from pydantic import BaseModel, HttpUrl, AnyUrl, SecretStr, conint # 正の数だけ許容する様にしてみる class PositiveNumber(BaseModel): value: conint(gt=0) # OK n1 = PositiveNumber(value=334) #NG, 負の数である n2 = PositiveNumber(value=-100) #> ValidationError: 1 validation error for PositiveNumber #> value #> ensure this value is greater than 0 (type=value_error.number.not_gt; limit_value=0) |
严格类型
在本文开头的示例中,我很感激将
您还可以声明更严格的字段,甚至不允许进行此转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from pydantic import BaseModel, conint, StrictInt # キャストを認めないint class StrictNumber(BaseModel): value: StrictInt # OK n1 = StrictNumber(value=4) # キャストしてint型になれるstr型であっても、int型ではないのでNG n2 = StrictNumber(value="4") #> ValidationError: 1 validation error for StrictNumber #> value #> value is not a valid integer (type=type_error.integer) |
它也可以与上一节中的约束类型组合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | from pydantic import BaseModel conint # 自然数だけ許容する class NaturalNumber(BaseModel): value: conint(strict=True, gt=0) # OK n1 = NaturalNumber(value=334) # NG, 負の数である n2 = NaturalNumber(value=-45) #> ValidationError: 1 validation error for NaturalNumber #> value #> ensure this value is greater than 0 (type=value_error.number.not_gt; limit_value=0) # キャストしてint型になれるstr型であっても、int型ではないのでNG n3 = NaturalNumber(value="45") #> ValidationError: 1 validation error for NaturalNumber #> value #> value is not a valid integer (type=type_error.integer) # float型も許容されない n4 = NaturalNumber(value=123.4) #> ValidationError: 1 validation error for NaturalNumber #> value #> value is not a valid integer (type=type_error.integer) |
验证者
可以在字段声明时编写简单的验证,但是可以使用
基本验证器
考虑一个简单的例子。
定义仅当
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | from pydantic import BaseModel, validator # nameに半角スペースが含まれていない場合を許容しない class User(BaseModel): name: str age: int @validator("name") def validate_name(cls, v): if ' ' not in v: raise ValueError("must contain a space") return v # OK Jiro = User(name="山田 二郎", age=17) # NG Saburo = User(name="山田三郎", age=14) #> ValidationError: 1 validation error for User #> name #> must contain a space (type=value_error) |
实现具有多个字段的验证器
例如,考虑一个
1 2 3 4 5 6 7 8 | from datetime import datetime from pydantic import BaseModel class Event(BaseModel): begin: datetime end: datetime event = Event(begin="2020-12-16T09:00:00+09:00", end="2020-12-16T12:00:00+09:00") |
这时,我想保证分配给
如果
我认为有几种方法可以做到。我想介绍两个。
第一种方法是使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | from datetime import datetime from pydantic import BaseModel, root_validator class Event(BaseModel): begin: datetime end: datetime @root_validator(pre=True) def validate_event_schedule(cls, values): _begin: datetime = values["begin"] _end: datetime = values["end"] if _begin >= _end: raise ValueError("Invalid event.") return values # OK event1 = Event(begin="2020-12-16T09:00:00+09:00", end="2020-12-16T12:00:00+09:00") # NG event2 = Event(begin="2020-12-16T12:00:00+09:00", end="2020-12-16T09:00:00+09:00") #> ValidationError: 1 validation error for Event #> __root__ #> Invalid event. (type=value_error) # NG event3 = Event(begin="2020-12-16T12:00:00+09:00", end="2020-12-16T12:00:00+09:00") #> ValidationError: 1 validation error for Event #> __root__ #> Invalid event. (type=value_error) |
另一个使用
我将首先介绍代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from datetime import datetime from pydantic import BaseModel, root_validator, validator class Event(BaseModel): begin: datetime end: datetime @validator("begin", pre=True) def validate_begin(cls, v): return v @validator("end") def validate_end(cls, v, values): if values["begin"] >= v: raise ValueError("Invalid schedule.") return v |
此代码定义了两个
实例化此
然后处理
但是,与
作为
换句话说,在此代码的情况下,每个字段的输入值检查的顺序如下。
我们介绍了以上两种方法。请让我知道是否有更好的方法。
对list ,dict ,Set 等中包含的每个元素的Validator
。
考虑一个满足以下规范的
-
它具有
List[int] 类型字段scores ,该字段恰好存储10个测试分数(int 类型)。 - 每个测试结果必须至少为50分。
- 10个测试结果的总分必须达到800分或以上。
代码如下。
如果要为
在下面的代码中,为名为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | from pydantic import BaseModel from typing import List class RepeatedExams(BaseModel): scores: List[int] # 試験結果の回数がちょうど10回であるか検証 @validator("scores", pre=True) def validate_num_of_exams(cls, v): if len(v) != 10: raise ValueError("The number of exams must be 10.") return v # 1回の試験結果が50点以上であるか検証 @validator("scores", each_item=True) def validate_each_score(cls, v): assert v >= 50, "Each score must be at least 50." return v # 試験結果の合計が800点以上であるか検証 @validator("scores") def validate_sum_score(cls, v): if sum(v) < 800: raise ValueError("sum of numbers greater than 800") return v # OK result1 = RepeatedExams(scores=[87, 88, 77, 100, 61, 59, 97, 75, 80, 85]) # NG, 9回しか試験を受けていない result2 = RepeatedExams(scores=[87, 88, 77, 100, 61, 59, 97, 75, 80]) #> ValidationError: 1 validation error for RepeatedExams #> scores #> The number of exams must be 10. (type=value_error) # NG, 50点未満の試験がある result3 = RepeatedExams(scores=[87, 88, 77, 100, 32, 59, 97, 75, 80, 85]) #> ValidationError: 1 validation error for RepeatedExams #> scores -> 4 #> Each score must be at least 50. (type=assertion_error) # NG, 10回の試験の合計が800点未満である result4 = RepeatedExams(scores=[87, 88, 77, 100, 51, 59, 97, 75, 80, 85]) #> ValidationError: 1 validation error for RepeatedExams #> scores #> sum of numbers greater than 800 (type=value_error) |
导出模型
从
您不仅可以转换和复制,还可以指定目标字段并仅输出特定字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | from pydantic import BaseModel, conint class User(BaseModel): name: str age: conint(strict=True, ge=0) height: conint(strict=True, ge=0) weight: conint(strict=True, ge=0) Kuko = User(name="Kuko", age=19, height=168, weight=58) print(Kuko) # 全フィールドを対象にdictに変換 Kuko_dict_1 = Kuko.dict() print(Kuko_dict_1) #> {'name': 'Kuko', 'age': 19, 'height': 168, 'weight': 58} # nameだけを対象にdictに変換 Kuko_name = Kuko.dict(include={"name"}) print(Kuko_name) #> {'name': 'Kuko'} # 全フィールドを対象にコピー print(Kuko.copy()) print(Kuko_2) #> name='Kuko' age=19 height=168 weight=58 # ageだけ除外してコピー Kuko_3 = Kuko.copy(exclude={"age"}) print(Kuko_3) #> name='Kuko' height=168 weight=58 # 全フィールドを対象にJSONに Kuko_json = Kuko.json() print(Kuko_json) #> {"name": "Kuko", "age": 19, "height": 168, "weight": 58} print(type(Kuko_json)) #> <class 'str'> |
末尾
我放弃了诸如Model Config和Schema之类的其他元素,因为我没有足够的时间来编写它们。
我希望以后可以添加它...