FastAPI 指南(一)
原文:
annas-archive.org/md5/aadba315b042a88fe9a981fd64d02c4a译者:飞龙
序言
这是对 FastAPI——一个现代 Python Web 框架的务实介绍。这也是一个关于我们偶尔会碰到的闪亮新物体如何变得非常有用的故事。当你遇到狼人时,一发银弹可谓非常实用。(而且你将在本书后面遇到狼人。)
我从 70 年代中期开始编写科学应用程序。在 1977 年我第一次在 PDP-11 上遇到 Unix 和 C 之后,我有一种这个 Unix 东西可能会流行起来的感觉。
在 80 年代和 90 年代初期,互联网虽然还没有商业化,但已经是一个免费软件和技术信息的良好来源。当名为 Mosaic 的网络浏览器在 1993 年在初生的开放互联网上发布时,我有一种这个网络东西可能会流行起来的感觉。
几年后我创立了自己的 Web 开发公司时,我的工具还是当时的惯用选择:PHP、HTML 和 Perl。几年后的一个合同工作中,我最终尝试了 Python,并对我能够多快地访问、操作和显示数据感到惊讶。在两周的空闲时间内,我成功复制了一个四名开发者耗时一年才编写完整的 C 应用程序的大部分功能。现在我有一种这个 Python 东西可能会流行起来的感觉。
此后,我的大部分工作涉及 Python 及其 Web 框架,主要是 Flask 和 Django。我特别喜欢 Flask 的简单性,并在许多工作中更倾向于使用它。但就在几年前,我在灌木丛中发现了一抹闪光:一款名为 FastAPI 的新 Python Web 框架,由 Sebastián Ramírez 编写。
在阅读他(优秀)的文档时,我对设计和所投入的思考深感印象深刻。特别是他的历史页面展示了他在评估替代方案时所付出的努力。这不是一个自我项目或有趣的实验,而是一个面向现实开发的严肃框架。现在我有一种这个 FastAPI 东西可能会流行起来的感觉。
我使用 FastAPI 编写了一个生物医学 API 站点,效果非常好,以至于我们团队在接下来的一年中用 FastAPI 重写了我们旧的核心 API。这个系统仍在运行中,并表现良好。我们的团队学习了本书中你将阅读到的基础知识,所有人都觉得我们正在写出更好的代码,速度更快,bug 更少。顺便说一句,我们中的一些人以前并没有用过 Python,只有我用过 FastAPI。
所以当我有机会向 O'Reilly 建议续集《介绍 Python》时,FastAPI 是我的首选。在我看来,FastAPI 至少会像 Flask 和 Django 一样有影响力,甚至更大。
正如我之前提到的,FastAPI 网站本身提供了世界级的文档,包括许多关于常见 Web 主题的细节:数据库、身份验证、部署等等。那么为什么要写一本书呢?
这本书并不打算全面无遗漏,因为那样会令人筋疲力尽。它的目的是实用——帮助你快速掌握 FastAPI 的主要思想并应用它们。我将指出各种需要一些侦查的技术,并提供日常最佳实践建议。
每一章都以预览内容开始。接下来,我尽量不忘记刚承诺的内容,提供细节和偶尔的旁白。最后,有一个简短易消化的复习。
俗话说,“这些是我事实依据的观点。”您的经验将是独特的,但我希望您能在这里找到足够有价值的内容,以成为更高效的 Web 开发者。
本书中使用的约定
本书中使用了以下排版约定:
Italic
指示新术语、网址、电子邮件地址、文件名和文件扩展名。
Constant width
用于程序列表,以及段落内引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
Constant width bold
显示用户应按照字面意义输入的命令或其他文本。
Constant width italic
显示应由用户提供值或由上下文确定值的文本。
提示
此元素表示提示或建议。
注意
此元素表示一般提示。
使用代码示例
补充材料(代码示例、练习等)可在https://github.com/madscheme/fastapi下载。
如果您在使用示例代码时遇到技术问题或困难,请发送电子邮件至support@oreilly.com。
本书旨在帮助您完成工作。一般来说,如果本书提供了示例代码,您可以在自己的程序和文档中使用它。除非您要复制大部分代码,否则无需联系我们以获取许可。例如,编写一个使用本书多个代码片段的程序不需要许可。销售或分发 O’Reilly 书籍中的示例需要许可。引用本书并引用示例代码来回答问题不需要许可。将本书大量示例代码整合到产品文档中需要许可。
我们感谢,但通常不要求归属。归属通常包括标题、作者、出版商和 ISBN 号码。例如:“FastAPI by Bill Lubanovic (O’Reilly)。2024 年版权归 Bill Lubanovic 所有,978-1-098-13550-8。”
如果您认为您使用的示例代码超出了合理使用范围或上述许可,请随时联系我们permissions@oreilly.com。
O’Reilly 在线学习
注意
40 多年来,O’Reilly Media提供技术和商业培训、知识和洞察,帮助公司取得成功。
我们独特的专家和创新者网络通过图书、文章和我们的在线学习平台分享他们的知识和专长。奥莱利的在线学习平台为您提供了按需访问实时培训课程、深入学习路径、交互式编码环境以及奥莱利和其他 200 多家出版商的大量文本和视频的机会。有关更多信息,请访问https://oreilly.com。
如何联系我们
有关本书的评论和问题,请联系出版商:
-
奥莱利媒体公司
-
北格拉文斯坦高速公路 1005 号
-
加州塞巴斯托波尔市 95472
-
800-889-8969(美国或加拿大)
-
707-829-7019(国际或本地)
-
707-829-0104(传真)
我们有一个为本书准备的网页,我们在其中列出勘误、示例和任何其他信息。您可以在https://oreil.ly/FastAPI访问此页面。
有关我们的图书和课程的新闻和信息,请访问https://oreilly.com。
在 LinkedIn 上找到我们:https://linkedin.com/company/oreilly-media。
在 Twitter 上关注我们:https://twitter.com/oreillymedia。
在 YouTube 上观看我们:https://youtube.com/oreillymedia。
致谢
感谢许多人,在许多地方,从中我学到了很多:
-
艾拉高中
-
匹兹堡大学
-
生物钟实验室,
明尼苏达大学
-
Intran
-
Crosfield-Dicomed
-
西北航空公司
-
Tela
-
WAM!NET
-
疯狂计划
-
SSESCO
-
Intradyn
-
Keep
-
汤姆逊路透社
-
Cray
-
企鹅计算
-
互联网档案馆
-
CrowdStrike
-
Flywheel
第一部分:有什么新东西?
世界因蒂姆·伯纳斯-李爵士发明的万维网¹ 和吉多·范罗苏姆设计的 Python 编程语言受益匪浅。
唯一微小的问题是,一家无名的计算机书出版商经常在其相关的网络和 Python 封面上放置蜘蛛和蛇。要是网络被称为“万维 Woof”(织物中的交叉线,也称为 weft),而 Python 被称为“Pooch”,这本书的封面可能会像图 I-1 一样。
图 I-1. FastAPI:现代狗狗开发
但我离题了。² 本书讨论以下内容:
互联网
特别高效的技术,它的变化以及如何为其开发软件
Python
特别高效的网络开发语言
FastAPI
特别高效的 Python 网络框架
第一部分的两章讨论了互联网和 Python 中的新兴主题:服务和 API;并发性;分层架构;以及大数据。
第二部分 是对 FastAPI 的高层次介绍,这是一个新的 Python 网络框架,对第一部分 中提出的问题有很好的答案。
第三部分 深入研究了 FastAPI 工具箱,包括在生产开发中学到的技巧。
最后,第四部分 展示了 FastAPI 网络示例的画廊。它们使用了一个共同的数据源——虚构的生物,可能比通常的随机说明更有趣和连贯。这些应该为特定应用程序提供了一个起点。
¹ 我曾经和他握过手。我一个月都没洗我的手,但我敢打赌他当时就洗了。
² 这不是最后一次。
第一章:现代网络
我所设想的网络,我们还没有见到。未来比过去要大得多。
Tim Berners-Lee
预览
曾经,互联网小而简单。开发者们喜欢在单个文件中混合使用 PHP、HTML 和 MySQL 调用,并自豪地告诉每个人去查看他们的网站。但随着时间的推移,网络变得庞大无比,页面数达到数不清、甚至是数不尽的数量——早期的游乐场成为了一个主题公园的元宇宙。
在本章中,我将指出一些对现代网络越来越重要的领域:
-
服务和 API
-
并发
-
层次
-
数据
下一章将展示 Python 在这些领域提供了什么。之后,我们将深入了解 FastAPI Web 框架及其提供的功能。
服务和 API
网络是一个连接的伟大结构。虽然大量活动仍然发生在内容方面——HTML、JavaScript、图片等,但越来越重视连接这些内容的应用程序接口(APIs)。
通常,一个网络服务负责低级数据库访问和中级业务逻辑(通常被合称为后端),而 JavaScript 或移动应用程序提供丰富的顶级前端(交互式用户界面)。这两个世界已变得更加复杂和分化,通常需要开发人员专注于其中之一。比以往更难成为全栈开发者了。¹
这两个世界通过 API 彼此交流。在现代网络中,API 设计与网站设计同样重要。API 是一种合同,类似于数据库模式。定义和修改 API 现在是一项主要工作。
API 的种类
每个 API 定义如下内容:
协议
控制结构
格式
内容结构
随着技术的发展,多种 API 方法已经发展,从孤立的机器到多任务系统,再到网络服务器。在某些时候,您可能会遇到其中的一种或多种,因此以下是在HTTP及其朋友中特别介绍的简要总结:
-
在网络出现之前,API 通常意味着非常紧密的连接,就像在应用程序中与库中的函数调用一样——比如在数学库中计算平方根。
-
*远程过程调用(RPCs)*的发明是为了在调用应用程序时调用其他进程中的函数,无论是在同一台机器上还是其他机器上。一个流行的现代例子是gRPC。
-
消息传递在进程之间的管道中发送小块数据。消息可能是类似动词的命令,也可能仅仅是感兴趣的名词事件。当前流行的消息传递解决方案广泛变化,从工具包到完整的服务器,包括Apache Kafka,RabbitMQ,NATS和ZeroMQ。通信可以遵循不同的模式:
请求-响应
一对一,如 Web 浏览器调用 Web 服务器。
发布-订阅,或 pub-sub
发布者 发布消息,订阅者 根据消息中的某些数据(如主题)对每条消息作出响应。
队列
类似于 pub-sub,但仅有一个池中的订阅者获取消息并对其作出响应。
任何这些都可以与 Web 服务一起使用,例如执行像发送电子邮件或创建缩略图图像这样的慢速后端任务。
HTTP
伯纳斯-李为他的万维网提出了三个组件:
HTML
一种用于显示数据的语言
HTTP
客户端-服务器协议
URL
用于 Web 资源的寻址方案
尽管这些在回顾中显而易见,但它们事实上是一个极其有用的组合。随着 Web 的发展,人们进行了各种尝试,一些想法,如 IMG 标签,经历了达尔文式的竞争。随着需求变得更加清晰,人们开始严格定义标准。
REST(ful)
Roy Fielding 博士论文中的一个章节定义了 表现状态转移(REST) —— 一种用于 HTTP 的 架构风格。² 尽管经常被引用,但它往往被广泛 误解。
一个粗略共享的适应已经演变并主导了现代网络。它被称为 RESTful,具有以下特点:
-
使用 HTTP 和客户端-服务器协议
-
无状态(每个连接都是独立的)
-
可缓存的
-
基于资源的
资源 是可以区分并执行操作的数据。Web 服务为每个要公开的功能提供一个 终端点 —— 一个独特的 URL 和 HTTP 动词(操作)。终端点也被称为 路由,因为它将 URL 路由到一个功能。
数据库用户熟悉 CRUD 缩写的过程:创建、读取、更新、删除。HTTP 动词非常 CRUD:
POST
创建(写入)
PUT
完全修改(替换)
PATCH
部分修改(更新)
GET
嗯,获取(读取、检索)
DELETE
嗯,删除
客户端通过 RESTful 终端点向服务器发送 请求,数据位于 HTTP 消息的以下区域之一:
-
头部信息
-
URL 字符串
-
查询参数
-
主体数值
接着,HTTP 响应 返回这些内容:
-
一个整数 状态码 指示以下内容:
100s
信息,请继续
200s
成功
300s
重定向
400s
客户端错误
500s
服务器错误
-
各种头部信息
-
一个可能为空、单一或 分块 的主体(以连续的片段)
至少有一个状态码是一个复活节彩蛋:418(我是 茶壶)应该由一个连接到网络的茶壶返回,如果被要求煮咖啡。
您将会发现很多关于 RESTful API 设计的网站和书籍,它们都有有用的经验法则。本书将在途中提供一些。
JSON 和 API 数据格式
前端应用程序可以与后端 Web 服务交换纯 ASCII 文本,但如何表示像列表这样的数据结构呢?
正是在我们真正需要的时候,*JavaScript 对象表示法(JSON)*横空出世——又一个简单的想法解决了一个重要问题,在事后看来显得显而易见。尽管 J 代表 JavaScript,其语法看起来也很像 Python。
JSON 已经大量取代了像 XML 和 SOAP 这样的旧尝试。在本书的其余部分,你会看到 JSON 是默认的 Web 服务输入和输出格式。
JSON:API
RESTful 设计与 JSON 数据格式的结合现在很常见。但在模糊性和书呆子之间仍然有一些余地。最近的JSON:API提案旨在稍微加强规范。本书将使用宽松的 RESTful 方法,但如果你有重大争论,JSON:API 或类似严格的方法可能会有所帮助。
GraphQL
对于某些目的来说,RESTful 接口可能很笨重。Facebook(现在是 Meta)设计了图形查询语言(GraphQL)来指定更灵活的服务查询。本书不会深入讲解 GraphQL,但如果你觉得 RESTful 设计对你的应用不足够,你可能需要深入了解一下。
并发
除了服务导向的增长外,连接到 Web 服务的数量的快速扩展要求更好的效率和规模。
我们希望减少以下内容:
延迟
前期等待时间
吞吐量
服务与其调用者之间每秒传输的字节数
在早期的网络时代,³ 人们梦想支持数百个同时连接,后来又担心“10K 问题”,现在却假定可以同时处理数百万个连接。
并发并不意味着完全的并行处理。多个处理不会在同一微秒内在单个 CPU 上发生。相反,并发主要避免忙等(即空闲 CPU 直到收到响应)。CPU 是很快的,但网络和磁盘慢了成千上万倍。所以,每当我们与网络或磁盘通信时,我们不希望只是一动不动地等待响应。
正常的 Python 执行是同步的:按代码指定的顺序一次处理一件事。有时我们希望是异步的:一会儿做一件事,然后做另一件事,再回到第一件事,依此类推。如果我们所有的代码都用 CPU 来计算事物(CPU bound),那么没有多余的时间去异步。但是如果我们执行某些让 CPU 等待外部事物完成的操作(I/O bound),我们就可以异步执行。
异步系统提供一个事件循环:将慢操作的请求发送并记录下来,但我们不会阻塞 CPU 等待它们的响应。相反,每次循环通过时都会进行一些即时处理,接下来处理在这段时间内收到的任何响应。
这些效果可能非常显著。本书后面会介绍,FastAPI 支持异步处理,使其比典型的 Web 框架快得多。
异步处理并不是魔法。在事件循环中仍需小心,避免进行过多的 CPU 密集型工作,因为那会拖慢一切。在本书的后面,你将看到 Python 的async和await关键字的用法,以及 FastAPI 如何让你混合使用同步和异步处理。
层次
绿巨人的粉丝可能还记得他提到自己的层次性格,对此驴子回答道:“像洋葱一样?”
嗯,如果食人魔和泪眼汪汪的蔬菜都有层次,那么软件也可以有。为了管理大小和复杂性,许多应用程序长期以来一直使用所谓的三层模型。⁴ 这并不是特别新鲜。术语可能有所不同,⁵ 但在本书中,我使用以下简单的术语分离(见图 1-1):
Web
HTTP 上的输入/输出层,组装客户端请求,调用服务层,并返回响应
服务
业务逻辑在需要时调用数据层
数据
访问数据存储和其他服务
模型
所有层共享的数据定义
Web 客户端
Web 浏览器或其他 HTTP 客户端软件
数据库
数据存储,通常是 SQL 或 NoSQL 服务器
图 1-1. 垂直层次
这些组件将帮助你在无需从头开始的情况下扩展你的站点。它们不是量子力学的法则,所以请将它们视作本书阐述的指南。
各层通过 API 进行通信。这些可以是简单的函数调用到分离的 Python 模块,也可以通过任何方法访问外部代码。如前所述,这可能包括 RPC、消息等。在本书中,我假设一个单一的 Web 服务器,Python 代码导入其他 Python 模块。模块处理分离和信息隐藏。
Web 层是用户通过客户端应用程序和 API 看到的层面。通常我们讨论的是一个符合 RESTful 标准的 Web 接口,包括 URL 和 JSON 编码的请求与响应。但也可以在 Web 层之外构建替代文本(或命令行界面,CLI)客户端。Python Web 代码可以导入服务层模块,但不应导入数据模块。
服务层包含这个网站提供的实际细节。这一层本质上看起来像一个库。它导入数据模块来访问数据库和外部服务,但不应知道具体细节。
数据层通过文件或客户端调用其他服务为服务层提供数据访问。还可能存在替代的数据层,与单一的服务层通信。
模型框并不是实际的层,而是各层共享的数据定义来源。如果你在它们之间传递内置的 Python 数据结构,则不需要这一层。正如你将看到的,FastAPI 包含 Pydantic,可以定义具有许多有用特性的数据结构。
为什么要进行这些分层?有很多原因,其中之一是每个层次可以:
-
由专家撰写。
-
在隔离环境中进行测试。
-
替换或补充:你可以添加第二个 Web 层,使用不同的 API,比如 gRPC,与一个 Web 层并行使用。
遵循幽灵剧组的一条规则:不要交叉流动。也就是说,不要让 Web 层的细节泄露到 Web 外,或者数据库层的细节泄露到数据层外。
你可以将层次想象成一个垂直堆叠,就像英国烘焙大赛中的蛋糕^(6)。
这里有分层分离的一些原因:
-
如果你不分离这些层,预期将会出现一个广为传播的网络模因:现在你有两个问题。
-
一旦层次混合,稍后分离将会非常困难。
-
如果代码逻辑混乱,你需要了解两个或更多专业知识来理解和编写测试将会很困难。
顺便说一句,虽然我称它们为层,但你不需要假设一个层次是“高于”或“低于”另一个,并且命令是沿着重力流动的。垂直沙文主义!你也可以将层次视为侧向通信的盒子(图 1-2)。
图 1-2. 侧向通信的盒子
无论你如何想象它们,盒子/层之间唯一的通信路径是箭头(API)。这对于测试和调试非常重要。如果工厂中存在未记录的门,夜间看守员将不可避免地感到惊讶。
Web 客户端与 Web 层之间的箭头使用 HTTP 或 HTTPS 传输大多数 JSON 文本。数据层与数据库之间的箭头使用特定于数据库的协议,并携带 SQL(或其他)文本。层之间的箭头是传递数据模型的函数调用。
此外,通过箭头流动的推荐数据格式如下:
客户端 ⇔ Web
使用 JSON 的 RESTful HTTP
Web ⇔ 服务
模型
服务 ⇔ 数据
模型
数据 ⇔ 数据库和服务
特定的 API
根据我自己的经验,这是我选择在本书中构建主题结构的方式。它是可行的,并且已经扩展到相当复杂的站点,但并不是神圣不可侵犯的。你可能有更好的设计!不管你如何做,以下是重要的几点:
-
分开领域特定的细节。
-
定义层次之间的标准 API。
-
不作弊;不泄露。
有时候决定代码的最佳归属层次是一个挑战。例如,第十一章 讨论了认证和授权需求以及如何实现它们——作为 Web 和服务之间的额外层,或者在其中一个中。软件开发有时与艺术同样重要。
数据
虽然 Web 经常被用作关系数据库的前端,但随着 NoSQL 或 NewSQL 数据库等多种存储和访问数据的方式的发展,其它方式也在增多。
但除了数据库外,机器学习(ML) — 或者 深度学习 或仅仅 AI — 正在从根本上改变技术景观。开发大型模型需要 大量 处理数据,这在传统上被称为提取、转换、加载(ETL)。
作为一种通用的服务架构,网络可以帮助解决许多 ML 系统的琐碎问题。
复习
网络使用许多 API,特别是 RESTful 的 API。异步调用允许更好的并发性,这加快了整体过程。Web 服务应用通常足够大,可以划分为多个层次。数据已经成为一个独立的重要领域。所有这些概念都在 Python 编程语言中得到了解决,在下一章中详细介绍。
¹ 几年前我放弃了尝试。
² 风格 意味着一个更高级别的模式,如 客户端-服务器,而不是一个特定的设计。
³ 大约在穴居人与巨型地懒一起踢毽子的时候。
⁴ 选择你自己的方言:层/层次,番茄/番茄/感谢。
⁵ 你经常会看到 模型-视图-控制器(MVC) 及其变体。通常伴随着宗教战争,我对此持不可知论立场。
⁶ 正如观众所知,如果你的层次结构松散,可能下周就不会回到帐篷了。
第二章:现代 Python
这都是 Confuse-a-Cat 每天的工作内容。
Monty Python
预览
Python 在与我们变化的技术世界保持同步时在进化。本章讨论了适用于前一章问题的具体 Python 功能,以及一些额外的:
-
工具
-
API 和服务
-
变量和类型提示
-
数据结构
-
Web 框架
工具
每种计算语言都有以下内容:
-
核心语言和内置标准包
-
添加外部包的方法
-
推荐的外部包
-
开发工具环境
接下来的章节列出了本书所需或推荐的 Python 工具。
这些可能随时间而改变!Python 的打包和开发工具是移动的目标,时不时会有更好的解决方案出现。
入门
你应该能够编写和运行像示例 2-1 这样的 Python 程序。
示例 2-1。这个 Python 程序是这样的:this.py
def paid_promotion():
print("(that calls this function!)")
print("This is the program")
paid_promotion()
print("that goes like this.")
要在文本窗口或终端命令行中执行此程序,我将使用一个$ 提示(系统请求您输入一些内容)。您在提示后键入的内容显示为**bold print**。如果您已将示例 2-1 保存为this.py文件,则可以像在示例 2-2 中显示的那样运行它。
示例 2-2。测试 this.py
$ python this.py
This is the program
(that calls this function!)
that goes like this.
一些代码示例使用交互式的 Python 解释器,只需键入**python**即可获得:
$ python
Python 3.9.1 (v3.9.1:1e5d33e9b9, Dec 7 2020, 12:10:52)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
前几行是特定于您的操作系统和 Python 版本。这里的>>>是您的提示符。交互解释器的一个方便额外功能是,如果您键入其名称,它将为您打印变量的值:
>>> wrong_answer = 43
>>> wrong_answer
43
这也适用于表达式:
>>> wrong_answer = 43
>>> wrong_answer - 3
40
如果你对 Python 比较陌生或想要快速复习,可以阅读接下来的几节。
Python 本身
你至少需要 Python 3.7 作为基本要求。这包括像类型提示和 asyncio 这样的功能,这些是 FastAPI 的核心要求。我建议至少使用 Python 3.9,它将有更长的支持周期。Python 的标准来源是Python 软件基金会。
包管理
你会想要在计算机上安全下载外部的 Python 包并将其安装。这方面的经典工具是pip。
但是你如何下载这个下载器呢?如果你从 Python 软件基金会安装了 Python,你应该已经有了 pip。如果没有,请按照 pip 网站上的说明获取它。在本书中,当我介绍一个新的 Python 包时,我会包括下载它的 pip 命令。
虽然你可以用普通的 pip 做很多事情,但你可能也想使用虚拟环境,并考虑像 Poetry 这样的替代工具。
虚拟环境
Pip 将下载和安装软件包,但它们应该放在哪里?尽管标准 Python 及其包含的库通常安装在操作系统的标准位置,但您可能(并且可能不应该)能够在那里进行任何更改。Pip 使用除系统目录之外的默认目录,因此您不会覆盖系统的标准 Python 文件。您可以更改此设置;有关详细信息,请参阅 pip 网站,了解适用于您操作系统的详情。
但是通常会使用多个版本的 Python,或者为项目安装特定的版本,这样你就能确切地知道其中包含哪些软件包。为此,Python 支持虚拟环境。这些只是目录(在非 Unix 世界中称为文件夹),pip 将下载的软件包写入其中。当你激活一个虚拟环境时,你的 Shell(主系统命令解释器)在加载 Python 模块时首先查找这些目录。
这个程序就是venv,自 Python 3.4 版本起就已经包含在标准 Python 中。
让我们创建一个名为venv1的虚拟环境。您可以像独立程序一样运行 venv 模块:
$ venv venv1
或作为 Python 模块:
$ python -m venv venv1
要使这成为您当前的 Python 环境,请运行此 Shell 命令(在 Linux 或 Mac 上;有关 Windows 和其他系统的详情,请参阅 venv 文档):
$ source venv1/bin/activate
现在,每次你运行pip install,它将在venv1下安装软件包。当你运行 Python 程序时,Python 解释器和模块就在那里。
要停用你的虚拟环境,请按 Control-D(Linux 或 Mac),或者键入**deactivate**(Windows)。
你可以创建像venv2这样的备选环境,并在它们之间进行停用/激活操作(尽管我希望你的命名想象力比我更强)。
Poetry
pip 和 venv 的这种组合非常常见,人们开始将它们组合在一起以节省步骤,并避免那些source命令的复杂性。其中一个这样的包是Pipenv,但一个更新的竞争对手叫做Poetry正在变得更加流行。
使用了 pip、Pipenv 和 Poetry 之后,我现在更喜欢 Poetry。用pip install poetry来获取它。Poetry 有许多子命令,比如poetry add用于向您的虚拟环境添加软件包,poetry install用于实际下载和安装它等等。查看 Poetry 网站或运行poetry命令以获取帮助。
除了下载单个软件包外,pip 和 Poetry 还管理配置文件中的多个软件包:requirements.txt用于 pip,pyproject.toml用于 Poetry。Poetry 和 pip 不仅下载软件包,还管理软件包可能对其他软件包的复杂依赖关系。您可以指定所需软件包的版本,如最小值、最大值、范围或确切值(也称为固定版本)。随着项目的增长和所依赖的软件包发生变化,这可能很重要。如果您使用的功能首次出现在某个版本中,您可能需要该软件包的最小版本,或者如果删除了某个功能,则可能需要该软件包的最大版本。
源代码格式化
源代码格式化比前几节的主题不那么重要,但仍然有帮助。避免使用工具对源代码进行格式化(小工具论)争论。一个不错的选择是 Black。使用 pip install black 安装它。
测试
测试在第十二章中有详细说明。尽管标准的 Python 测试包是 unittest,但大多数 Python 开发人员使用的产业强度 Python 测试包是 pytest。使用 pip install pytest 安装它。
源代码控制和持续集成
现在几乎普遍的源代码控制解决方案是Git,使用像 GitHub 和 GitLab 这样的存储库(repos)。使用 Git 不限于 Python 或 FastAPI,但你可能会在开发中花费大量时间使用 Git。pre-commit 工具在提交到 Git 之前在本地运行各种测试(如 black 和 pytest)。推送到远程 Git 存储库后,可能会在那里运行更多的持续集成(CI)测试。
第十二章 和 “故障排除” 有更多细节。
Web 工具
第三章 展示了如何安装和使用本书中使用的主要 Python Web 工具:
FastAPI
Web 框架本身
Uvicorn
异步 Web 服务器
HTTPie
一个类似于 curl 的文本 Web 客户端
Requests
同步 Web 客户端包
HTTPX
同步/异步 Web 客户端包
API 和服务
Python 的模块和包对于创建不会成为“大块泥巴”的大型应用程序至关重要。即使在单进程 Web 服务中,通过模块和导入的精心设计,你也可以保持第一章中讨论的分离。
Python 的内置数据结构非常灵活,非常诱人,可以在各处使用。但在接下来的章节中,你会看到我们可以定义更高级的模型来使我们的层间通信更清洁。这些模型依赖于一个相对较新的 Python 添加功能称为类型提示。让我们深入了解一下,但首先简要了解 Python 如何处理变量。这不会伤害到你。
变量是名称
在软件世界中,术语对象有许多定义——也许太多了。在 Python 中,对象是程序中每个不同数据的数据结构,从像 5 这样的整数,到函数,到你可能定义的任何东西。它指定了,除其他事务信息外,以下内容:
-
一个独特的标识值
-
与硬件匹配的低级类型
-
特定的值(物理位)
-
变量的引用计数,即指向它的变量数目
Python 在对象级别上是强类型的(它的类型不会改变,尽管其值可能会)。如果一个对象的值可以被改变,则称其为可变的,否则称其为不可变的。
但在变量层面上,Python 与许多其他计算语言不同,这可能令人困惑。在许多其他语言中,变量本质上是指向内存区域的直接指针,该区域包含按照计算机硬件设计存储的原始值的位。如果您给该变量赋予一个新值,语言将会用新值覆盖内存中的旧值。
这是直接且快速的。编译器跟踪了每个东西的位置。这是 C 等语言比 Python 更快的一个原因。作为开发者,您需要确保每个变量只分配正确类型的值。
现在,这里是 Python 的一个重要区别:Python 变量只是一个名称,暂时关联到内存中的一个更高级的对象。如果你给一个引用不可变对象的变量赋予一个新值,实际上你创建了一个包含该值的新对象,然后让该名称指向这个新对象。旧对象(名称曾经引用的对象)随后变为自由状态,并且如果没有其他名称仍然引用它(即其引用计数为 0),其内存可以被回收。
在Introducing Python(O’Reilly)中,我将对象比作坐在内存架子上的塑料盒子,而名称/变量则是粘在这些盒子上的便签。或者你可以将名称想象为附在这些盒子上的带有字符串的标签。
通常情况下,当你使用一个名称时,你将其分配给一个对象,并且它会保持附着状态。这种简单的一致性有助于你理解你的代码。变量的作用域是名称在其中引用相同对象的代码区域,例如在函数内部。你可以在不同的作用域中使用相同的名称,但每个作用域都引用不同的对象。
虽然在 Python 程序中,您可以使一个变量引用不同的对象,但这并不一定是一个好的实践。如果不查看,您无法确定第 100 行的名称x是否与第 20 行的名称x在相同的作用域内。(顺便说一句,x是一个糟糕的名称。我们应该选择那些实际上有意义的名称。)
类型提示
所有这些背景都有一个重点。
Python 3.6 增加了类型提示,用于声明变量引用的对象的类型。这些提示并不由 Python 解释器在运行时强制执行!相反,它们可以被各种工具用来确保您对变量的使用是一致的。标准类型检查器称为mypy,我稍后会展示给你看。
类型提示可能看起来只是一件好事,就像程序员使用的许多 lint 工具,用于避免错误。例如,它可能提醒您,您的变量count引用了一个 Python 类型为int的对象。但是提示,尽管它们是可选的并且是未强制执行的注释(字面上是提示),却有意想不到的用途。在本书的后面,您将看到 FastAPI 如何调整 Pydantic 包以巧妙利用类型提示。
类型声明的添加可能是其他以前无类型语言的趋势。例如,许多 JavaScript 开发人员已转向TypeScript。
数据结构
在第五章中,您将了解有关 Python 和数据结构的详细信息。
Web 框架
作为其他功能之一,Web 框架在 HTTP 字节和 Python 数据结构之间进行转换。它可以节省大量精力。另一方面,如果其中一部分不按您的需求工作,则可能需要入侵解决方案。俗话说,不要重复造轮子——除非您不能获得圆形的。
Web 服务器网关接口(WSGI)是将应用程序代码连接到 Web 服务器的同步 Python标准规范。传统的 Python Web 框架都建立在 WSGI 之上。但同步通信可能意味着等待一些比 CPU 慢得多的东西,如磁盘或网络。然后您将寻找更好的并发性。近年来,并发性变得更加重要。因此,开发了 Python 的异步服务器网关接口(ASGI)规范。第四章详细讨论了这一点。
Django
Django是一个全功能的 Web 框架,自称为“完美主义者的截止日期 Web 框架”。它由 Adrian Holovaty 和 Simon Willison 于 2003 年宣布,并以 20 世纪比利时爵士吉他手 Django Reinhardt 命名。Django 经常用于数据库支持的企业网站。在第七章中,我将更多地详细介绍 Django。
Flask
相比之下,由 Armin Ronacher 在 2010 年推出的Flask是一个微框架。第七章更多地讨论了 Flask 及其与 Django 和 FastAPI 的比较。
FastAPI
在舞会上与其他求婚者见面后,我们最终遇到了引人入胜的 FastAPI,这本书的主题。尽管 FastAPI 由 Sebastián Ramírez 于 2018 年发布,但它已经攀升至 Python Web 框架的第三位,仅次于 Flask 和 Django,并且增长速度更快。2022 年的比较显示它可能在某个时候超过它们。
注意
截至 2023 年 10 月底,GitHub 上的星标数如下:
-
Django:73.8 千
-
Flask:64.8 千
-
FastAPI:64 千
经过对替代方案的仔细调查,Ramírez 提出了一个设计,该设计主要基于两个第三方 Python 包:
-
Starlette 用于 Web 的详细信息
-
Pydantic 的数据详情
他还为最终产品添加了自己的成分和特殊酱汁。您将在下一章中看到我所指的。
回顾
本章涵盖了与今天的 Python 相关的许多内容:
-
Python Web 开发者的有用工具
-
API 和服务的显著性
-
Python 的类型提示、对象和变量
-
Web 服务的数据结构
-
Web 框架
第二部分:FastAPI 之旅
本部分的章节提供了 FastAPI 的千尺视角——更像是无人机而不是间谍卫星。它们迅速介绍基础知识,但保持在水面之上,避免将你淹没在细节中。这些章节相对较短,旨在为第三部分的深度提供背景。
当你适应了本部分的理念后,第三部分 将深入探讨这些细节。那里你可以做出一些真正的贡献,或者造成一些伤害。不评判;这取决于你。
第三章:FastAPI 指南
FastAPI 是一个现代的、快速(高性能)的 Web 框架,用于使用 Python 3.6+ 标准的类型提示构建 API。
FastAPI 的创始人 Sebastián Ramírez
预览
FastAPI 由 Sebastián Ramírez 在 2018 年发布。在许多方面,它比大多数 Python Web 框架更现代化——利用了最近几年内添加到 Python 3 中的功能。本章是 FastAPI 主要特性的快速概述,重点是你需要了解的首要内容:如何处理 Web 请求和响应。
FastAPI 是什么?
与任何 Web 框架一样,FastAPI 帮助您构建 Web 应用程序。每个框架都设计了一些操作以使某些操作更加简便——通过功能、省略和默认设置。顾名思义,FastAPI 的目标是开发 Web API,尽管您也可以将其用于传统的 Web 内容应用程序。
FastAPI 网站声称具有以下优势:
性能
在某些情况下,速度与 Node.js 和 Go 一样快,这在 Python 框架中是不寻常的。
更快的开发速度
没有锋利的边缘或怪异行为。
更好的代码质量
类型提示和模型有助于减少错误。
自动生成的文档和测试页面
比手动编辑 OpenAPI 描述更容易。
FastAPI 使用以下内容:
-
Python 类型提示
-
Starlette 用于 Web 机制,包括异步支持
-
Pydantic 用于数据定义和验证
-
特殊集成以利用和扩展其他功能
这种组合为 Web 应用程序,特别是 RESTful Web 服务,提供了令人愉悦的开发环境。
一个 FastAPI 应用程序
让我们编写一个微小的 FastAPI 应用——一个只有一个端点的 Web 服务。目前,我们处于我所谓的 Web 层,仅处理 Web 请求和响应。首先,安装我们将要使用的基本 Python 包:
-
FastAPI 框架:
pip install fastapi -
Uvicorn Web 服务器:
pip install uvicorn -
HTTPie 文本 Web 客户端:
pip install httpie -
Requests 同步 Web 客户端包:
pip install requests -
HTTPX 同步/异步 Web 客户端包:
pip install httpx
虽然 curl 是最著名的文本 Web 客户端,但我认为 HTTPie 更易于使用。此外,它默认使用 JSON 编码和解码,这与 FastAPI 更匹配。本章后面,你将看到一个包含访问特定端点所需 curl 命令行语法的截图。
让我们在 示例 3-1 中跟随一个内向的 Web 开发者,并将此代码保存为文件 hello.py。
示例 3-1. 一个害羞的端点(hello.py)
from fastapi import FastAPI
app = FastAPI()
@app.get("/hi")
def greet():
return "Hello? World?"
以下是需要注意的一些要点:
-
app是代表整个 Web 应用程序的顶级 FastAPI 对象。 -
@app.get("/hi")是一个 路径装饰器。它告诉 FastAPI 如下内容:-
对于此服务器上的
"/hi"URL 的请求应该被指向以下函数。 -
此装饰器仅适用于 HTTP 的
GET动词。您还可以使用其他 HTTP 动词(PUT、POST等)响应"/hi"URL,每个对应一个单独的函数。
-
-
def greet()是一个 path function,它是 HTTP 请求和响应的主要接触点。在本例中,它没有参数,但后续章节将展示 FastAPI 更多功能。
下一步是在 Web 服务器中运行此 Web 应用程序。FastAPI 本身不包含 Web 服务器,但推荐使用 Uvicorn。您可以以两种方式启动 Uvicorn 和 FastAPI Web 应用程序:外部或内部。
要通过命令行外部启动 Uvicorn,请参阅 示例 3-2。
Example 3-2. 使用命令行启动 Uvicorn
$ uvicorn hello:app --reload
hello 指的是 hello.py 文件,而 app 是其中的 FastAPI 变量名。
或者,您可以在应用程序本身内部启动 Uvicorn,如 示例 3-3。
Example 3-3. 在内部启动 Uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/hi")
def greet():
return "Hello? World?"
if __name__ == "__main__":
import uvicorn
uvicorn.run("hello:app", reload=True)
在任一情况下,reload 告诉 Uvicorn 如果 hello.py 更改了,重新启动 Web 服务器。在本章中,我们将频繁使用此自动重新加载功能。
无论您使用外部还是内部方法,默认情况下都将在您的计算机(名为 localhost)上使用端口 8000。如果您希望使用其他设置,两种方法都有 host 和 port 参数。
现在服务器有一个单一的端点(/hi),并准备接受请求。
让我们用多个 web 客户端测试:
-
对于浏览器,在顶部地址栏输入 URL。
-
对于 HTTPie,请输入显示的命令(
$表示您系统 shell 的命令提示符)。 -
对于 Requests 或 HTTPX,请在交互模式下使用 Python,并在
>>>提示后输入。
如前言所述,您输入的内容位于一个
bold monospaced font
并且输出在一个
normal monospaced font
示例 3-4 到 3-7 展示了测试 Web 服务器全新 /hi 端点的不同方式。
Example 3-4. 在浏览器中测试 /hi
http://localhost:8000/hi
Example 3-5. 使用 Requests 测试 /hi
>>> import requests
>>> r = requests.get("http://localhost:8000/hi")
>>> r.json()
'Hello? World?'
Example 3-6. 使用 HTTPX 测试 /hi,这几乎与 Requests 相同
>>> import httpx
>>> r = httpx.get("http://localhost:8000/hi")
>>> r.json()
'Hello? World?'
注意
无论您使用 Requests 还是 HTTPX 来测试 FastAPI 路由都无所谓。但是 第十三章 展示了在进行其他异步调用时使用 HTTPX 的情况。因此,本章的其余示例使用 Requests。
Example 3-7. 使用 HTTPie 测试 /hi
$ http localhost:8000/hi
HTTP/1.1 200 OK
content-length: 15
content-type: application/json
date: Thu, 30 Jun 2022 07:38:27 GMT
server: uvicorn
"Hello? World?"
使用 -b 参数在 示例 3-8 中跳过响应头,并只打印主体。
Example 3-8. 使用 HTTPie 测试 /hi,仅打印响应主体
$ http -b localhost:8000/hi
"Hello? World?"
示例 3-9 获取完整的请求头和响应,带有 -v。
Example 3-9. 使用 HTTPie 测试 /hi 并获取所有内容
$ http -v localhost:8000/hi
GET /hi HTTP/1.1
Accept: /
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8000
User-Agent: HTTPie/3.2.1
HTTP/1.1 200 OK
content-length: 15
content-type: application/json
date: Thu, 30 Jun 2022 08:05:06 GMT
server: uvicorn
"Hello? World?"
本书中的一些示例显示了默认的 HTTPie 输出(响应头和主体),而其他示例仅显示主体。
HTTP 请求
示例 3-9 仅包含一个特定请求:对 localhost 服务器上端口 8000 的 GET 请求 /hi URL。
Web 请求在 HTTP 请求的不同部分中存储数据,而 FastAPI 允许您顺利访问它们。从 示例 3-9 中的示例请求开始,示例 3-10 显示了 http 命令发送到 Web 服务器的 HTTP 请求。
示例 3-10. HTTP 请求
GET /hi HTTP/1.1
Accept: /
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8000
User-Agent: HTTPie/3.2.1
本请求包含以下内容:
-
动词 (
GET) 和路径 (/hi) -
任何查询参数(在此例中任何
?后面的文本,无) -
其他 HTTP 标头
-
没有请求体内容
FastAPI 将其解析为方便的定义:
Header
HTTP 标头
Path
URL
Query
查询参数(在 URL 结尾的 ? 后面)
Body
HTTP body
注意
FastAPI 提供数据来自 HTTP 请求的各个部分的方式是其最佳特性之一,也是大多数 Python Web 框架改进的一部分。所有需要的参数可以在路径函数内声明和直接提供,使用之前列表中的定义 (Path, Query 等),以及你编写的函数。这使用了一种称为依赖注入的技术,我们将在接下来的内容中讨论,并在 第六章 中详细展开。
通过添加一个名为 who 的参数,让我们的早期应用程序变得更加个性化,以回应那个哀求般的 Hello?。我们将尝试不同的方法来传递这个新参数:
-
在 URL 路径 中
-
作为查询参数,在 URL 的
?后面 -
在 HTTP body 中
-
作为 HTTP 标头
URL 路径
在 示例 3-11 中编辑 hello.py。
示例 3-11. 返回问候路径
from fastapi import FastAPI
app = FastAPI()
@app.get("/hi/{who}")
def greet(who):
return f"Hello? {who}?"
一旦从编辑器保存此更改,Uvicorn 应该重新启动。(否则,我们将创建 hello2.py 等,并每次重新运行 Uvicorn。)如果有拼写错误,请不断尝试直到修复,Uvicorn 不会对你造成困扰。
在 URL 中添加 {who}(在 @app.get 后面)告诉 FastAPI 在该位置期望一个名为 who 的变量。FastAPI 然后将其分配给下面 greet() 函数中的 who 参数。这显示了路径装饰器和路径函数之间的协调。
注意
此处不要使用 Python f-string 来修改 URL 字符串 ("/hi/{who}")。大括号由 FastAPI 本身用于匹配作为路径参数的 URL 片段。
在示例 3-12 到 3-14 中,使用先前讨论的各种方法测试这个修改后的端点。
示例 3-12. 在浏览器中测试 /hi/Mom
localhost:8000/hi/Mom
示例 3-13. 使用 HTTPie 测试 /hi/Mom
$ http localhost:8000/hi/Mom
HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Thu, 30 Jun 2022 08:09:02 GMT
server: uvicorn
"Hello? Mom?"
示例 3-14. 使用 Requests 测试 /hi/Mom
>>> import requests
>>> r = requests.get("http://localhost:8000/hi/Mom")
>>> r.json()
'Hello? Mom?'
在每种情况下,字符串 "Mom" 被作为 URL 的一部分传递,并作为 greet() 路径函数中的 who 变量传递,并作为响应的一部分返回。
在每种情况下,响应都是 JSON 字符串(取决于使用的测试客户端是单引号还是双引号) "Hello? Mom?"。
查询参数
查询参数是在 URL 中 *name=value* 字符串之后的 ? 后面,用 & 字符分隔。在 示例 3-15 中再次编辑 hello.py。
示例 3-15. 返回问候查询参数
from fastapi import FastAPI
app = FastAPI()
@app.get("/hi")
def greet(who):
return f"Hello? {who}?"
端点函数再次被定义为 greet(who),但是这次在前一个装饰器行中 URL 上没有 {who},所以 FastAPI 现在假设 who 是一个查询参数。测试示例 3-16 和 3-17。
示例 3-16. 使用浏览器测试 示例 3-15
localhost:8000/hi?who=Mom
示例 3-17. 使用 HTTPie 测试 示例 3-15
$ http -b localhost:8000/hi?who=Mom
"Hello? Mom?"
在 示例 3-18 中,您可以使用查询参数调用 HTTPie(注意 ==)。
示例 3-18. 使用 HTTPie 和参数测试 示例 3-15
$ http -b localhost:8000/hi who==Mom
"Hello? Mom?"
您可以在 HTTPie 中使用多个这些参数,将它们作为空格分隔的参数更容易输入。
示例 3-19 和 3-20 展示了 Requests 的相同替代方案。
示例 3-19. 使用 Requests 测试 示例 3-15
>>> import requests
>>> r = requests.get("http://localhost:8000/hi?who=Mom")
>>> r.json()
'Hello? Mom?'
示例 3-20. 使用 Requests 和参数测试 示例 3-15
>>> import requests
>>> params = {"who": "Mom"}
>>> r = requests.get("http://localhost:8000/hi", params=params)
>>> r.json()
'Hello? Mom?'
在每种情况下,您以一种新的方式提供字符串 "Mom",并将其传递给路径函数并通过最终的响应。
主体
我们可以向 GET 端点提供路径或查询参数,但不能从请求体中获取值。在 HTTP 中,GET 应该是 幂等 的——一个计算机术语,意思是 询问相同的问题,得到相同的答案。HTTP GET 应该只返回内容。请求体用于在创建(POST)或更新(PUT 或 PATCH)时将内容发送到服务器。第九章 展示了绕过此限制的方法。
因此,在 示例 3-21 中,让我们将端点从 GET 更改为 POST。(严格来说,我们没有创建任何内容,所以 POST 不合适,但如果 RESTful 大师起诉我们,那么嘿,看看酷炫的法院。)
示例 3-21. 返回问候主体
from fastapi import FastAPI, Body
app = FastAPI()
@app.post("/hi")
def greet(who:str = Body(embed=True)):
return f"Hello? {who}?"
注意
这次需要 Body(embed=True) 告诉 FastAPI,我们这次从 JSON 格式的请求体中获取 who 的值。embed 部分意味着它应该像 {"who": "Mom"} 这样看起来,而不只是 "Mom"。
尝试在 示例 3-22 中使用 HTTPie 进行测试,使用 -v 显示生成的请求体(注意单个 = 参数表示 JSON 主体数据)。
示例 3-22. 使用 HTTPie 测试 示例 3-21
$ http -v localhost:8000/hi who=Mom
POST /hi HTTP/1.1
Accept: application/json, /;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 14
Content-Type: application/json
Host: localhost:8000
User-Agent: HTTPie/3.2.1
{
"who": "Mom"
}
HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Thu, 30 Jun 2022 08:37:00 GMT
server: uvicorn
"Hello? Mom?"
最后,在 示例 3-23 中使用 Requests 进行测试,其中使用其 json 参数将 JSON 编码数据传递到请求体中。
示例 3-23. 使用 Requests 测试 示例 3-21
>>> import requests
>>> r = requests.post("http://localhost:8000/hi", json={"who": "Mom"})
>>> r.json()
'Hello? Mom?'
HTTP 头信息
最后,在 示例 3-24 中尝试将问候参数作为 HTTP 头信息传递。
示例 3-24. 返回问候头信息
from fastapi import FastAPI, Header
app = FastAPI()
@app.post("/hi")
def greet(who:str = Header()):
return f"Hello? {who}?"
让我们在 示例 3-25 中只使用 HTTPie 进行测试。它使用 *name:value* 来指定 HTTP 头信息。
示例 3-25. 使用 HTTPie 测试 示例 3-24
$ http -v localhost:8000/hi who:Mom
GET /hi HTTP/1.1
Accept: */\*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8000
User-Agent: HTTPie/3.2.1
who: Mom
HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Mon, 16 Jan 2023 05:14:46 GMT
server: uvicorn
"Hello? Mom?"
FastAPI 将 HTTP 头键转换为小写,并将连字符 (-) 转换为下划线 (_)。因此,您可以在示例 3-26 和 3-27 中像这样打印 HTTP User-Agent 头信息的值。
示例 3-26. 返回 User-Agent 头信息(hello.py)
from fastapi import FastAPI, Header
app = FastAPI()
@app.post("/agent")
def get_agent(user_agent:str = Header()):
return user_agent
示例 3-27. 使用 HTTPie 测试 User-Agent 头信息
$ http -v localhost:8000/agent
GET /agent HTTP/1.1
Accept: */\*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: localhost:8000
User-Agent: HTTPie/3.2.1
HTTP/1.1 200 OK
content-length: 14
content-type: application/json
date: Mon, 16 Jan 2023 05:21:35 GMT
server: uvicorn
"HTTPie/3.2.1"
多个请求数据
您可以在同一个路径函数中使用多个方法。也就是说,您可以从 URL、查询参数、HTTP 主体、HTTP 头部、cookie 等获取数据。并且您可以编写自己的依赖函数,以特殊方式处理和组合它们,例如用于分页或身份验证。您将在 第六章 和 第 III 部分 的各个章节中看到其中的一些。
哪种方法最好?
这里有一些建议:
-
在传递 URL 中的参数时,遵循 RESTful 准则是标准做法。
-
查询字符串通常用于提供可选参数,如分页。
-
通常用于更大输入的主体,例如整体或部分模型。
在每种情况下,如果您在数据定义中提供类型提示,您的参数将由 Pydantic 自动进行类型检查。这确保它们既存在又正确。
HTTP 响应
默认情况下,FastAPI 将从端点函数返回的任何内容转换为 JSON;HTTP 响应具有标题行 Content-type: application/json。因此,虽然 greet() 函数最初返回字符串 "Hello? World?",但 FastAPI 将其转换为 JSON。这是 FastAPI 为简化 API 开发选择的默认之一。
在这种情况下,Python 字符串 "Hello? World?" 被转换为其等效的 JSON 字符串 "Hello? World?",它仍然是同样的字符串。但是无论您返回什么,FastAPI 都会将其转换为 JSON,无论是内置的 Python 类型还是 Pydantic 模型。
状态码
默认情况下,FastAPI 返回 200 状态码;异常会引发 4*xx* 代码。
在路径装饰器中,指定应该在一切顺利时返回的 HTTP 状态码(异常将生成自己的代码并覆盖它)。将 示例 3-28 中的代码添加到您的 hello.py 中的某个位置,并使用 示例 3-29 进行测试。
示例 3-28. 指定 HTTP 状态码(添加到 hello.py)
@app.get("/happy")
def happy(status_code=200):
return ":)"
示例 3-29. 测试 HTTP 状态码
$ http localhost:8000/happy
HTTP/1.1 200 OK
content-length: 4
content-type: application/json
date: Sun, 05 Feb 2023 04:37:32 GMT
server: uvicorn
":)"
头部
您可以像 示例 3-30 中那样注入 HTTP 响应头部(您不需要返回 response)。
示例 3-30. 设置 HTTP 头部(添加到 hello.py)
from fastapi import Response
@app.get("/header/{name}/{value}")
def header(name: str, value: str, response:Response):
response.headers[name] = value
return "normal body"
让我们看看是否成功(示例 3-31)。
示例 3-31. 测试响应的 HTTP 头部
$ http localhost:8000/header/marco/polo
HTTP/1.1 200 OK
content-length: 13
content-type: application/json
date: Wed, 31 May 2023 17:47:38 GMT
marco: polo
server: uvicorn
"normal body"
响应类型
响应类型(从 fastapi.responses 导入这些类)包括以下内容:
-
JSONResponse(默认) -
HTMLResponse -
PlainTextResponse -
RedirectResponse -
FileResponse -
StreamingResponse
我将在 第十五章 中进一步讨论最后两点。
对于其他输出格式(也称为 MIME 类型),您可以使用一个通用的 Response 类,需要以下内容:
content
字符串或字节
media_type
字符串 MIME 类型
status_code
HTTP 整数状态码
headers
一个字符串 dict
类型转换
路径函数可以返回任何内容,默认情况下(使用JSONResponse),FastAPI 将其转换为 JSON 字符串并返回,包括任何 Pydantic 模型类。
但是它是如何做到的呢?如果你使用过 Python 的 json 库,可能已经看到它在给定某些数据类型(如datetime)时会引发异常。FastAPI 使用名为jsonable_encoder()的内部函数将任何数据结构转换为“可 JSON 化”的 Python 数据结构,然后调用通常的json.dumps()将其转换为 JSON 字符串。示例 3-32 展示了一个可以用 pytest 运行的测试。
示例 3-32. 使用jsonable_encoder()避免 JSON 爆炸
import datetime
import pytest
from fastapi.encoders import jsonable_encoder
import json
@pytest.fixture
def data():
return datetime.datetime.now()
def test_json_dump(data):
with pytest.raises(Exception):
_ = json.dumps(data)
def test_encoder(data):
out = jsonable_encoder(data)
assert out
json_out = json.dumps(out)
assert json_out
模型类型和response_model
可能存在具有许多相同字段但一个专门用于用户输入、一个用于输出和一个用于内部使用的不同类。这些变体的一些原因可能包括以下几点:
-
从输出中删除一些敏感信息,比如去识别个人医疗数据,如果你遇到了《健康保险可移植性和责任法案》(HIPAA)的要求。
-
向用户输入添加字段(例如创建日期和时间)。
示例 3-33 展示了一个虚构案例的三个相关类:
-
TagIn是定义用户需要提供的类(在本例中仅为名为tag的字符串)。 -
Tag是基于TagIn创建的,增加了两个字段:created(创建此Tag的时间)和secret(一个内部字符串,可能存储在数据库中,但不应该对外界公开)。 -
TagOut是定义可以返回给用户(通过查找或搜索端点)的类。它包含原始TagIn对象的tag字段及其派生的Tag对象,还有为Tag生成的created字段,但不包括secret。
示例 3-33. 模型变体(model/tag.py)
from datetime import datetime
from pydantic import BaseClass
class TagIn(BaseClass):
tag: str
class Tag(BaseClass):
tag: str
created: datetime
secret: str
class TagOut(BaseClass):
tag: str
created: datetime
你可以以不同的方式从 FastAPI 路径函数返回除默认 JSON 以外的数据类型。一种方法是在路径装饰器中使用response_model参数,让 FastAPI 返回其他内容。FastAPI 将删除你返回的对象中出现但未在response_model指定对象中的任何字段。
在示例 3-34 中,假设你编写了一个名为service/tag.py的新服务模块,其中包含 create() 和 get() 函数,为这个 web 模块提供调用。这些低层次的细节在此不重要。重要的是底部的get_one()路径函数以及其路径装饰器中的response_model=TagOut。这会自动将内部的Tag对象转换为经过清理的TagOut对象。
示例 3-34. 使用response_model返回不同的响应类型(web/tag.py)
import datetime
from model.tag import TagIn, Tag, TagOut
import service.tag as service
@app.post('/')
def create(tag_in: TagIn) -> TagIn:
tag: Tag = Tag(tag=tag_in.tag, created=datetime.utcnow(),
secret="shhhh")
service.create(tag)
return tag_in
@app.get('/{tag_str}', response_model=TagOut)
def get_one(tag_str: str) -> TagOut:
tag: Tag = service.get(tag_str)
return tag
尽管我们返回了一个Tag,response_model将其转换为TagOut。
自动化文档
本节假设您正在运行来自示例 3-21 的 Web 应用程序版本,该版本通过POST请求将who参数发送到 *http://localhost:8000/hi*。
说服你的浏览器访问 URL http://localhost:8000/docs。
你会看到一些类似于图 3-1 的东西(我裁剪了以下截图,以强调特定区域)。
图 3-1. 生成的文档页面
那是从哪里来的?
FastAPI 从你的代码生成 OpenAPI 规范,并包括此页面来显示和测试所有你的端点。这只是它秘密酱料的一部分。
点击绿色框右侧的下箭头以打开它以进行测试(图 3-2)。
图 3-2. 打开文档页面
点击右侧的“试一试”按钮。现在你会看到一个区域,让你在主体部分输入一个值(图 3-3)。
图 3-3. 数据输入页面
点击那个"string"。将它改成**"Cousin Eddie"**(保持双引号)。然后点击底部的蓝色执行按钮。
现在看看执行按钮下面的响应部分(图 3-4)。
“响应主体”框显示了出现了 Cousin Eddie。
所以,这是测试网站的另一种方式(除了之前使用浏览器、HTTPie 和 Requests 的例子)。
图 3-4. 响应页面
顺便提一下,在响应显示的 Curl 框中可以看到,与使用 HTTPie 相比,使用 curl 进行命令行测试需要更多的输入。HTTPie 的自动 JSON 编码在这里非常有帮助。
提示
这种自动化文档实际上非常重要。随着您的 Web 服务增长到数百个端点,一个始终更新的文档和测试页面将非常有帮助。
复杂数据
这些示例仅展示了如何将一个字符串传递给端点。许多端点,特别是GET或DELETE端点,可能根本不需要参数,或者只需要一些简单的参数,如字符串和数字。但是,在创建(POST)或修改(PUT或PATCH)资源时,我们通常需要更复杂的数据结构。第五章展示了 FastAPI 如何使用 Pydantic 和数据模型来实现这些操作的清洁方法。
复习
在这一章中,我们使用了 FastAPI 来创建一个只有一个端点的网站。多个 Web 客户端进行了测试:一个是网页浏览器,另外还有 HTTPie 文本程序、Requests Python 包和 HTTPX Python 包。从一个简单的GET调用开始,请求参数通过 URL 路径、查询参数和 HTTP 头部发送到服务器。然后,HTTP 主体用于向POST端点发送数据。接着,本章展示了如何返回不同类型的 HTTP 响应。最后,一个自动生成的表单页面为第四个测试客户端提供了文档和实时表单。
这份 FastAPI 概述将在 第八章 中进一步展开。
第四章:异步、并发和 Starlette 之旅
Starlette 是一个轻量级的 ASGI 框架/工具包,非常适合在 Python 中构建异步 Web 服务。
Starlette 的创建者 Tom Christie
预览
上一章简要介绍了开发者在编写新 FastAPI 应用时会遇到的第一件事情。本章强调了 FastAPI 的底层 Starlette 库,特别是其对async处理的支持。在概述 Python 中“同时执行多个任务”的多种方式之后,您将看到其最新的async和await关键字如何被整合到 Starlette 和 FastAPI 中。
Starlette
FastAPI 的许多 Web 代码基于Tom Christie 创建的 Starlette 包。它可以作为自己的 Web 框架使用,也可以作为其他框架(如 FastAPI)的库使用。与任何其他 Web 框架一样,Starlette 处理所有常规的 HTTP 请求解析和响应生成。它类似于Flask 底层的 Werkzeug。
但是它最重要的特性是支持现代 Python 异步 Web 标准:ASGI。直到现在,大多数 Python Web 框架(如 Flask 和 Django)都是基于传统的同步WSGI 标准。因为 Web 应用程序经常连接到较慢的代码(例如数据库、文件和网络访问),ASGI 避免了基于 WSGI 的应用程序的阻塞和忙等待。
因此,使用 Starlette 及其相关框架的 Python Web 包是最快的,甚至与 Go 和 Node.js 应用程序一较高下。
并行计算类型
在深入了解 Starlette 和 FastAPI 提供的async支持的详细信息之前,了解一下我们可以实现concurrency的多种方式是很有用的。
在parallel计算中,任务同时分布在多个专用 CPU 上。这在像图形和机器学习这样的“数值计算”应用程序中很常见。
在concurrent计算中,每个 CPU 在多个任务之间切换。某些任务比其他任务花费的时间更长,我们希望减少所需的总时间。读取文件或访问远程网络服务比在 CPU 中运行计算慢得多,字面上慢了成千上百万倍。
Web 应用程序完成了大量缓慢的工作。我们如何使 Web 服务器或任何服务器运行得更快?本节讨论了一些可能性,从系统范围内到本章重点:FastAPI 对 Python 的async和await的实现。
分布和并行计算
如果您有一个真正大型的应用程序——一个在单个 CPU 上会显得吃力的应用程序——您可以将其分解成片段,并使这些片段在单个机器的多个 CPU 上运行或在多台机器上运行。您可以以许多种方式做到这一点,如果您拥有这样的应用程序,您已经了解了其中的一些方式。管理所有这些片段比管理单个服务器更复杂和昂贵。
本书关注的是可以放在单个盒子上的小到中型应用程序。这些应用程序可以有同步和异步代码的混合,由 FastAPI 很好地管理。
操作系统进程
操作系统(或OS,因为打字疼)调度资源:内存、CPU、设备、网络等等。它运行的每个程序都在一个或多个进程中执行其代码。操作系统为每个进程提供受管理的、受保护的资源访问,包括它们何时可以使用 CPU。
大多数系统使用抢占式进程调度,不允许任何进程独占 CPU、内存或任何其他资源。操作系统根据其设计和设置不断挂起和恢复进程。
对于开发者来说,好消息是:这不是你的问题!但通常伴随着好消息而来的坏消息是:即使你想改变,你也不能做太多事情。
对于 CPU 密集型 Python 应用程序,通常的解决方案是使用多个进程并让操作系统管理它们。Python 有一个multiprocessing module用于此目的。
操作系统线程
您也可以在单个进程内运行控制线程。Python 的threading package管理这些线程。
当程序受 I/O 限制时,通常建议使用线程,而当程序受 CPU 限制时,建议使用多个进程。但线程编程很棘手,可能导致难以找到的错误。在介绍 Python中,我把线程比作在闹鬼的房子中飘荡的幽灵:独立而看不见,只能通过它们的效果来检测。嘿,谁移动了那个烛台?
传统上,Python 保持基于进程和基于线程的库分开。开发者必须学习其中的奥秘细节才能使用它们。一个更近期的包叫做concurrent.futures,它是一个更高级别的接口,使它们更易于使用。
如您将看到的,通过新的 async 函数,您可以更轻松地获得线程的好处。FastAPI 还通过线程池管理普通同步函数(def而不是async def)的线程。
绿色线程
更神秘的机制由绿色线程,例如greenlet,gevent和Eventlet呈现出来。这些线程是协作式的(而不是抢占式的)。它们类似于操作系统线程,但在用户空间(即您的程序)而不是操作系统内核中运行。它们通过猴子补丁标准 Python 函数(在运行时修改标准 Python 函数)使并发代码看起来像正常的顺序代码:当它们阻塞等待 I/O 时,它们放弃控制权。
操作系统线程比操作系统进程“轻”(使用更少内存),而绿色线程比操作系统线程更轻。在一些基准测试中,所有异步方法通常比它们的同步对应方法更快。
注意
在你读完本章之后,你可能会想知道哪个更好:gevent 还是 asyncio?我认为并没有一个适用于所有情况的单一偏好。绿色线程早在之前就已实现(使用了来自多人在线游戏Eve Online的思想)。本书采用了 Python 的标准 asyncio,这与线程相比更简单,性能也更好。
回调
互动应用程序的开发者,如游戏和图形用户界面的开发者,可能已经熟悉回调。您编写函数并将其与事件(如鼠标点击、按键或时间)关联起来。这个类别中最显著的 Python 包是Twisted。它的名字反映了基于回调的程序有点“内外颠倒”和难以理解的现实。
Python 生成器
像大多数语言一样,Python 通常是顺序执行代码。当您调用一个函数时,Python 会从它的第一行运行到结束或return。
但是在 Python 的生成器函数中,您可以在任何点停止并返回,并在以后返回到该点。这个技巧就是yield关键字。
在一集Simpsons中,Homer 将他的车撞到了一尊鹿的雕像上,接着有三行对话。 示例 4-1 定义了一个普通的 Python 函数来return这些行作为列表,并让调用者对其进行迭代。
示例 4-1. 使用 return
>>> def doh():
... return ["Homer: D'oh!", "Marge: A deer!", "Lisa: A female deer!"]
...
>>> for line in doh():
... print(line)
...
Homer: D'oh!
Marge: A deer!
Lisa: A female deer!
当列表相对较小时,这种方法非常有效。但是如果我们要获取所有Simpsons剧集的对话呢?列表会占用内存。
示例 4-2 展示了一个生成器函数如何分配这些行。
示例 4-2. 使用 yield
>>> def doh2():
... yield "Homer: D'oh!"
... yield "Marge: A deer!"
... yield "Lisa: A female deer!"
...
>>> for line in doh2():
... print(line)
...
Homer: D'oh!
Marge: A deer!
Lisa: A female deer!
我们不是在普通函数doh()返回的列表上进行迭代,而是在生成器函数doh2()返回的生成器对象上进行迭代。实际的迭代(for...in)看起来一样。Python 从doh2()返回第一个字符串,但会追踪它的位置以便于下一次迭代,直到函数耗尽对话。
任何包含yield的函数都是生成器函数。鉴于这种能力可以返回到函数中间并恢复执行,下一节看起来像是一个合理的适应。
Python 的 async、await 和 asyncio
Python 的asyncio特性已经在不同的版本中引入。您至少要运行 Python 3.7,这是async和await成为保留关键字的版本。
以下示例展示了一个只有在异步运行时才会有趣的笑话。自己运行这两个示例,因为时机很重要。
首先,运行不好笑的 示例 4-3。
示例 4-3. 枯燥
>>> import time
>>>
>>> def q():
... print("Why can't programmers tell jokes?")
... time.sleep(3)
...
>>> def a():
... print("Timing!")
...
>>> def main():
... q()
... a()
...
>>> main()
Why can't programmers tell jokes?
Timing!
在问题和答案之间会有三秒钟的间隙。哈欠。
但是异步的 示例 4-4 有些不同。
示例 4-4. 滑稽
>>> import asyncio
>>>
>>> async def q():
... print("Why can't programmers tell jokes?")
... await asyncio.sleep(3)
...
>>> async def a():
... print("Timing!")
...
>>> async def main():
... await asyncio.gather(q(), a())
...
>>> asyncio.run(main())
Why can't programmers tell jokes?
Timing!
这次,答案应该在问题之后立即出现,然后是三秒的沉默——就像一个程序员在讲述一样。哈哈!咳咳。
注
在 示例 4-4 中,我使用了 asyncio.gather() 和 asyncio.run(),但调用异步函数有多种方法。在使用 FastAPI 时,你不需要使用这些。
运行 示例 4-4 时,Python 认为是这样的:
-
执行
q()。现在只是第一行。 -
好吧,你懒惰的异步
q(),我已经设置好秒表,三秒钟后我会回来找你。 -
与此同时,我将运行
a(),并立即打印答案。 -
没有其他的
await,所以回到q()。 -
无聊的事件循环!我将坐在这里,等待剩下的三秒钟。
-
好了,现在我完成了。
此示例使用 asyncio.sleep() 用于需要一些时间的函数,就像读取文件或访问网站的函数一样。在那些可能大部分时间都在等待的函数前面加上 await。该函数在其 def 前需要有 async。
注意
如果你定义了一个带有 async def 的函数,它的调用者必须在调用它之前放置一个 await。而且调用者本身必须声明为 async def,并且 其 调用者必须 await 它,以此类推。
顺便说一句,即使函数中没有调用其他异步函数的 await 调用,你也可以将函数声明为 async。这不会有害处。
FastAPI 和异步
经过那段漫长的田野之旅,让我们回到 FastAPI,看看为什么任何这些都很重要。
因为 Web 服务器花费大量时间在等待上,通过避免部分等待,即并发,可以提高性能。其他 Web 服务器使用了之前提到的多种方法:线程、gevent 等等。FastAPI 作为最快的 Python Web 框架之一的原因之一是其整合了异步代码,通过底层 Starlette 包的 ASGI 支持,以及其自己的一些发明。
注意
单独使用 async 和 await 并不能使代码运行更快。事实上,由于异步设置的开销,它可能会慢一点。async 的主要用途是避免长时间等待 I/O。
现在,让我们看看我们之前的 Web 端点调用,并了解如何使它们异步。
FastAPI 文档中将将 URL 映射到代码的函数称为 路径函数。我还称它们为 Web 端点,你在 第三章 中看到它们的同步示例。让我们制作一些异步的。就像之前的示例一样,我们现在只使用简单的类型,比如数字和字符串。第五章 引入了 类型提示 和 Pydantic,我们将需要处理更复杂的数据结构。
示例 4-5 重新访问了上一章的第一个 FastAPI 程序,并将其改为异步。
示例 4-5. 一个害羞的异步端点(greet_async.py)
from fastapi import FastAPI
import asyncio
app = FastAPI()
@app.get("/hi")
async def greet():
await asyncio.sleep(1)
return "Hello? World?"
要运行那一块 Web 代码,你需要像 Uvicorn 这样的 Web 服务器。
第一种方法是在命令行上运行 Uvicorn:
$ uvicorn greet_async:app
第二种方法,就像 示例 4-6 中一样,是在示例代码内部从主程序而不是模块中调用 Uvicorn。
示例 4-6. 另一个害羞的异步端点(greet_async_uvicorn.py)
from fastapi import FastAPI
import asyncio
import uvicorn
app = FastAPI()
@app.get("/hi")
async def greet():
await asyncio.sleep(1)
return "Hello? World?"
if __name__ == "__main__":
uvicorn.run("greet_async_uvicorn:app")
当作为独立程序运行时,Python 将其命名为main。那个if __name__...的东西是 Python 只在被调用为主程序时运行它的方式。是的,这很丑陋。
这段代码在返回其畏缩的问候之前将暂停一秒钟。与使用标准sleep(1)函数的同步函数唯一的区别是,异步示例允许 Web 服务器在此期间处理其他请求。
使用asyncio.sleep(1)模拟可能需要一秒钟的真实世界函数,比如调用数据库或下载网页。后面的章节将展示从 Web 层到服务层再到数据层的这些调用的实际示例,实际上在进行真正的工作时花费这段等待时间。
FastAPI 在接收到对 URL /hi 的GET请求时会自行调用这个异步greet()路径函数。您不需要在任何地方添加await。但是对于您创建的任何其他async def函数定义,调用者必须在每次调用前加上await。
注意
FastAPI 运行一个异步事件循环,协调异步路径函数,并为同步路径函数运行一个线程池。开发者不需要了解复杂的细节,这是一个很大的优点。例如,您不需要像之前的(独立的、非 FastAPI)笑话示例中那样运行asyncio.gather()或asyncio.run()方法。
直接使用 Starlette
FastAPI 没有像它对 Pydantic 那样公开 Starlette。Starlette 主要是引擎室内不断运行的机器。
但是如果你感兴趣,你可以直接使用 Starlette 来编写 Web 应用程序。前一章节的示例 3-1 可能看起来像示例 4-7。
示例 4-7. 使用 Starlette:starlette_hello.py
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
async def greeting(request):
return JSONResponse('Hello? World?')
app = Starlette(debug=True, routes=[
Route('/hi', greeting),
])
使用以下命令运行这个 Web 应用程序:
$ uvicorn starlette_hello:app
在我看来,FastAPI 的新增功能使得 Web API 的开发更加容易。
插曲:清洁线索之屋
你拥有一个小(非常小:只有你一个人)的清洁公司。你一直靠泡面过活,但刚刚签下的合同将让你能够买更好的泡面。
您的客户购买了一座建造在 Clue 棋盘游戏风格中的老宅,并希望很快在那里举办角色派对。但是这个地方一团糟。如果玛丽·康多(Marie Kondo)看到这个地方,她可能会做如下事情:
-
尖叫
-
笑喷
-
逃跑
-
以上所有内容
您的合同包括一个速度奖金。如何在最少的时间内彻底清洁这个地方?最好的方法本来应该是有更多的线索保护单元(CPU),但是你就是全部。
所以你可以尝试以下其中之一:
-
在一个房间里做完所有事情,然后再下一个房间,以此类推。
-
在一个房间内完成一个特定任务,然后再下一个房间,以此类推。比如在厨房和餐厅里擦拭银器,或者在台球室里擦拭台球。
你选择的方法的总时间会不同吗?也许会。但更重要的是要考虑是否需要等待任何步骤的时间。一个例子可能是在地面上:清洁地毯和打蜡后,它们可能需要几个小时才能干透,然后才能把家具搬回去。
所以,这是你每个房间的计划:
-
清洁所有静态部分(窗户等)。
-
把房间里的所有家具移到大厅。
-
从地毯和/或硬木地板上去除多年的污垢。
-
做以下任何一种:
-
等地毯或打蜡干透,但告别你的奖金吧。
-
现在去下一个房间,然后重复。在最后一个房间之后,把家具搬回第一个房间,依此类推。
-
等待干燥的方法是同步的,如果时间不是一个因素并且你需要休息的话可能是最好的选择。第二种是异步的,为每个房间节省等待时间。
假设你选择了异步路径,因为有钱可赚。你让旧的垃圾发光,从你感激的客户那里获得了奖金。后来的派对结果非常成功,除了这些问题:
-
一个没有梗的客人扮成了马里奥。
-
你在舞厅里打蜡过度了,在醉醺醺的普拉姆教授穿着袜子溜冰,最终撞到桌子上,把香槟洒在了斯卡雷特小姐身上。
故事的道德:
-
需求可能会有冲突和/或奇怪。
-
估算时间和精力可能取决于许多因素。
-
顺序任务可能更多是一种艺术而不是科学。
-
当所有事情都完成时,你会感觉很棒。嗯,拉面。
复习
在总览了增加并发方式后,本章扩展了使用最近的 Python 关键字async和await的函数。它展示了 FastAPI 和 Starlette 如何处理传统的同步函数和这些新的异步函数。
下一章介绍了 FastAPI 的第二部分:Pydantic 如何帮助您定义数据。
第五章:Pydantic、类型提示和模型之旅
使用 Python 类型提示进行数据验证和设置管理。
快速且可扩展,Pydantic 与您的 linters/IDE/大脑完美配合。使用纯粹、规范的 Python 3.6+ 定义数据,然后用 Pydantic 进行验证。
Samuel Colvin,Pydantic 的开发者
预览
FastAPI 主要依赖于一个名为 Pydantic 的 Python 包。它使用模型(Python 对象类)来定义数据结构。在编写更大型应用程序时,这些在 FastAPI 应用中广泛使用,并且是一个真正的优势。
类型提示
是时候了解更多关于 Python 类型提示 的内容了。
第二章 提到,在许多计算机语言中,变量直接指向内存中的值。这要求程序员声明其类型,以确定值的大小和位数。在 Python 中,变量只是与对象相关联的名称,而对象才有类型。
在标准编程中,变量通常与相同的对象相关联。如果我们为该变量关联一个类型提示,我们可以避免一些编程错误。因此,Python 在语言中添加了类型提示,位于标准 typing 模块中。Python 解释器会忽略类型提示语法,并将程序运行为若不存在一样。那么,它的意义何在?
你可能在一行中将变量视为字符串,但后来忘记了,并将其分配给不同类型的对象。虽然其他语言的编译器会抱怨,但 Python 不会。标准 Python 解释器将捕获常规语法错误和运行时异常,但不会检查变量类型的混合。像 mypy 这样的辅助工具会关注类型提示,并警告您任何不匹配的情况。
Python 开发者可以利用提示,编写超越类型错误检查的工具。接下来的部分将描述 Pydantic 包的开发过程,以解决一些不太明显的需求。稍后,您将看到它与 FastAPI 的集成,大大简化了许多 Web 开发问题的处理。
顺便说一句,类型提示是什么样的?变量有一种语法,函数返回值有另一种语法。
变量类型提示可能仅包括类型:
*name*: *type*
或者初始化变量并赋予一个值:
*name*: *type* = *value*
类型 可以是标准的 Python 简单类型,如 int 或 str,也可以是集合类型,如 tuple、list 或 dict:
thing: str = "yeti"
注意
在 Python 3.9 之前,您需要从 typing 模块导入这些标准类型名称的大写版本:
from typing import Str
thing: Str = "yeti"
这里有一些初始化的示例:
physics_magic_number: float = 1.0/137.03599913
hp_lovecraft_noun: str = "ichor"
exploding_sheep: tuple = "sis", "boom", bah!"
responses: dict = {"Marco": "Polo", "answer": 42}
您还可以包含集合的子类型:
*name*: dict[*keytype*, *valtype*] = {*key1*: *val1*, *key2*: *val2*}
typing 模块对子类型有有用的额外功能;最常见的如下:
Any
任何类型
Union
任何指定的类型,比如 Union[str, int]。
注意
在 Python 3.10 及以上版本中,您可以使用 *type1* | *type2* 而不是 Union[*type1*, *type2*]。
对于 Python dict 的 Pydantic 定义示例如下:
from typing import Any
responses: dict[str, Any] = {"Marco": "Polo", "answer": 42}
或者,稍微具体一点:
from typing import Union
responses: dict[str, Union[str, int]] = {"Marco": "Polo", "answer": 42}
或(Python 3.10 及以上版本):
responses: dict[str, str | int] = {"Marco": "Polo", "answer": 42}
注意,类型提示的变量行是合法的 Python 语法,但裸变量行不是:
$ python
...
>>> thing0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name *thing0* is not defined
>>> thing0: str
此外,正常的 Python 解释器无法捕捉到错误的类型使用:
$ python
...
>>> thing1: str = "yeti"
>>> thing1 = 47
但它们将被 mypy 捕获。如果您尚未安装它,请运行 pip install mypy。将这两行保存到一个名为 stuff.py 的文件中,¹,然后尝试这样做:
$ mypy stuff.py
stuff.py:2: error: Incompatible types in assignment
(expression has type "int", variable has type "str")
Found 1 error in 1 file (checked 1 source file)
函数返回类型提示使用箭头而不是冒号:
*function*(*args*) -> *type*:
下面是一个 Pydantic 函数返回的示例:
def get_thing() -> str:
return "yeti"
您可以使用任何类型,包括您定义的类或它们的组合。您将在几页中看到这一点。
数据分组
通常,我们需要将一组相关的变量放在一起,而不是传递许多单独的变量。我们如何将多个变量作为一组集成,并保留类型提示?
让我们抛开前几章中单调的问候例子,从现在开始使用更丰富的数据。就像本书的其余部分一样,我们将使用神秘动物(虚构的生物)的例子,以及(同样虚构的)探险家。我们的初始神秘动物定义仅包含以下字符串变量:
name
键
country
两个字符的 ISO 国家代码(3166-1 alpha 2)或 * = all
area
可选;美国州或其他国家的分区
description
自由格式
aka
也称为…
探险家将拥有以下内容:
name
键
country
两个字符的 ISO 国家代码
description
自由格式
Python 的历史数据分组结构(超出基本的 int、string 等)在这里列出:
tuple
一个不可变的对象序列
list
一个可变的对象序列
set
可变的不同对象
dict
可变的键-值对象对(键必须是不可变类型)
元组(示例 5-1)和列表(示例 5-2)仅允许您通过其偏移访问成员变量,因此您必须记住每个位置的内容。
示例 5-1. 使用元组
>>> tuple_thing = ("yeti", "CN", "Himalayas",
"Hirsute Himalayan", "Abominable Snowman")
>>> print("Name is", tuple_thing[0])
Name is yeti
示例 5-2. 使用列表
>>> list_thing = ["yeti", "CN", "Himalayas",
"Hirsute Himalayan", "Abominable Snowman"]
>>> print("Name is", list_thing[0])
Name is yeti
示例 5-3 表明,通过为整数偏移定义名称,您可以得到更详细的解释。
示例 5-3. 使用元组和命名偏移
>>> NAME = 0
>>> COUNTRY = 1
>>> AREA = 2
>>> DESCRIPTION = 3
>>> AKA = 4
>>> tuple_thing = ("yeti", "CN", "Himalayas",
"Hirsute Himalayan", "Abominable Snowman")
>>> print("Name is", tuple_thing[NAME])
Name is yeti
在 示例 5-4 中,字典更好一些,可以通过描述性键访问。
示例 5-4. 使用字典
>>> dict_thing = {"name": "yeti",
... "country": "CN",
... "area": "Himalayas",
... "description": "Hirsute Himalayan",
... "aka": "Abominable Snowman"}
>>> print("Name is", dict_thing["name"])
Name is yeti
集合仅包含唯一值,因此对于聚类不是非常有用。
在 示例 5-5 中,命名元组 是一个可以通过整数偏移或名称访问的元组。
示例 5-5. 使用命名元组
>>> from collections import namedtuple
>>> CreatureNamedTuple = namedtuple("CreatureNamedTuple",
... "name, country, area, description, aka")
>>> namedtuple_thing = CreatureNamedTuple("yeti",
... "CN",
... "Himalaya",
... "Hirsute HImalayan",
... "Abominable Snowman")
>>> print("Name is", namedtuple_thing[0])
Name is yeti
>>> print("Name is", namedtuple_thing.name)
Name is yeti
注意
您不能说 namedtuple_thing["name"]。它是一个 tuple,而不是一个 dict,所以索引需要是一个整数。
示例 5-6 定义了一个新的 Python class,并添加了所有属性与 self。但您需要大量键入才能定义它们。
示例 5-6. 使用标准类
>>> class CreatureClass():
... def __init__(self,
... name: str,
... country: str,
... area: str,
... description: str,
... aka: str):
... self.name = name
... self.country = country
... self.area = area
... self.description = description
... self.aka = aka
...
>>> class_thing = CreatureClass(
... "yeti",
... "CN",
... "Himalayas"
... "Hirsute Himalayan",
... "Abominable Snowman")
>>> print("Name is", class_thing.name)
Name is yeti
注意
你可能会想,没什么大不了的?使用常规类,你可以添加更多数据(属性),但特别是行为(方法)。你可能会在一个疯狂的日子里决定添加一个查找探险者最爱歌曲的方法。(这不适用于某些生物。²)但这里的用例只是为了在各层之间无干扰地移动一堆数据,并在进出时进行验证。同时,方法是方形的钉子,会在数据库的圆孔中挣扎着不合适。
Python 有没有类似于其他计算机语言所说的记录或结构体(一组名称和值)?Python 的一个新特性是数据类。示例 5-7 展示了在数据类中,所有的self内容如何消失。
示例 5-7. 使用数据类
>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class CreatureDataClass():
... name: str
... country: str
... area: str
... description: str
... aka: str
...
>>> dataclass_thing = CreatureDataClass(
... "yeti",
... "CN",
... "Himalayas"
... "Hirsute Himalayan",
... "Abominable Snowman")
>>> print("Name is", dataclass_thing.name)
Name is yeti
这对于保持变量在一起的部分相当不错。但我们想要更多,所以让我们向圣诞老人要这些:
-
一个可能的替代类型的联合
-
缺失/可选值
-
默认值
-
数据验证
-
序列化为和从 JSON 等格式
替代方案
使用 Python 的内置数据结构,特别是字典,是很有吸引力的。但你最终会发现字典有点太“松散”。自由是有代价的。你需要检查所有的:
-
键是可选的吗?
-
如果键缺失,是否有默认值?
-
键是否存在?
-
如果是,键的值是正确的类型吗?
-
如果是,值是否在正确的范围内或匹配某个模式?
至少有三个解决方案至少解决了一些这些要求:
Python 的标准部分。
第 第三方,但比数据类更加完善。
也是第三方,但已集成到 FastAPI 中,如果你已经在使用 FastAPI,这是一个容易的选择。如果你正在阅读这本书,那很可能是这样。
三者的一个方便比较在YouTube上。一个结论是 Pydantic 在验证方面脱颖而出,并且与 FastAPI 的集成捕捉了许多潜在的数据错误。另一个是 Pydantic 依赖于继承(从BaseModel类),而其他两个使用 Python 装饰器来定义它们的对象。这更像是风格问题。
在另一项比较中,Pydantic 在marshmallow和令人着迷的Voluptuous这样的旧验证包上表现更优。Pydantic 的另一个大优点是它使用标准的 Python 类型提示语法;旧的库在类型提示出现之前就已经存在,并自己开发了类型提示。
所以我在这本书中选择使用 Pydantic,但如果你没有使用 FastAPI,你可能会找到这两个替代方案的用法。
Pydantic 提供了指定任何组合这些检查的方法:
-
必需与可选
-
如果未指定但必需的默认值
-
预期的数据类型或类型
-
值范围限制
-
其他基于函数的检查(如有需要)。
-
序列化和反序列化
一个简单的示例
你已经看到如何通过 URL、查询参数或 HTTP 主体向 Web 端点提供简单的字符串。问题在于,通常请求和接收多种类型的数据组。这就是 Pydantic 模型首次出现在 FastAPI 中的地方。
此初始示例将使用三个文件:
-
model.py 定义了一个 Pydantic 模型。
-
data.py 是一个虚假数据源,定义了一个模型实例。
-
web.py 定义了一个 FastAPI Web 端点,返回虚假数据。
为了简单起见,在本章中,让我们将所有文件放在同一个目录中。在讨论更大网站的后续章节中,我们将把它们分开放置到各自的层中。首先,在 Example 5-8 中定义一个 model 用于一个生物。
Example 5-8. 定义一个生物模型:model.py
from pydantic import BaseModel
class Creature(BaseModel):
name: str
country: str
area: str
description: str
aka: str
thing = Creature(
name="yeti",
country="CN",
area="Himalayas",
description="Hirsute Himalayan",
aka="Abominable Snowman")
)
print("Name is", thing.name)
Creature 类继承自 Pydantic 的 BaseModel。: str 在 name、country、area、description 和 aka 后面是类型提示,表示每个都是 Python 字符串。
注意
在本示例中,所有字段都是必需的。在 Pydantic 中,如果类型描述中没有 Optional,则字段必须具有值。
在 Example 5-9 中,如果包括它们的名称,则参数可以按任意顺序传递。
Example 5-9. 创建一个生物
>>> thing = Creature(
... name="yeti",
... country="CN",
... area="Himalayas"
... description="Hirsute Himalayan",
... aka="Abominable Snowman")
>>> print("Name is", thing.name)
Name is yeti
现在,Example 5-10 定义了一个数据的小来源;在后续章节中,数据库将执行此操作。类型提示 list[Creature] 告诉 Python 这是一个 Creature 对象列表。
Example 5-10. 在 data.py 中定义虚假数据
from model import Creature
_creatures: list[Creature] = [
Creature(name="yeti",
country="CN",
area="Himalayas",
description="Hirsute Himalayan",
aka="Abominable Snowman"
),
Creature(name="sasquatch",
country="US",
area="*",
description="Yeti's Cousin Eddie",
aka="Bigfoot")
]
def get_creatures() -> list[Creature]:
return _creatures
(因为大脚几乎无处不在,我们在 "*" 处使用 Bigfoot 的 area。)
此代码导入了我们刚刚编写的 model.py。它通过调用 _creatures 的 Creature 对象列表来进行一些数据隐藏,并提供 get_creatures() 函数来返回它们。
Example 5-11 列出了 web.py,一个定义 FastAPI Web 端点的文件。
Example 5-11. 定义一个 FastAPI Web 端点:web.py
from model import Creature
from fastapi import FastAPI
app = FastAPI()
@app.get("/creature")
def get_all() -> list[Creature]:
from data import get_creatures
return get_creatures()
现在,在 Example 5-12 中启动此单端点服务器。
Example 5-12. 启动 Uvicorn
$ uvicorn creature:app
INFO: Started server process [24782]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
在另一个窗口中,Example 5-13 使用 HTTPie Web 客户端访问 Web 应用程序(如果喜欢也可以尝试浏览器或 Requests 模块)。
Example 5-13. 使用 HTTPie 进行测试
$ http http://localhost:8000/creature
HTTP/1.1 200 OK
content-length: 183
content-type: application/json
date: Mon, 12 Sep 2022 02:21:15 GMT
server: uvicorn
{
"aka": "Abominable Snowman",
"area": "Himalayas",
"country": "CN",
"name": "yeti",
"description": "Hirsute Himalayan"
},
{
"aka": "Bigfoot",
"country": "US",
"area": "*",
"name": "sasquatch",
"description": "Yeti's Cousin Eddie"
}
FastAPI 和 Starlette 自动将原始 Creature 模型对象列表转换为 JSON 字符串。这是 FastAPI 中的默认输出格式,因此我们无需指定它。
另外,你最初启动 Uvicorn Web 服务器的窗口应该打印了一行日志:
INFO: 127.0.0.1:52375 - "GET /creature HTTP/1.1" 200 OK
验证类型
前一节展示了如何执行以下操作:
-
将类型提示应用于变量和函数
-
定义和使用 Pydantic 模型
-
从数据源返回模型列表
-
将模型列表返回给 Web 客户端,自动将模型列表转换为 JSON
现在,让我们真正开始验证数据工作。
尝试将错误类型的值分配给一个或多个 Creature 字段。让我们使用一个独立的测试来进行测试(Pydantic 不适用于任何 Web 代码;它是一个数据处理工具)。
[Example 5-14 列出了 test1.py。
示例 5-14. 测试 Creature 模型
from model import Creature
dragon = Creature(
name="dragon",
description=["incorrect", "string", "list"],
country="*" ,
area="*",
aka="firedrake")
现在在 示例 5-15 中尝试测试。
示例 5-15. 运行测试
$ python test1.py
Traceback (most recent call last):
File ".../test1.py", line 3, in <module>
dragon = Creature(
File "pydantic/main.py", line 342, in
pydantic.main.BaseModel.*init*
pydantic.error_wrappers.ValidationError:
1 validation error for Creature description
str type expected (type=type_error.str)
发现我们已经将字符串列表分配给 description 字段,但它希望是一个普通的字符串。
验证值
即使值的类型与其在 Creature 类中的规格相匹配,可能仍需要通过更多检查。一些限制可以放置在值本身上:
-
整数(
conint)或浮点数:gt大于
lt小于
ge大于或等于
le小于或等于
multiple_of值的整数倍
-
字符串(
constr):min_length最小字符(非字节)长度
max_length最大字符长度
to_upper转换为大写
to_lower转换为小写
regex匹配 Python 正则表达式
-
元组、列表或集合:
min_items最小元素数量
max_items最大元素数量
这些在模型的类型部分指定。
示例 5-16 确保 name 字段始终至少为两个字符长。否则,""(空字符串)是有效的字符串。
示例 5-16. 查看验证失败
>>> from pydantic import BaseModel, constr
>>>
>>> class Creature(BaseModel):
... name: constr(min_length=2)
... country: str
... area: str
... description: str
... aka: str
...
>>> bad_creature = Creature(name="!",
... description="it's a raccoon",
... area="your attic")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "pydantic/main.py", line 342,
in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError:
1 validation error for Creature name
ensure this value has at least 2 characters
(type=value_error.any_str.min_length; limit_value=2)
constr 意味着受限字符串。 示例 5-17 使用另一种方式,即 Pydantic 的 Field 规范。
示例 5-17. 另一个验证失败,使用 Field
>>> from pydantic import BaseModel, Field
>>>
>>> class Creature(BaseModel):
... name: str = Field(..., min_length=2)
... country: str
... area: str
... description: str
... aka: str
...
>>> bad_creature = Creature(name="!",
... area="your attic",
... description="it's a raccoon")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "pydantic/main.py", line 342,
in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError:
1 validation error for Creature name
ensure this value has at least 2 characters
(type=value_error.any_str.min_length; limit_value=2)
... 参数传递给 Field() 意味着需要一个值,并且没有默认值。
这是 Pydantic 的简要介绍。主要的收获是它允许您自动验证数据。当从 Web 或数据层获取数据时,您将看到这是多么有用。
回顾
在您的 Web 应用程序中传递的最佳数据定义方式是模型。Pydantic 利用 Python 的类型提示来定义在应用程序中传递的数据模型。接下来是:定义依赖项以将特定细节与通用代码分离。
¹ 我有任何可察觉的想象力吗?嗯……没有。
² 除了那些尖叫的雪人小团体(一个乐队的好名字)。