翼度科技»论坛 编程开发 python 查看内容

FastAPI-5:Pydantic、类型提示和模型预览

5

主题

5

帖子

15

积分

新手上路

Rank: 1

积分
15
5 Pydantic、类型提示和模型

FastAPI主要基于Pydantic。它使用模型(Python对象类)来定义数据结构。这些模型在FastAPI应用程序中被大量使用,是编写大型应用程序时的真正优势。

5.1 类型提示

在许多计算机语言中,变量直接指向内存中的值。这就要求程序员声明它的类型,以便确定值的大小和位数。在Python中,变量只是与对象相关联的名称,而对象才有类型。
变量通常与同一个对象相关联。如果我们将类型提示与变量关联起来,就可以避免一些编程错误。因此,Python 在语言的标准类型模块中加入了类型提示。Python解释器会忽略类型提示语法,运行程序时就像没有类型提示一样。那有什么意义呢?
您可能在一行中将一个变量视为字符串,但后来却忘记了,并将其赋值给一个不同类型的对象。虽然其他语言的编译器会提示,但 Python不会。标准 Python解释器会捕获正常的语法错误和运行时异常,但不会捕获变量的混合类型。像mypy这样的辅助工具会关注类型提示,并对任何不匹配发出警告。
Python开发人员也可以使用这些提示,他们可以编写比类型错误检查更多的工具。下面几节将介绍Pydantic软件包是如何开发的,以满足一些并不明显的需求。稍后,您将看到它与FastAPI的集成如何使许多网络开发问题变得更容易处理。
变量类型提示可能只包括类型:name: type
或者用一个值初始化变量:name: type = value
类型可以是标准的Python简单类型,如int或str,也可以是复杂类型,如tuple、list或dict:name: type = value
在Python 3.9之前,需要从typing模块导入这些标准类型名的大写版本:
  1. from typing import Str
  2. thing: Str = "yeti"
复制代码
下面是一些带有初始化的示例:
  1. physics_magic_number: float = 1.0/137.03599913
  2. hp_lovecraft_noun: str = "ichor"
  3. exploding_sheep: tuple = "sis", "boom", bah!"
  4. responses: dict = {"Marco": "Polo", "answer": 42}
复制代码
还可以指定子类型:name: dict[keytype, valtype] = {key1: val1, key2: val2}
最常见的子类如下:

  • Any:任意类型
  • Union:任何指定类型,如 Union[str,int]。
在Python 3.10及更高版本中,可以用type1 | type2 代替 Union[type1,type2]。
Python dict的 Pydantic定义示例如下:
  1. from typing import Any
  2. responses: dict[str, Any] = {"Marco": "Polo", "answer": 42}
复制代码
或者更具体一点
  1. from typing import Any
  2. responses: dict[str, Any] = {"Marco": "Polo", "answer": 42}
复制代码
或 (Python 3.10 及更高版本):
  1. responses: dict[str, str | int] = {"Marco": "Polo", "answer": 42}
复制代码
请注意,类型提示的变量行是合法的Python,而裸变量行则不是:
  1. $ python
  2. ...
  3. >>> thing0
  4. Traceback (most recent call last):
  5.   File "<stdin>", line 1, in <module>
  6. NameError: name thing0 is not defined
  7. >>> thing0: str
复制代码
此外,Python解释器不会捕获不正确的类型使用:
  1. $ python
  2. ...
  3. >>> thing1: str = "yeti"
  4. >>> thing1 = 47
复制代码
但它们会被mypy捕获。如果还没有,运行 pip install mypy。将前面两行保存到名为 stuff.py,1 的文件中,然后试试下面的方法:
  1. $ mypy stuff.py
  2. stuff.py:2: error: Incompatible types in assignment
  3. (expression has type "int", variable has type "str")
  4. Found 1 error in 1 file (checked 1 source file)
复制代码
函数返回类型提示使用了箭头而不是冒号:function(args) -> type:
下面是一个函数返回的 Pydantic 示例:
  1. def get_thing() -> str:
  2.    return "yeti"
复制代码
5.2 数据分组

通常,我们需要将一组相关的变量放在一起,而不是传递大量的单个变量。如何将多个变量整合为一组并保持类型提示呢?在本书的其他章节中,我们将使用密码生物(想象中的生物)和寻找它们的探险家(也是想象中的)的例子。我们最初的密码生物定义将只包含以下字符串变量:

  • name:关键字
  • country:两字符 ISO 国家代码(3166-1 alpha 2)或 * = 全部
  • area 可选;美国州或其他国家分区
  • description:自由格式
  • aka:又称
而探险者将拥有以下内容:

  • name:关键字
  • country:两字符 ISO 国家代码(3166-1 alpha 2)或 * = 全部
  • area 可选;美国州或其他国家分区
  • description:自由格式
这里列出了Python数据分组结构(除了基本的 int、字符串之类):

  • 元组:不可变的对象序列
  • 列表:可变的对象序列
  • 集合:可变的不同对象
  • 字典:可变的键值对象对(键必须是不可变的类型)
  1. # 使用元组
  2. >>> tuple_thing = ("yeti", "CN", "Himalayas",
  3.     "Hirsute Himalayan", "Abominable Snowman")
  4. >>> print("Name is", tuple_thing[0])
  5. Name is yeti
  6. # 使用列表
  7. >>> list_thing = ["yeti", "CN", "Himalayas",
  8.     "Hirsute Himalayan", "Abominable Snowman"]
  9. >>> print("Name is", list_thing[0])
  10. Name is yeti
  11. # 使用元组和命名偏移量
  12. >>> NAME = 0
  13. >>> COUNTRY = 1
  14. >>> AREA = 2
  15. >>> DESCRIPTION = 3
  16. >>> AKA = 4
  17. >>> tuple_thing = ("yeti", "CN", "Himalayas",
  18.     "Hirsute Himalayan", "Abominable Snowman")
  19. >>> print("Name is", tuple_thing[NAME])
  20. Name is yeti
  21. # 使用字典
  22. >>> dict_thing = {"name": "yeti",
  23. ...     "country": "CN",
  24. ...     "area": "Himalayas",
  25. ...     "description": "Hirsute Himalayan",
  26. ...     "aka": "Abominable Snowman"}
  27. >>> print("Name is", dict_thing["name"])
  28. Name is yeti
  29. # 使用命名元组
  30. >>> from collections import namedtuple
  31. >>> CreatureNamedTuple = namedtuple("CreatureNamedTuple",
  32. ...     "name, country, area, description, aka")
  33. >>> namedtuple_thing = CreatureNamedTuple("yeti",
  34. ...     "CN",
  35. ...     "Himalaya",
  36. ...     "Hirsute HImalayan",
  37. ...     "Abominable Snowman")
  38. >>> print("Name is", namedtuple_thing[0])
  39. Name is yeti
  40. >>> print("Name is", namedtuple_thing.name)
  41. Name is yeti
  42. # 标准类
  43. >>> class CreatureClass():
  44. ...     def __init__(self,
  45. ...       name: str,
  46. ...       country: str,
  47. ...       area: str,
  48. ...       description: str,
  49. ...       aka: str):
  50. ...         self.name = name
  51. ...         self.country = country
  52. ...         self.area = area
  53. ...         self.description = description
  54. ...         self.aka = aka
  55. ...
  56. >>> class_thing = CreatureClass(
  57. ...     "yeti",
  58. ...     "CN",
  59. ...     "Himalayas"
  60. ...     "Hirsute Himalayan",
  61. ...     "Abominable Snowman")
  62. >>> print("Name is", class_thing.name)
  63. Name is yeti
  64. # 数据类
  65. >>> from dataclasses import dataclass
  66. >>>
  67. >>> @dataclass
  68. ... class CreatureDataClass():
  69. ...     name: str
  70. ...     country: str
  71. ...     area: str
  72. ...     description: str
  73. ...     aka: str
  74. ...
  75. >>> dataclass_thing = CreatureDataClass(
  76. ...     "yeti",
  77. ...     "CN",
  78. ...     "Himalayas"
  79. ...     "Hirsute Himalayan",
  80. ...     "Abominable Snowman")
  81. >>> print("Name is", dataclass_thing.name)
  82. Name is yeti
复制代码
这对于保持变量的一致性来说已经很不错了。但我们还想要更多:

  • 可能的替代类型的联合
  • 缺失/可选值
  • 默认值
  • 数据验证
  • 与JSON等格式的序列化

5.3 替代方案

使用 Python 内置的数据结构(尤其是字典)很有诱惑力。但你不可避免地会发现字典有点过于 “松散”。自由是有代价的。你需要检查一切:

  • 键是否可选?
  • 如果键缺失,是否有默认值?
  • 键是否存在?
  • 如果存在,键值的类型是否正确?
  • 如果存在,值是否在正确的范围内或与模式匹配?
至少有三种解决方案可以满足这些要求中的至少一部分:Dataclasses、attrs(Dataclasses的超集)、Pydantic(集成到了FastAPI中)。
Pydantic 在验证方面非常突出,它与FastAPI的集成可以捕捉到许多潜在的数据错误。Pydantic依赖于继承(从 BaseModel 类继承),而其他两个软件则使用Python装饰器来定义对象。Pydantic 的另一大优点是它使用了标准的 Python 类型提示语法,而旧版库则在类型提示之前就自行推出了类型提示。
因此,我在本书中将使用 Pydantic,但如果你不使用 FastAPI,你也可能会发现这两种库中的任何一种都有用武之地。
Pydantic 提供了指定这些检查的任意组合的方法:

  • 必须与可选
  • 未指定但需要的默认值
  • 预期的数据类型
  • 值范围限制
  • 其他基于函数的检查(如果需要)
  • 序列化和反序列化
参考资料

5.4简单示例

这个初始示例将使用三个文件:

  • model.py定义Pydantic 模型。
  • data.py 假数据源,定义了一个模型实例。
  • web.py 定义了返回假数据的FastAPI网络端点。
定义生物模型:model.py
  1. from pydantic import BaseModel
  2. class Creature(BaseModel):
  3.     name: str
  4.     country: str
  5.     area: str
  6.     description: str
  7.     aka: str
  8. thing = Creature(
  9.     name="yeti",
  10.     country="CN",
  11.     area="Himalayas",
  12.     description="Hirsute Himalayan",
  13.     aka="Abominable Snowman")
  14. print("Name is", thing.name)
复制代码
Creature类继承自Pydantic的BaseModel。name、country、area、description和aka后面的 : str部分是类型提示,表明每个字符串都是Python字符串。
  1. >>> thing = Creature(
  2. ...     name="yeti",
  3. ...     country="CN",
  4. ...     area="Himalayas"
  5. ...     description="Hirsute Himalayan",
  6. ...     aka="Abominable Snowman")
  7. >>> print("Name is", thing.name)
  8. Name is yeti
复制代码
在 data.py 中定义假数据
从模型导入生物
  1. from model import Creature
  2. _creatures: list[Creature] = [
  3.     Creature(name="yeti",
  4.              country="CN",
  5.              area="Himalayas",
  6.              description="Hirsute Himalayan",
  7.              aka="Abominable Snowman"
  8.              ),
  9.     Creature(name="sasquatch",
  10.              country="US",
  11.              area="*",
  12.              description="Yeti's Cousin Eddie",
  13.              aka="Bigfoot")
  14. ]
  15. def get_creatures() -> list[Creature]:
  16.     return _creatures
复制代码
这段代码导入了我们刚刚编写的 model.py。通过调用它的生物对象列表 _creatures,并提供 get_creatures() 函数来返回它们,它做了一点数据隐藏。
web.py:
  1. from model import Creature
  2. from fastapi import FastAPI
  3. app = FastAPI()
  4. @app.get("/creature")
  5. def get_all() -> list[Creature]:
  6.     from data import get_creatures
  7.     return get_creatures()
  8. if __name__ == "__main__":
  9.     import uvicorn
  10.     uvicorn.run("web:app", reload=True)
复制代码
现在启动服务器。
  1. $ python web.py
  2. INFO:     Will watch for changes in these directories: ['D:\\code\\fastapi-main\\example']
  3. INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
  4. INFO:     Started reloader process [19124] using WatchFiles
  5. INFO:     Started server process [22344]
  6. INFO:     Waiting for application startup.
  7. INFO:     Application startup complete.
复制代码
验证:
  1. $ http http://localhost:8000/creature
  2. HTTP/1.1 200 OK
  3. content-length: 211
  4. content-type: application/json
  5. date: Sat, 08 Jun 2024 02:20:40 GMT
  6. server: uvicorn
  7. [
  8.     {
  9.         "aka": "Abominable Snowman",
  10.         "area": "Himalayas",
  11.         "country": "CN",
  12.         "description": "Hirsute Himalayan",
  13.         "name": "yeti"
  14.     },
  15.     {
  16.         "aka": "Bigfoot",
  17.         "area": "*",
  18.         "country": "US",
  19.         "description": "Yeti's Cousin Eddie",
  20.         "name": "sasquatch"
  21.     }
  22. ]
复制代码
5.5 验证类型

试着给一个或多个 “生物 ”字段分配一个错误类型的值。让我们使用独立测试来实现这一点(Pydantic 并不适用于任何网页代码;这是一个数据问题)。
  1. from model import Creature
  2. dragon = Creature(
  3.     name="dragon",
  4.     description=["incorrect", "string", "list"],
  5.     country="*"
  6.     )
复制代码
运行测试
  1. $ python 5-14.py
  2. Name is yeti
  3. Traceback (most recent call last):
  4.   File "D:\code\fastapi-main\example\5-14.py", line 3, in <module>
  5.     dragon = Creature(
  6.   File "C:\Users\xuron\AppData\Roaming\Python\Python310\site-packages\pydantic\main.py", line 176, in __init__
  7.     self.__pydantic_validator__.validate_python(data, self_instance=self)
  8. pydantic_core._pydantic_core.ValidationError: 3 validation errors for Creature
  9. area
  10.   Field required [type=missing, input_value={'name': 'dragon', 'descr...'list'], 'country': '*'}, input_type=dict]
  11.     For further information visit https://errors.pydantic.dev/2.7/v/missing
  12. description
  13.   Input should be a valid string [type=string_type, input_value=['incorrect', 'string', 'list'], input_type=list]
  14.     For further information visit https://errors.pydantic.dev/2.7/v/string_type
  15. aka
  16.   Field required [type=missing, input_value={'name': 'dragon', 'descr...'list'], 'country': '*'}, input_type=dict]
  17.     For further information visit https://errors.pydantic.dev/2.7/v/missing
复制代码
5.6 验证值

即使值的类型符合 Creature 类中的说明,也可能需要通过更多检查。可以对值本身进行一些限制:

  • Integer (conint)或float:

    • gt: 大于
    • lt:小于
    • ge:大于或等于
    • le:小于或等于
    • multiple_of: 数值的整数倍

  • String (constr):

    • min_length:字符(非byte)的最小长度
    • max_length:最大字符长度
    • to_upper:转换为大写字母
    • to_lower:转为小写
    • regex:匹配Python正则表达式

  • 元组、列表或集合:

    • min_items:最小元素数
    • max_items:元素的最大数量

这些在模型的类型部分中指定。
实例:确保名称字段总是至少有两个字符长。否则,“”(空字符串)就是有效字符串。
  1. from pydantic import BaseModel, constr
  2. class Creature(BaseModel):
  3.     name: constr(min_length=2)
  4.     country: str
  5.     area: str
  6.     description: str
  7.     aka: str
  8. bad_creature = Creature(name="!", description="it's a raccoon", area="your attic")
复制代码
执行:
  1. Traceback (most recent call last):
  2.   File D:\ProgramData\anaconda3\lib\site-packages\spyder_kernels\py3compat.py:356 in compat_exec
  3.     exec(code, globals, locals)
  4.   File d:\code\test5.py:10
  5.     bad_creature = Creature(name="!", description="it's a raccoon", area="your attic")
  6.   File ~\AppData\Roaming\Python\Python310\site-packages\pydantic\main.py:176 in __init__
  7.     self.__pydantic_validator__.validate_python(data, self_instance=self)
  8. ValidationError: 3 validation errors for Creature
  9. name
  10.   String should have at least 2 characters [type=string_too_short, input_value='!', input_type=str]
  11.     For further information visit https://errors.pydantic.dev/2.7/v/string_too_short
  12. country
  13.   Field required [type=missing, input_value={'name': '!', 'descriptio...", 'area': 'your attic'}, input_type=dict]
  14.     For further information visit https://errors.pydantic.dev/2.7/v/missing
  15. aka
  16.   Field required [type=missing, input_value={'name': '!', 'descriptio...", 'area': 'your attic'}, input_type=dict]
  17.     For further information visit https://errors.pydantic.dev/2.7/v/missing
复制代码
下列使用了另一种方法,即Pydantic字段规范。
  1. from pydantic import BaseModel, constr
  2. class Creature(BaseModel):
  3.     name: constr(min_length=2)
  4.     country: str
  5.     area: str
  6.     description: str
  7.     aka: str
  8. bad_creature = Creature(name="!", description="it's a raccoon", area="your attic")
复制代码
执行:
  1. Traceback (most recent call last):
  2.   File D:\ProgramData\anaconda3\lib\site-packages\spyder_kernels\py3compat.py:356 in compat_exec
  3.     exec(code, globals, locals)
  4.   File d:\code\test5.py:10
  5.     bad_creature = Creature(name="!", description="it's a raccoon", area="your attic")
  6.   File ~\AppData\Roaming\Python\Python310\site-packages\pydantic\main.py:176 in __init__
  7.     self.__pydantic_validator__.validate_python(data, self_instance=self)
  8. ValidationError: 3 validation errors for Creature
  9. name
  10.   String should have at least 2 characters [type=string_too_short, input_value='!', input_type=str]
  11.     For further information visit https://errors.pydantic.dev/2.7/v/string_too_short
  12. country
  13.   Field required [type=missing, input_value={'name': '!', 'descriptio...", 'area': 'your attic'}, input_type=dict]
  14.     For further information visit https://errors.pydantic.dev/2.7/v/missing
  15. aka
  16.   Field required [type=missing, input_value={'name': '!', 'descriptio...", 'area': 'your attic'}, input_type=dict]
  17.     For further information visit https://errors.pydantic.dev/2.7/v/missing
复制代码
Field() 的...参数表示需要一个值,而且没有默认值
这只是对 Pydantic 的简单介绍。主要的收获是,它能让你自动验证数据。在从网络层或数据层获取数据时,您将看到这一点有多么有用。
5.6 小结

模型是定义将在网络应用程序中传递的数据的最佳方式。Pydantic利用Python的类型提示来定义数据模型,以便在应用程序中传递。

来源:https://www.cnblogs.com/testing-/p/18236466
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具