Python-冒险之旅-全-
Python 冒险之旅(全)
原文:A Pythonic Adventure
译者:飞龙
前置内容
前言
这本书基于一个真实的故事。我的儿子,埃里克,喜欢去星巴克尝试不同口味和配料的美式咖啡。有一天,他决定自己准备美式咖啡,并款待他的朋友们。他拿了他的平板电脑收集他们的订单,但我建议他为此创建一个简单的程序。他之前尝试学习编程,但大部分练习对他来说都很无聊。这次,他看到了一个可以用编程解决的问题,并对此产生了兴趣。
这就是这本书的起源。我希望你能找到自己感兴趣的可以用编程解决的问题——并且我希望这本书能帮到你。
这里有一些建议:
-
不要急躁——我理解你直接跳到最后一章、下载程序最终版本的代码并运行的愿望。但请不要这样做。一步一步来,自己编写代码(请不要复制粘贴,谢谢!),尝试运行,然后继续前进。有时候,你可能需要返回并重新阅读一章。有时候,你可能需要休息一下。不要担心!休息一下,重复阅读这一章——只是不要放弃。
-
犯错——当一切进行得非常顺利时,你不会学到东西。学习的唯一方式就是犯错。不要害怕犯错。尝试代码,改变一些东西,获取错误信息,并阅读它们。在互联网上搜索错误信息,你会发现成千上万的人犯了同样的错误。学习他们是如何解决的,然后解决你的问题。继续前进,不要放弃。
-
提问——向你的朋友、父母和祖父母提问,向互联网提问。向某人解释你的问题——有时候,这足以让你自己找到解决方案。没有所谓的“愚蠢的问题”——不要害羞。提问,不要放弃。
-
更进一步——修改你用这本书创建的应用程序。改变一些东西,让它看起来更像你自己的应用程序。考虑你可以创建的其他应用程序。看看你周围:什么可以被自动化?你能创建一个类似于你已知的应用程序或网站的应用程序吗?告诉你的朋友们你的想法——也许你们可以一起创造一些东西。编程很酷。不要放弃。
-
发表意见——请在 liveBook 讨论论坛上分享你的经验。与朋友分享你的想法和观点。讨论你解决的问题将使你的 Python 之旅变得更加有趣。
致谢
这是我写的第一本书,没有众多个人宝贵的支持和指导,我无法完成它。
首先,我想对我的父母表示衷心的感谢。我的父亲,一个坚信 STEM 教育重要性的坚定信仰者,用科普书籍装满了我们的家,并鼓励我学习编程。他敏锐地预测到编程将成为第二种读写能力,而且他预测得非常准确。我的母亲,一位终身从事物理教学的老师,慷慨地将她的教学基因传承给了我。
我还必须感谢我的儿子们,西蒙和埃里克,他们给了我教他们 Python 的机会。这段宝贵的经历让我作为一个教育者和程序员都得到了学习和成长。
致我的妻子,塔蒂亚娜,我衷心感谢她无尽的耐心和坚定不移的支持。她优雅地忍受了许多周末,当我专注于键盘上的写作而不是享受她愉快的陪伴时。在整个过程中,她倾听我的想法和观点,提供了宝贵的建议和视角。
我对我的 Manning 编辑们表示感激:Troy Dreier、Toni Arritola 和 Marie-Therese Smith,他们在将这本书付诸实践方面发挥了关键作用。Troy 认出了我早期草稿的潜力,并将其带到了 Manning 以供考虑,没有这一点,这本书永远不会开始。Toni 温和而坚持不懈的鼓励确保了这本书的及时完成。没有她,这本书永远不会完成。Marie-Therese,作为一名教育工作者、工程师和数据科学家,她仔细测试了代码和技术细节,防止读者在第一个编码示例中遇到困难。
我还想感谢 Nicholas H. Tollervey,Mu 编辑的主要作者,以及所有为这个出色的开源项目做出贡献的人。这个面向初学者的 Python 开发环境为我完成这本书的旅程提供了我所需的一切,从 Python 的第一步到全栈网络应用程序的开发。
致所有审稿人:Afif Hayder、Al Pezewski、Alan Gil Forza、Alexis Perrier、Amogh Raghunath、Andrew R. Freed、Arsalan Khan、Arya ArunKumar、Jean-Baptiste Bang Nteme、Ben McNamara、Bernard Fuentes、Chris Kardell、Christian Sutton、Clara McAdams、Clemens Baader、David Kuhta、Dhivya Sivasubramanian、Elona Vialatte、Faiyaz Evan Hayder、Ganesh Swaminathan、George Thomas、Greg Freed、Hannah Cheung、Hasin Hayder、Jackson McNamara、Jana Dragosavljević、Janit Anjaria、Jimena de Jesús Mata Cobián、João Dinis Ferreira、Keith Kim、Kevin Cheung、Mafinar Khan、Marc-Anthony Taylor、Mary Anne Thygesen、Miguel Eduardo Gil Biraud、Nathan-Steven Taylor、Nik Piepenbreier、Ninoslav Čerkez、Or Golan、Pavel Šimon、Philippe Vialatte、Regina de Jesús Mata Cobián、Robert Kulagowski、Rupa Lahiri、Sataduru Roy、Shaurya Dara、Shivansh Batra、Shyon Roy、Sravanthi Reddy、Srisha Reddy、Srisha Thimmareddy、Sudeep Batra、Sumit Bhattacharyya、Tarun Ganesh、Tawhida Hussain、Walter Alexander Mata López、Will Pezewski 和 Wolfie Baader,你们的建议帮助使这本书变得更好。
最后,我要向整个 Python 社区表示衷心的感谢。多年来,我从播客、视频、会议演讲、书籍和在线论坛中获得的知识财富是无法估量的。这个社区的热情和支持无疑是 Python 受开发者喜爱的一个重要原因。谢谢大家!
关于这本书
我写这本书是为了那些想通过实际项目学习编程的青少年。在书中,你会发现两个兄弟在合作开发咖啡店应用程序时的一系列非正式对话。跟随他们的对话,和他们一起开发自己的应用程序。
谁应该阅读这本书?
可能你已经上过编程课程了——无论是线上还是在学校。你掌握了基础知识,但还没有尝试创建一个真实的项目。
可能你还没有开始编程,但你知道如何使用电脑,并且想要了解编程是如何工作的。
可能你已经使用过一些在线应用程序,现在想要创建自己的。
可能你的哥哥或姐姐已经创建过计算机程序和应用程序,你也想学习编程。
如果上述情况都符合你——太好了!通过这本书,你将和埃里克和西蒙一起逐步学习,并在几周内创建自己的应用程序。
如果你是一位(外)祖父母,并且想要一个易于遵循的指南,用你的(外)孙辈学习编程,这本书也适合你!即使你想自己学习编程,也不要认为这本书只适合孩子。每个人都欢迎学习 Python 并创建应用程序!
本书是如何组织的:路线图
本书每一章都涵盖了埃里克和西蒙(后来是艾米莉)一天内所创建的内容。他们每天一起坐大约一个小时,讨论他们的应用程序,并编写代码。你也可以这样做——每天读一章,并为那一章编写代码。如果你有疑问,可以在本书的论坛中提问或在网上搜索。
本书共有 15 章。
第一部分包括前 9 章,其中埃里克和西蒙在他们的应用程序的第一个版本上工作,这是一个基于文本的应用程序。这意味着它的工作方式就像聊天一样,询问客户他们想要订购什么,并等待他们的回答。
在第一章中,埃里克得到了他应用程序的想法。他想为他的朋友们准备咖啡饮料,并需要收集他们的订单。西蒙帮助他创建了应用程序的第一个版本。
在第二章中,埃里克学习了列表。列表是组织你的菜单,包含不同口味和配料的好方法。
在第三章中,埃里克注意到他重复了多次代码。西蒙建议使用函数来避免这种情况。应用程序代码变得更短,更容易阅读。
在第四章中,埃里克和西蒙处理用户错误。如果客户在输入订单时出错怎么办?你的应用程序应该处理这种情况。
在第五章中,西蒙教埃里克如何使用文件。现在,商店经理可以编辑文本文件,从菜单中添加或删除口味或配料。
在第六章中,兄弟俩为应用程序创建了一个主菜单。这使得他们可以连续运行应用程序,并为一位又一位的客户提供服务。
在第七章中,埃里克和西蒙创建了更多的函数。他们使用函数来获取订单并打印出来。
在第八章中,埃里克学习了关于 JSON 的知识。西蒙解释了如何使用 JSON 文件来存储订单。
在第九章中,兄弟们完成了应用程序的文本版本。他们讨论了如何制作其网络版本。
在第二部分(第十章至第十五章),埃里克、艾米丽和西蒙在他们的应用程序的网络版本上工作。他们创建了基于网页的菜单,连接数据库,并处理样式和颜色。最后,他们在智能手机和平板电脑上测试了他们的应用程序。
在第十章中,艾米丽加入了兄弟们。西蒙向艾米丽和埃里克展示了如何创建一个非常简单的网络应用程序。他们学习了如何使用网页表单和菜单。
在第十一章中,艾米丽和埃里克在他们的咖啡馆创建网页表单和菜单。
在第十二章中,西蒙向艾米丽和埃里克解释了如何使用数据库来存储订单。他们学习了如何使用结构化查询语言(SQL)来完成这项工作。
在第十三章中,朋友们开始使他们的网络应用程序变得美观。他们学习了如何在网页上使用字体、颜色和图片。
在第十四章中,西蒙向艾米丽和埃里克展示了如何使用人工智能(AI)改进他们的应用程序代码、编写注释和解释错误信息。
在第十五章中,艾米丽、埃里克和西蒙将应用程序部署到他们的智能手机和平板电脑上并进行了测试。它成功了!他们讨论了下一步:如何将应用程序部署到云上以及如何添加更多功能。他们决定继续开发应用程序,但使用网站来交流他们的想法。
我建议您按章节顺序阅读这本书。请不要跳过标记为“YOUR TURN”的部分。您将不得不以自己的方式编写埃里克、艾米丽和西蒙编写的代码。发挥你的想象力,改变事物,创建你自己的独特应用程序!有时,你可能想休息一下。有时,你可能想重新阅读一章以更好地理解它。这是可以的。以你自己的节奏阅读这本书——但不要放弃!
如果你在某个代码片段上遇到了困难,尝试从我们的代码仓库(下文将讨论)中复制它。如果有什么不清楚的地方,请在 liveBook 论坛(稍后讨论)中提问。
关于代码
本书源代码可在 GitHub 上找到:github.com/pavelanni/pythonicadventure-code。每个章节的末尾都提供了一个链接,可以访问该章节的代码。您还可以从本书的 liveBook(在线)版本中获取可执行的代码片段,链接为livebook.manning.com/book/a-pythonic-adventure。
本书包含许多源代码示例,既有编号列表,也有与普通文本内联。在这两种情况下,源代码都使用固定宽度字体如这样来格式化,以便与普通文本区分开来。
在许多情况下,原始源代码已被重新格式化;我们添加了换行并重新整理了缩进,以适应书中的可用页面空间。
此外,当代码在文本中描述时,源代码中的注释通常已从列表中删除。许多列表都有代码注释,突出显示重要概念。
liveBook 讨论论坛
购买《A Pythonic Adventure》包括免费访问 liveBook,Manning 的在线阅读平台。使用 liveBook 的独家讨论功能,你可以对全书或特定章节或段落添加评论。为自己做笔记、提问和回答技术问题以及从作者和其他用户那里获得帮助都非常方便。要访问论坛,请访问livebook.manning.com/book/a-pythonic-adventure/discussion。你还可以在livebook.manning.com/discussion了解更多关于 Manning 论坛和行为准则的信息。
Manning 对我们读者的承诺是提供一个场所,让读者之间以及读者与作者之间可以进行有意义的对话。这不是对作者参与特定数量活动的承诺,作者对论坛的贡献仍然是自愿的(且未付费)。我们建议你尝试向作者提出一些挑战性的问题,以免他的兴趣转移!只要本书仍在印刷中,论坛和先前讨论的存档将可通过出版社的网站访问。
其他在线资源
本书有一个配套网站:pythonicadventure.com。在这个网站上,我们收集了一些其他类似项目的想法(以防你不喜欢咖啡店项目),故障排除信息以及改进应用程序的想法。
获取 Python 信息的最佳来源是官方网站:www.python.org。你可以在那里找到所有必要的文档、博客、文章和教程。对初学者最有用的网站之一是这些链接到其他资源的集合:wiki.python.org/moin/BeginnersGuide。
进一步学习
完成这本书后,你将准备好更认真地使用 Python。在你继续 Python 之旅时,可以考虑以下书籍:
-
Tiny Python Projects by Ken Youens-Clark (
www.manning.com/books/tiny-python-projects) -
Python Workout by Reuven M. Lerner (
www.manning.com/books/python-workout) -
Practices of the Python Pro by Dane Hillard (
www.manning.com/books/practices-of-the-python-pro)
关于作者

帕维尔·安尼是 SambaNova Systems 的首席客户工程师。在加入这家 AI 初创公司之前,帕维尔曾在 Sun Microsystems、Oracle 和 Red Hat 工作。在他的职业生涯中,他的主要职责是培训人员并推广新技术。他开发和讲授了关于编程语言、Unix 和 Linux 操作系统、Kubernetes 以及其他主题的课程。
1 为朋友准备咖啡:第一步
在本章
-
Erik 有了一个想法
-
Erik 和 Simon 讨论未来的应用
-
Erik 安装了一个代码编辑器,并尝试运行他的第一个 Python 程序
-
Simon 解释如何使用变量
-
Erik 用 Python 编写了他的第一个对话
所有这一切都始于一个阳光明媚的夏日。Erik 带着一个想法回家:他想为他的朋友们准备咖啡饮料。谁知道他会为这个创建自己的在线应用程序呢?
“我会让它就像星巴克一样,有很多风味和配料,”他想。“我想我需要的一切都有了:咖啡,三四种可以添加的风味,还有一些巧克力奶油作为配料。太棒了!”
“我的 iPad 呢?”他问他的哥哥 Simon。
“你停下的地方。为什么?”
“我需要用它来收集咖啡店的订单!”
几分钟后,他带着 iPad 上的笔记回来了,为他的朋友们准备了四杯饮料,然后又离开了。
“这不是个好主意吗?”当他带着四个空塑料杯子回家时,他问 Simon。
“是的,好主意,”Simon 说,“但是……”
“‘但是’是什么意思?”Erik 问。他感觉他的哥哥想破坏他的心情——就像他通常做的那样。
“你用 iPad 下订单,但只是把它当作一张普通的纸笔记本。你可以为你的咖啡店创建一个简单的应用程序,并用它来接收订单。”
“你是说,像在线商店那样?有菜单和所有那些?”Erik 已经想象出了他自己的网店,顶部有一个大大的标题:“Erik 的咖啡店。”
“当然,是的。你知道你在那个在线课程中学到了一点 Python,不是吗?”
“是的,但我不记得太多。我们做了一些练习……我想让它看起来像真正的在线商店会很难。”
“别担心,”Simon 说,“我们会一步一步来。我在学校的机器人团队里已经做了几个类似的项目。”
注意 如果你完全没有编程经验,别担心。Erik 无论如何都不记得他的课程内容,所以我们将从头开始。
Simon 正在上高中的最后一年。他几年前就学习了 Python,并在学校的计算机科学俱乐部以及最近在机器人团队中使用它。
“所以你是说我们可以构建一个真正的在线应用程序?”Erik 还没有完全信服。
“是的,当然。如果你不退出我的课,”Simon 微笑说,“你将在几周内完成它。然后你的顾客将能够选择他们想要的任何饮料,添加风味……”
“还有配料!”Erik 补充道。
“是的,还有配料。在他们确认订单后,你会在订单页面上看到它。你会知道该为谁准备什么。就像这样,”Simon 拿起一张纸,开始画一个简单的网页。“这将是你订单页面。

“这将是你订单的列表。”

“酷!你认为我们可以做到吗?”Erik 仍然不敢相信他的哥哥。
“当然!正如我说的,不要放弃。你在暑假期间还有很多时间完成它。”
注意:如果你不喜欢咖啡馆的想法,还有几个其他的项目想法可以使用。其中一些将在埃里克的朋友艾米丽在后面的章节中与他一起加入时讨论。更多详细信息请参阅附录 A。
首要事项:安装
“让我们从一些简单的事情开始。你会很快记住 Python 的。你在你的笔记本电脑上安装了它吗?”西蒙问道。
“不,我不这么认为。”
“这是一个非常适合像你这样的初学者的 Python 编辑器。它叫做 Mu。试着找到它并安装它。你可以做到的,我相信。”
埃里克找到了他可以下载它的网站:codewith.mu/。

他从下载页面下载了安装程序:codewith.mu/en/download。

他点击了说明按钮,找到了一个包含他电脑所有步骤的说明页面。
“别担心,这不是玩具。这是一个完美的编辑器,”西蒙说。“我们在我们的机器人团队中使用它来与微控制器一起工作。正如你所看到的,有 Windows、macOS 和 Linux 的版本。我使用 Linux 版本与我的团队一起工作。”
注意:你可以在附录 B 中找到所有必要的链接和说明。
“有其他适用于 Python 的编辑器吗?”埃里克不想仅仅遵循他哥哥的指示。
“是的,当然,很多。对于初学者来说,另一个好选择是 Thonny。看这里:thonny.org/。”

“我喜欢它!”埃里克说。“而且名字很有趣。”
“当然,还有其他适用于每个平台的代码编辑器:
-
VS Code (
code.visualstudio.com/) -
Sublime Text (
www.sublimetext.com/)
“它们都与 Python 完美兼容。即使是像 Vim(www.vim.org/)和 Emacs(www.gnu.org/software/emacs/)这样非常老的编辑器,也支持 Python,但你必须是一个非常严肃的程序员才能使用它们,”西蒙对他的兄弟眨了眨眼。
“Mu 和 Thonny,”西蒙继续说,“当你安装它们时都会包含 Python。要使用其他编辑器中的 Python,你必须先安装它。在某些系统上,如 Linux 和 macOS,Python 从一开始就安装了。在 Windows 上,你需要安装它。如果你想的话,我可以稍后给你演示。”
你的机会来了! 安装你的 Python 环境
现在轮到你了。打开你的笔记本电脑或台式机并安装 Mu 编辑器。你可以在附录 B 中找到不同平台上的完整说明(它适用于 Windows、macOS 和 Linux)。
如果你更喜欢其他编辑器,请随意安装它而不是 Mu。不要害怕尝试!
如何与计算机交流
“让我们启动 Mu 并开始编写你的咖啡店程序,”西蒙说。
埃里克启动了 Mu 并看到了它的第一个窗口。

“从菜单中选择 Python 3 并点击确定,”西蒙建议。
埃里克按照西蒙说的做了。“从现在开始,”西蒙继续说,“Mu 会记住你更喜欢使用 Python 3。也许你注意到了还有一些其他模式可以用来与微控制器一起工作,构建网络应用程序,以及其他事情。但现在,我们只需要 Python 3 模式。”
现在,埃里克面前是编辑器窗口。

“我这里应该写什么?”埃里克问。
“你想要你的程序先做什么?”
“它应该显示‘欢迎来到埃里克的咖啡店!’”
“太好了!让我们来写它。记得 Python 中的 print() 函数吗?”
埃里克开始输入。这个第一步很简单。

“现在呢?”
“现在运行它。点击运行。”

埃里克点击了按钮,突然又出现了一个窗口询问他是否想要保存程序。这很简单。埃里克输入了文件名,coffeeshop,准备按下 Enter 键保存文件时,西蒙说:“等等,等等……别忘了在文件名后添加 .py。你必须让你的文本编辑器知道它是一个 Python 程序。Mu 会自动添加,但其他编辑器不会,所以请确保所有你的 Python 文件都以 .py 结尾。”

埃里克在文件名后添加了 .py 并保存了文件。紧接着,他注意到编辑器窗口底部又出现了一个窗口。那是咖啡店问候语——正是他想要的!
“它工作了!”埃里克很高兴。
“当然它工作了。为什么不会呢?”西蒙回答。“但你想要收集订单,不是吗?”
“是的,我会询问客户的名字和他们想要什么。”
“然后呢?”西蒙显然知道答案,但他想让埃里克自己找到。
“然后我会打印‘你好!这是你的订单:’并显示他们的名字、口味和配料,就像在真正的收据上一样。”
“好主意,”西蒙说。“但看:当你编写你的程序时,你不知道你的朋友想要点什么,对吧?所以你不能在程序中写‘你点了焦糖。’不同的客户会点不同的事物。对于亚历克斯来说是焦糖,对于艾米丽来说是草莓。所以你看,你的口味会变化,客户的姓名也会变化。记得在编程中这个叫什么吗?”
“这是一个变量!”埃里克很高兴他记得几个月前在 Python 课程中学到的这个。
“对!”西蒙也很高兴。“变量就像一个盒子:你可以往里面放东西,然后打开它看看盒子里有什么。你也可以用别的东西替换盒子里面的东西。”
“在我们的情况下,”西蒙继续说,“让我们从一个叫做answer的盒子开始,把从你的客户那里听到的任何东西都存放在那个盒子里。你问你的客户他们的名字,他们回答‘亚历克斯’,例如。你把这个答案放在叫做answer的盒子里,并保留在那里。当你想打印出来时,你告诉 Python:‘请打印现在在叫做answer的盒子里的任何东西。’下一个客户的名字可能是艾米丽,所以你把‘艾米丽’放在盒子里。下次,Python 会打印‘艾米丽’而不是‘亚历克斯’,因为现在在叫做answer的盒子里的是‘艾米丽’。让我们为这个写代码。”
“就在这里,同一个文件里?”埃里克问道。
“当然,继续在同一个文件里进行。要从客户那里获取信息,我们使用叫做input()的函数。当你调用它时,它会等待用户输入。所以用户在键盘上输入一些东西,然后按 Enter 键。然后函数返回用户输入的内容。”
“等等,等等,”埃里克打断西蒙。“你说的‘返回’是什么意思?还有,你在谈论函数。当然我知道它们是什么,但你能否告诉我你说的‘函数’是什么意思?”埃里克不想表现出他几乎完全不记得之前课程中学到的关于函数的知识。
“函数是一段执行某些操作的代码。几乎每段代码都会执行某些操作,但有些代码我们比其他代码用得更多。你以后会创建自己的函数,但现在我们将使用别人写的函数。人们经常使用的操作,比如打印某物。你没有注意到,但当你之前程序中写print()时,你已经使用了一个函数。在编程中,我们说你调用一个函数。”
“啊哈,我明白了,”埃里克说。“有括号的东西叫做‘函数’。”
“没错。你可以在那些括号里放些东西,函数会处理它。例如,它会打印你的信息。你传递给函数的东西叫做参数。有时参数是一个字符串,有时是一个数字,有时有多个参数。”
“‘字符串’是什么?”埃里克问道。
“在这种情况下,字符串是一个单词或几个单词。我们会经常用到它们,所以你问得好,”西蒙说。
“我们说我们向函数‘传递参数’,”西蒙继续说。“函数会对这些参数做些事情,并得到一些作为结果。例如,它可能计算一些东西,或者用你传递的字符串做些事情,比如将其转换为大写或加密。然后它返回这个结果到你的主程序。”
“但我怎么看到结果呢?”埃里克问道。“函数会打印出来吗?”
“不,它不会。这就是我们需要变量的地方。我们告诉 Python:‘请用这些参数调用这个函数,并将它返回的任何内容放入这个盒子,抱歉,这个变量。’所有这些操作都使用简单的‘等号’完成,就像这样:=。例如,如果你想调用input()函数并将它返回的内容放入变量answer中,你只需写下这个:
answer = input()
“在你保存客户的答案后,你可以打印它。你调用print()函数,并将你的变量作为参数传递。”
“太好了,”Erik 说,“现在我明白怎么写了。”他开始在编辑器中输入。一分钟左右,他有了这个:
print("Welcome to Erik's Coffee Shop!")
answer = input()
print(answer)
“我应该运行它吗?”他问 Simon。
“当然,继续,点击运行 ![]()
Erik 点击运行 ![]()
“它显示‘欢迎来到 Erik 的咖啡店’然后什么也没有。”
“你期望什么?”Simon 问。
“它会问我我的名字。”
“但你没有告诉 Python 它应该询问什么。现在它正在等待你的输入。输入一些内容。”
Erik 输入“Erik”并按下了 Enter 键。
Python 打印出Erik。

“它工作了!”Erik 说。
你的回合! 编写你的第一个对话
写下 Erik 刚刚编写的对话程序。这是一个简短的程序,所以我建议你自己输入,而不是从书中复制。为你的咖啡店起一个名字,并在第一条“欢迎”信息中使用它。或者,如果你想的话,可以创建其他商店。它将卖什么?冰淇淋?披萨?鲜花?宠物玩具?
尝试运行你的程序。它是否如你所期望的那样工作?如果不,从书中或从书网站(github.com/pavelanni/pythonicadventure-code/tree/main/ch01)复制它并再次运行。它应该可以工作。
“是的,它工作了,”Simon 说,“但让我们让它更用户友好。记住,当它只显示‘欢迎’时,你感到困惑。你应该告诉用户你希望他们做什么。此外,你可以在只打印Erik之前添加一些像‘这里是你的订单,Erik’这样的内容。”
“你可以将这个字符串作为参数传递给input()函数。我们称之为提示字符串。它向用户解释我们希望他们做什么。在print()函数中,你可以在answer变量之前添加你想要打印的字符串。让我帮你。”
Simon 帮助 Erik 将那些字符串添加到代码中,之后它看起来是这样的:
print("Welcome to Erik's Coffee Shop!")
answer = input("Please enter your name: ")
print("Here is your order, ", answer)
Simon 注意到 Erik 正在寻找运行按钮
,他解释说:“在再次点击运行
之前,你必须停止之前的 Python 会话。看到三个尖括号了吗?这意味着 Python 正在运行并等待你的输入。我稍后会解释如何使用 Python 这种方式,但现在,只需点击停止
,然后再次点击运行
。”
现在程序要求下单,并按照 Erik 编写的程序回答:
Welcome to Erik's Coffee Shop!
Please enter your name: Erik
Here is your order, Erik
>>>
轮到你了! 让你的程序更加用户友好
在你的第一个程序中添加一个提示和一个输出字符串。创建一个提示,询问客户的名字,比如“很高兴见到你!你叫什么名字?”
“这看起来用户友好多了,不是吗?”西蒙问道。“始终考虑你的用户,问问自己:‘我期望用户做什么?他们在这里可能会犯错误吗?’”
“现在,”西蒙继续说,“我们必须询问客户的订单。你说你有咖啡和巧克力。你还提到了口味和配料?”
“是的,”埃里克说,“我想问他们想要哪种配料和口味。”
“好吧,去问他们吧。你只需要重复相同的代码,但别忘了更改提示。我想你应该在最后打印整个订单,而不是每个问题之后。试试看。”
埃里克编写了这段代码,并在最后一行停了下来:
print("Welcome to Erik's Coffee Shop!")
answer = input("Please enter your name: ")
answer = input("Please enter your drink: ")
answer = input("Please enter your flavor: ")
answer = input("Please enter your topping: ")
print("Here is your order: ", answer)
“你告诉我把答案放在answer变量里。但我怎么知道哪个是口味和配料?”埃里克感到困惑。
“是的,我告诉你要把答案放在一个叫做answer的变量里,”西蒙回答。“我们来到了计算机科学中最困难的问题之一:命名变量,”他微笑着说。“当然,你不会把所有的答案都存储在叫做answer的变量里。让我们为不同的答案使用不同的变量,并给它们有意义的名字。对于客户的名字,我们将使用一个叫做name的变量——这很简单。如果你询问主要饮料,就把答案放在drink或product变量里。对于口味和配料答案,使用flavor和topping变量。”
“在最后,”西蒙继续说,“使用几个print()函数,将每个变量打印到单独的一行上。继续吧。如果需要,我会帮你。”
埃里克继续工作在他的代码上,最终产生了这个:
列表 1.1 咖啡店应用程序的第一个版本
print("Welcome to Erik's Coffee Shop!")
name = input("Please enter your name: ")
drink = input("Please enter your drink: ")
flavor = input("Please enter your flavor: ")
topping = input("Please enter your topping: ")
print("Here is your order, ", name)
print("Main product: ", drink)
print("Flavor: ", flavor)
print("Topping: ", topping)
print("Thanks for your order!")
埃里克点击了运行,他的程序开始了一个对话。埃里克回答了所有问题,并得到了他的输出:
Welcome to Erik's Coffee Shop!
Please enter your name: Erik
Please enter your drink: coffee
Please enter your flavor: caramel
Please enter your topping: chocolate
Here is your order, Erik
Main product: coffee
Flavor: caramel
Topping: chocolate
Thanks for your order!
>>>
西蒙注意到了最后一行,并表扬了埃里克的主动性:“感谢你的客户总是件好事。”
“是的,我在几家咖啡店的收据上看到了这一点,”埃里克说。他很高兴自己除了哥哥告诉他的事情外,还做了一些自己的事情。
轮到你了! 在对话中添加更多选项。使用变量。
编辑你的上一个程序,并添加新的对话行。再次,你可以自由地更改提示和打印的字符串,以便它们更适合你的项目。
改变你的打印输出。看看你访问的地方的收据(咖啡店、餐厅、杂货店、其他商店)。尝试让你的打印输出看起来类似。使用文本符号如|、_、=、+等,让你的输出看起来更有趣。
西蒙决定今天应该结束。
“我想我们今天已经取得了很好的进展,”他说。“让我们看看到目前为止我们做了什么。首先,我们安装了你的编程环境。”
“是的,”埃里克说。“我喜欢这个 Mu 编辑器。它使用颜色来显示程序的不同部分。它还用红色显示我的字符串,直到我把引号放在最后。它还有暗黑模式!我知道真正的程序员总是使用暗黑模式!我想我会继续使用它。”
“第二,”西蒙说,“我们第一次使用了函数。那是什么?”
“是print()函数,”埃里克说。“我告诉它要打印什么,然后它就打印出来了。”
“对。你调用了函数,并且向它传递了一个参数。”
“第三,”西蒙继续说,“你使用了另一个函数来获取用户信息。”
“是input(),”埃里克说。“我把答案保存在变量中。”
“太棒了!”西蒙对他的兄弟感到非常自豪。“你进步得很好。”
轮到你了! 自己解释一下。
尝试用你自己的话来解释。
-
函数是什么?给出一些例子。
-
函数参数是什么?给出一些例子。
-
你如何调用一个函数?
-
变量是什么?为什么你需要它们?
-
你应该如何命名你的变量?为什么?
程序是什么?
“最后,让我们再增加一点理论,”西蒙说。“我们刚刚构建了一个非常简单的程序,但它包含了任何其他程序的所有主要组件。我们向用户请求了一些输入。在收到用户的数据后,我们对这些数据进行了处理。我们通常称之为处理。在我们的例子中,我们只是存储了数据,但我们可以用其他方式处理它,对吧?”
“比如?”埃里克问道。
“例如,你输入了‘coffee’,但是可能我们希望所有产品和口味名称都以大写字母开头。Python 有一个特殊的函数可以做到这一点。所以我们在收到数据后可以处理数据。”
“这是个好主意,我想添加它!”埃里克说。
“当然,我们会那样做。最后,在处理完数据后,我们将其打印出来。换句话说,我们产生了一些输出。看这里。”

“输入并不总是来自用户,”西蒙继续说。“有时没有与用户的交互,程序从其他地方获取数据。例如,从互联网,比如最近的体育结果。有时从传感器,比如在机器人技术中。或者从文档和图像中。”
“通常,输出不仅仅是打印出来。我的机器人团队从传感器获取输入,我们在微控制器中处理它们,我们的输出是给电机的信号,比如‘向左转,向前移动。’但结构仍然是相同的:输入 → 处理 → 输出。”
“理论已经足够了,”西蒙说。“明天我们将致力于改进你的程序。”
“改进?”埃里克感到惊讶。“但是它已经很好用了,不是吗?”
“如果你的用户输入了你店里没有的东西怎么办?”西蒙问道。“比如‘枫糖浆’?你会怎么办?你应该告诉你的用户你咖啡店有什么,他们可以点什么。所以明天我们将工作在菜单上。此外,我们还将看看在出现错误的情况下我们能做什么。”
今天你学到的新东西
-
变量—一个(一个盒子)可以存储值的地方。例如,我们可以存储数字、字母或字符串。变量一次只能存储一个东西。
-
函数—一段执行我们希望再次执行的操作的代码。
-
函数参数—函数执行其工作所需的信息。
-
调用函数—写出函数的名称,并在括号内包含参数。
本章的代码
你可以在这里找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch01。
为你的项目提供更多想法
在本书的配套网站上找到更多关于你应用的想法:pythonicadventure.com/projects/。现在不用担心 Flask、SQLite 和 Web 应用(那里提到了这些)的问题。我们稍后会接触到这些。
2 列表:菜单上有什么?
在本章
-
埃里克开始在咖啡馆中使用菜单
-
埃里克使用 Python 列表来存储他的饮料、口味和配料
-
埃里克使用
for循环来打印列表 -
西蒙解释计算机如何存储数字和字符串
-
埃里克了解到了关于列表索引的一个重要事情
第二天,埃里克准备好继续他的咖啡馆应用程序的开发。他记得西蒙提到过关于顾客可能在对话中输入的缺失产品的事情。他来到他的哥哥那里问道:“你昨天说顾客可以输入我在店里没有的东西。我该怎么做?”
“记得你上次在咖啡馆或餐馆的时候。你是怎么知道你可以点什么的?”
“他们有一个菜单,上面列出了他们店里有的产品。”
“对!”西蒙说,“一份菜单!这就是我们今天要创建的。咖啡馆里的菜单是什么样子的?”
“这是一个列表。像咖啡、巧克力和脱咖啡这样的饮料列表。我可以添加的口味列表,比如焦糖、薄荷和其他。还有配料列表。”
“对,列表!”西蒙非常高兴埃里克用了这个词。“就像这样,对吧?”他迅速草拟了一份看起来像菜单的东西。

“列表就是我们需要的!Python 中有列表——你可能还记得。列表在 Python 中非常有用。它们可以包含数字、字符串,甚至其他列表。例如……”西蒙又拿了一张纸,写下了几个例子。

“让我们为你的菜单创建列表。你只需要给每个列表起一个名字,比如flavors,然后在你选择的方括号中列出你的口味。然后以同样的方式为你的配料和主要饮料做同样的事情。别忘了你的口味、配料和饮料是字符串,所以它们应该用引号括起来。你可以在你的编辑器中创建一个新的文件,比如命名为menu.py。”
埃里克打开了他的编辑器并开始输入。几分钟内他输入了以下内容:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
“非常好,”西蒙说。“现在让我们把它们打印成菜单的形式。”
“就打印drinks?”埃里克建议。
“你可以这样做,但看起来不会很好。试试看。”
埃里克在最后添加了print()语句:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
print(drinks)
“点击运行
,”西蒙说。
埃里克点击并看到了编辑器窗口底部的输出:
['chocolate', 'coffee', 'decaf']
>>>
“如果你的菜单是一个列表,我们应该把它打印成列表的形式,”西蒙说。“你应该给你的用户提供一种从列表中进行选择的方式。例如,你可以要求他们输入一个字母。但这里我们有巧克力和咖啡,所以你不能只用第一个字母‘C’来代表两者。让我们用数字代替。对于每个菜单项,我们将打印一个数字。然后你的用户将输入他们选择的数字。例如,1 代表巧克力,2 代表咖啡。就像这样。”西蒙拿起一张纸,画了一个简单的菜单。

“是的,我在一家中餐馆看到过这个——每道菜都有一个编号,”埃里克回忆道。“但在 Python 中我该如何实现?”
“你有一个包含几个项目的列表,”西蒙开始解释。“你必须打印每个项目,并在其前面添加一个数字。当我们需要在 Python 中重复某事时,我们使用循环。在这种情况下,它将是一个for循环。你告诉 Python 对于列表中的每个项目,它必须做某事。比如打印它。
“编写一个简单的for循环,”西蒙继续说。“让我为你写第一个。”西蒙拿起埃里克的键盘,在他的代码中添加了几行:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
for d in drinks:
print(d)
西蒙点击运行,他们看到了输出:
chocolate
coffee
decaf
>>>
“现在轮到你了,”西蒙说。“为其他两个列表编写相同的代码。注意,我使用了另一个变量来打印列表项。这个列表叫做drinks。我为列表中的每一款饮料只使用了d。通常最好为你的变量使用有意义的名称,比如drinks用于饮料列表。但如果一个变量只在一个循环中用于遍历列表,名称可以很短,比如一个或两个字母。这不是规则,但更容易输入。
“还有一件重要的事情,”西蒙继续说,“在 Python 中,空格非常重要。你看到print()函数调用向右移动了四个空格吗?这就是我们告诉 Python 在循环中应该重复什么的方式。向右移动的部分称为块。你在这个块中放入的任何内容都会为列表中的每个项目重复。现在块中只有一个函数调用,但稍后我们会添加其他内容。
“你也注意到了,我没有在键盘上输入四个空格。我们的编辑器自动为我们完成了这个操作。你将使用的所有 Python 编程编辑器都有这个功能。当它们看到冒号(:)时,会自动将下一行向右移动。这被称为‘开始一个块’。现在继续编写循环。”
埃里克创建了两个额外的循环来打印其他两个列表。他喜欢使用较短的变量名(更少的输入!)。他还注意到编辑器会自动将冒号后面的行向右移动。非常有用!这是他得到的结果:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
for d in drinks:
print(d)
for f in flavors:
print(f)
for t in toppings:
print(t)
他保存了程序并运行了:
chocolate
coffee
decaf
caramel
vanilla
peppermint
raspberry
plain
chocolate
cinnamon
caramel
>>>
轮到你了! 创建你的产品列表并打印它
打开你的代码编辑器并创建一个类似于埃里克刚刚创建的程序。它应该包含三个或更多的项目列表。然后使用循环打印出这些列表的内容。
你可以使用埃里克的菜单项或创建自己的。冰淇淋口味、百吉饼、迷你人偶——你想要什么都可以!
“非常好,”西蒙说。“但我们还没有数字。我们必须修复它。记得我告诉你我们会向块中添加其他内容吗?这是我提出的建议。我们将创建一个变量来保存列表中项目的编号。每次我们到达下一个项目时,我们将该变量加一。在这种情况下,‘巧克力’将是编号一,‘咖啡’是编号二,以此类推。
“让我给你看看,”西蒙再次拿起埃里克的键盘:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
i = 1
for d in drinks:
print(i, d)
i = i + 1
for f in flavors:
print(f)
for t in toppings:
print(t)
他运行了程序,他们看到了这个:
1 chocolate
2 coffee
3 decaf
caramel
vanilla
peppermint
raspberry
plain
chocolate
cinnamon
caramel
>>>
“你看:我添加了i变量。对于每个列表项,现在我不仅打印它的值,还打印它的编号。然后我将数字加一,从 1 到 2,然后从 2 到 3,以此类推。现在继续修改剩下的部分,”西蒙说。
埃里克做了修改:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
i = 1
for d in drinks:
print(i, d)
i = i + 1
for f in flavors:
print(i, f)
i = i + 1
for t in toppings:
print(i, t)
i = i + 1
当他运行程序时,他看到了这个输出:
1 chocolate
2 coffee
3 decaf
4 caramel
5 vanilla
6 peppermint
7 raspberry
8 plain
9 chocolate
10 cinnamon
11 caramel
>>>
“但这不是我想要的!”埃里克说。“我认为饮料应该是 1,2,3,然后是口味 1,2,3,再次是配料 1,2,3。”
“没错!”西蒙同意。“你会怎么做这个?”
“使用不同的变量?”
“是的,这是可能的。但你可以使用相同的i变量。重要的是要在每个循环前将其设置为 1。我们称这个过程为初始化变量。”
埃里克在每个循环前添加了i = 1,得到了这个结果:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
i = 1
for d in drinks:
print(i, d)
i = i + 1
i = 1
for f in flavors:
print(i, f)
i = i + 1
i = 1
for t in toppings:
print(i, t)
i = i + 1
你的机会! 打印三个(或更多)带编号的菜单
修改你的上一个程序,为菜单项添加数字。使用循环。别忘了为每个新列表重置i变量(通常称为项目计数器)。
他点击了运行,得到了这个输出:
1 chocolate
2 coffee
3 decaf
1 caramel
2 vanilla
3 peppermint
4 raspberry
5 plain
1 chocolate
2 cinnamon
3 caramel
>>>
“现在让我们让它看起来更漂亮一些,”西蒙说。“在每个列表前加上标题,比如‘我们的饮料’。记住,我们应该让用户知道他们看到的是什么,以及他们应该做什么。”
埃里克添加了一些标题。他甚至在每个标题下添加了一条额外的线条。他确信这样会使它看起来像真正的菜单:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
print("Erik's Coffee Shop drinks")
print("-------------------------")
i = 1
for d in drinks:
print(i, d)
i = i + 1
print("Erik's Coffee Shop flavors")
print("--------------------------")
i = 1
for f in flavors:
print(i, f)
i = i + 1
print("Erik's Coffee Shop toppings")
print("---------------------------")
i = 1
for t in toppings:
print(i, t)
i = i + 1
你的机会! 给你的菜单添加标题
给你的菜单添加标题,使输出更美观。在标题中使用你的商店名称。尽量使用其他符号,而不仅仅是破折号。
输出正如他预期的那样很漂亮:
Erik's Coffee Shop drinks
-------------------------
1 chocolate
2 coffee
3 decaf
Erik's Coffee Shop flavors
--------------------------
1 caramel
2 vanilla
3 peppermint
4 raspberry
5 plain
Erik's Coffee Shop toppings
---------------------------
1 chocolate
2 cinnamon
3 caramel
>>>
“看起来不错,”西蒙说。“这个格式的优点是现在你的菜单中有三个列表,你的程序中也有三个列表。”
“现在,”西蒙继续说,“对于菜单中的每个列表,你必须要求用户选择一个项目,然后从他们那里获取这个信息。你是如何从用户那里获取信息的?你昨天就做到了,记得吗?”
“使用input()吗?”埃里克问道。
“当然!”西蒙很高兴埃里克记得了之前的课程。“你可以自己写,不是吗?”
“让我试试,”埃里克说,并开始编辑他的代码。他记得应该使用input()函数。然后他把提示放在括号内,在左边使用了一个变量。他记得不应该为不同的问题使用相同的变量。
这是他写的:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
print("Erik's Coffee Shop drinks")
print("-------------------------")
i = 1
for d in drinks:
print(i, d)
i = i + 1
drink = input("Choose your drink: ")
print("Erik's Coffee Shop flavors")
print("--------------------------")
i = 1
for f in flavors:
print(i, f)
i = i + 1
flavor = input("Choose your flavor: ")
print("Erik's Coffee Shop toppings")
print("---------------------------")
i = 1
for t in toppings:
print(i, t)
i = i + 1
topping = input("Choose your topping: ")
你的机会! 在你的菜单中添加用户输入
在你的菜单中添加input()函数。使用合适的变量名来存储用户的答案。
“现在怎么办?”他问西蒙。
“现在用户输入一个数字,你使用这个数字来找到项目。在 Python 中,我们称这个数字为列表的索引。如果你把这个数字放在列表名称旁边的方括号中,你就能得到那个项目。就像这样,”然后他写了一个例子:

“所以在你知道数字后,你可以在列表中找到该项目。你可以打印,‘这是你的订单’,就像你昨天做的那样,但现在你会从菜单中获取这些项目。试试看。如果需要,我会帮你。”
这有点难。埃里克看了看昨天的程序,并将它下面的行复制到这个程序的底部。然后他按照西蒙的建议,将变量如drink替换为列表项。
这里是他的代码:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
print("Erik's Coffee Shop drinks")
print("-------------------------")
i = 1
for d in drinks:
print(i, d)
i = i + 1
drink = input("Choose your drink: ")
print("Erik's Coffee Shop flavors")
print("--------------------------")
i = 1
for f in flavors:
print(i, f)
i = i + 1
flavor = input("Choose your flavor: ")
print("Erik's Coffee Shop toppings")
print("---------------------------")
i = 1
for t in toppings:
print(i, t)
i = i + 1
topping = input("Choose your topping: ")
print("Here is your order: ")
print("Main product: ", drinks[drink])
print("Flavor: ", flavors[flavor])
print("Topping: ", toppings[topping])
print("Thanks for your order!")
“现在运行它,看看它会给我们什么结果,”西蒙说。
埃里克点击了运行,程序打印了饮料菜单并询问他的选择。到目前为止一切正常。埃里克迅速输入了所有三个菜单的数字,并看到了以下输出:
Here is your order:
Traceback (most recent call last):
File "/home/erik/mu_code/menu.py", line 30, in <module>
print("Main product: ", drinks[drink])
TypeError: list indices must be integers or slices, not str
>>>
“这是什么?”他感到困惑。
“恭喜你!”西蒙说。
“你为什么这么高兴?我的程序不起作用?”埃里克开始对他哥哥生气。
“一点也不!”西蒙说,“你从 Python 得到了第一个错误信息,这是一个好兆头!犯错并修复它们是学习的唯一方法。你已经得到了一个错误信息——现在让我们尝试解决问题。通常 Python 会告诉你为什么会发生这种情况。首先,阅读最后一条信息。它说了什么?”
“关于切片的东西……必须是整数而不是字符串。这是什么?”
“我同意,学习阅读 Python 错误信息需要一些练习。这里它告诉你,当你使用变量作为列表的索引时,该变量应该是一个整数,比如 1、2、3。”
“但我输入了数字!”埃里克仍然感到困惑。
“是的,你在键盘上输入了数字。但 Python 中,你从键盘输入的任何东西都是一个字符串。Python 区分包含数字‘1’的字符串和整数 1。”
“这来自于计算机存储东西的方式,”西蒙继续说,“计算机在内存中保留数字 1,但是当它显示给你时,它会将其转换为字符串‘1’。计算机也会将它们从键盘获取的数字转换为字符串。你在键盘上输入‘123’,计算机得到这个字符串并将其转换为数字 123。看这里。”西蒙用计算机、键盘、显示器和用户画了一张图。

“我们需要告诉 Python 将你在键盘上输入的字符串转换为整数。有一个特殊的函数叫做int()。让我给你展示如何使用它。”
西蒙在埃里克的程序中改变了一个地方,并让他在其他两个地方也做同样的修改。以下是埃里克的程序修改后的样子:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
print("Erik's Coffee Shop drinks")
print("-------------------------")
i = 1
for d in drinks:
print(i, d)
i = i + 1
drink = input("Choose your drink: ")
print("Erik's Coffee Shop flavors")
print("--------------------------")
i = 1
for f in flavors:
print(i, f)
i = i + 1
flavor = input("Choose your flavor: ")
print("Erik's Coffee Shop toppings")
print("---------------------------")
i = 1
for t in toppings:
print(i, t)
i = i + 1
topping = input("Choose your topping: ")
print("Here is your order: ")
print("Main product: ", drinks[int(drink)]) ❶
print("Flavor: ", flavors[int(flavor)]) ❶
print("Topping: ", toppings[int(topping)]) ❶
print("Thanks for your order!")
❶ 将字符串转换为数字
埃里克运行了程序,输入了他的选择(咖啡、焦糖、巧克力)并得到了以下输出:
Here is your order:
Main product: decaf
Flavor: vanilla
Topping: cinnamon
Thanks for your order!
>>>
“什么?全错了!”埃里克惊叫道,“这不是我选择的!”
“我没有告诉你关于 Python 列表的一个重要事情,”西蒙说,“它们的索引从 0 开始,而不是 1。所以如果你想从列表中获取第一个项目,你应该在方括号中放入 0。如果你想获取第二个项目,你使用 1 作为索引。”
“但是为什么?”埃里克对这样奇怪的事情感到震惊。
“这是一个很长的故事,”西蒙回答道。 “这来自于计算机在内存中存储列表的方式。你使用的索引是你应该从列表开头跳过的项目数量,以获取你想要的项目。如果你想获取列表的第一个项目,你不需要跳过任何项目。你只需从列表的开头取它。所以你应该跳过的项目数量是零,对吧?这就是为什么第一个元素的索引是 0。看这里。” 西蒙又画了一张图。

“我现在该怎么做?” 艾瑞克问道。 他认为自己理解了西蒙的解释,但仍然对这个不便感到烦恼。
“我看到你很烦恼,”西蒙说。 “别担心,你会很快习惯的。而且,你会像所有真正的程序员一样,从零开始计算一切,”西蒙微笑着说。 “现在你只需从方括号中的每个索引中减去一个。但要注意:你必须在将输入转换为整数之后,而不是之前添加那个- 1。就像这样:drinks[int(drink) - 1]。”
艾瑞克修复了他的代码,现在它看起来像这样:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
print("Erik's Coffee Shop drinks")
print("-------------------------")
i = 1
for d in drinks:
print(i, d)
i = i + 1
drink = input("Choose your drink: ")
print("Erik's Coffee Shop flavors")
print("--------------------------")
i = 1
for f in flavors:
print(i, f)
i = i + 1
flavor = input("Choose your flavor: ")
print("Erik's Coffee Shop toppings")
print("---------------------------")
i = 1
for t in toppings:
print(i, t)
i = i + 1
topping = input("Choose your topping: ")
print("Here is your order: ")
print("Main product: ", drinks[int(drink) - 1])
print("Flavor: ", flavors[int(flavor) - 1])
print("Topping: ", toppings[int(topping) - 1])
print("Thanks for your order!")
轮到你了! 修改你的程序以打印订单
按照艾瑞克刚才的方式修改你的程序并打印你的订单。别忘了将输入字符串转换为数字。别忘了从每个数字中减去一个(1)——列表索引从零开始,记住?
他运行了程序,输入了 2,1,1,最终得到了他想要的结果:
Here is your order:
Main product: chocolate
Flavor: caramel
Topping: chocolate
Thanks for your order!
>>>
“酷!它工作了!” 艾瑞克显然很高兴。 “我喜欢我的咖啡馆程序!我们完成了吗?”
“差不多,”西蒙回答道。 “看,你几乎三次写出了完全相同的代码。”
“那有什么问题?”
“想象一下你想在代码中更改一些东西。例如,你可能想更改打印菜单项的方式。你将不得不在所有三个地方进行更改。或者如果你决定添加其他菜单列表,你甚至可能需要在更多的地方进行更改。想象一下你想在你的咖啡馆里添加甜点。这意味着你将不得不再次复制这段代码。如果你在代码中犯了错误怎么办?程序员称之为bug(我稍后会告诉你原因)。然后你将不得不在四个地方修复那个 bug,重复自己。程序员喜欢 DRY 原则:不要重复自己。”
“但我不知道我怎么能做到这一点,”艾瑞克感到困惑。 “如果我有三个菜单列表,我必须打印三次。我必须三次向用户请求输入。”
“我们在这里可以使用一个函数,”西蒙解释道。 “记得我们开始使用print()函数时,我告诉过你,对于我们要反复进行的操作,我们使用函数。到目前为止,我们使用的是别人写的函数。现在我们将创建我们自己的函数并使用它。”
“这很酷。我喜欢它,”艾瑞克说。
“太好了,我们明天再做。我认为我们今天已经做得足够多了。你做得很好,艾瑞克。” 西蒙确实很高兴他的兄弟在进步。“让我们回顾一下我们今天学到了什么。第一件事是什么?”
“首先,我们创建了列表,” 艾瑞克说。“我们把所有的饮料、口味和配料都放在列表中。”
“很好,接下来是什么?”
“然后我们使用循环打印了列表,然后打印了每个饮料或口味的旁边数字。”
“是的,没错,” 西蒙确认道。“继续,之后发生了什么?”
“然后我尝试从列表中打印饮料,但 Python 给我返回了一个错误。你解释了数字是如何存储在计算机内存中的。然后我们转换了数字,我再次尝试打印我的订单。而且正是因为你没有告诉我索引是从零开始的”(艾瑞克没有忘记这一点),“我的订单打印出了所有错误的项目!”
“请,原谅我,” 西蒙微笑着说。“但我相信你现在会记得更好!”
“终于修好了,现在运行得很好!” 艾瑞克说。
“干得好!” 西蒙给艾瑞克竖起大拇指。“我们明天继续,并编写我们的第一个函数。”
今天你学到的新东西
-
列表—Python 中项目的一个集合。列表中可以包含字符串或数字,甚至它们的混合。
-
列表索引—我们用来从列表中检索项目的数字。列表中的索引总是从零开始,每个下一个元素增加一:0,1,2,3,依此类推。
-
数字和字符串—这些是 Python 中不同类型的变量。当你想在屏幕上打印某物或从键盘获取输入时,你总是使用字符串。当你想对用户接收到的数字进行任何数学运算时,你必须将它们从字符串转换为数字。
本章代码
你可以在这里找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch02。
3 个函数:不要重复自己!
在本章中
-
西蒙解释如何避免重复(以及为什么)
-
艾瑞克编写他的第一个 Python 函数
-
艾瑞克开始使用 Python 的交互式工具
-
艾瑞克改进了他的函数,使他的收据看起来更专业
“我们昨天做到哪里了?”西蒙第二天问艾瑞克。
“你本来要告诉我为什么错误被称为错误的,”艾瑞克说。
“对!”西蒙说。“信不信由你,很多年前,计算机是由叫做继电器这样的机电装置组成的,它们有类似于开关的触点。有一天,工程师们发现他们的程序中有一个错误。他们的代码是正确的,但他们发现一个错误卡在了一个继电器触点之间。这就是为什么程序员把错误称为错误。错误通常隐藏在你的代码中,你必须移除它们才能让你的程序工作。还有什么?”
“你说我不应该重复自己。你也说过我们今天要自己写一个函数。”
“对!首先,告诉我你到目前为止对函数的了解。”
“我们已经使用了一些函数,”艾瑞克回答。“我们使用了 print() 和 input()。你说有人写了它们,所以我们可以使用它们。我们也可以在函数中使用参数。我们只需要在括号中放入参数,函数就会对它们做些什么,比如打印它们。”
“没错!你是个优秀的学生!”西蒙微笑着说。“函数的重要之处在于它们每次都可以做相同的事情,但使用不同的参数。所以如果你看到自己在多次做相同的事情,你应该看看是否可以将其转换成一个函数。为了决定,你应该看看你重复的代码,并问自己哪些部分是相同的,哪些是不同的。你的程序昨天是从哪里开始的?”
“在这里。”艾瑞克在编辑器中打开了 menu.py 文件:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
print("Erik's Coffee Shop drinks")
print("-------------------------")
i = 1
for d in drinks:
print(i, d)
i = i + 1
drink = input("Choose your drink: ")
print("Erik's Coffee Shop flavors")
print("--------------------------")
i = 1
for f in flavors:
print(i, f)
i = i + 1
flavor = input("Choose your flavor: ")
print("Erik's Coffee Shop toppings")
print("---------------------------")
i = 1
for t in toppings:
print(i, t)
i = i + 1
topping = input("Choose your topping: ")
print("Here is your order: ")
print("Main product: ", drinks[int(drink) - 1])
print("Flavor: ", flavors[int(flavor) - 1])
print("Topping: ", toppings[int(topping) - 1])
print("Thanks for your order!")
“看,这里有什么是重复的吗?”西蒙问,并开始用艾瑞克的代码画图。

“循环。开头的 print() 和结尾的 input()。”
“这三者有什么不同吗?”
“input() 的提示不同,”艾瑞克回答。“而且标题也略有不同。”
“而且你正在运行 for 循环遍历不同的列表,比如 drinks、flavors 和 toppings,对吧?”西蒙决定帮助艾瑞克。“所以我们将不同的东西作为参数传递。在我们的情况下,它们将是选择列表、菜单标题和输入提示。”
“让我们在编辑器中新建一个文件,并命名为 menu_function.py。我们将在那里写我们的函数。”
艾瑞克在编辑器中点击了“新建”然后“保存”。然后他输入了 menu_function.py,准备开始写代码。
“Python 中的函数以单词 def 开头,后面跟着函数名,”西蒙继续说。“让我们把我们的函数命名为 menu。然后你打开括号,列出你的参数。”
艾瑞克写下了 def menu( 并不确定接下来该做什么。
西蒙帮忙说,“我们刚刚讨论了你的参数。我看到你在想如何命名它们。记住,命名变量和参数是计算机科学中最困难的问题之一。你不是一个人。让我们将它们命名为choices、title和prompt。只需在开括号后输入它们,并在闭括号后加一个冒号。”
埃里克输入了以下内容:
def menu(choices, title, prompt):
他注意到在按下 Enter 键后,光标移动到了下一行,但向右移动了四个空格。“我应该在这里写吗?”他问西蒙。
“是的,当然!”西蒙回答。“你看,编辑器正在帮助你编写函数!现在看看你昨天的代码,开始复制你想放入函数中的内容。首先,我们打印了标题,所以我们也在这里做同样的事情,但不是打印实际的字符串,而是打印名为title的参数。你甚至可以像之前一样在它后面放上一行破折号。”
埃里克写道:
def menu(choices, title, prompt):
print(title)
print("--------------------------------")
“现在写循环,”西蒙继续说。“但你的列表现在叫做choices,而不是drinks或flavors。你可以在循环中使用变量c,因为它是choices的第一个字母。”
埃里克从先前的程序中复制了循环。他没有询问他的哥哥就添加了input()函数和prompt:
def menu(choices, title, prompt):
print(title)
print("--------------------------------")
i = 1
for c in choices:
print(i, c)
i = i + 1
choice = input(prompt)
“太好了!”当看到埃里克的代码时,西蒙说。“现在我们必须返回选择。”
“我们难道不能直接在我们的主程序中打印choice变量吗?”埃里克问道。
“不!这是函数的一个重要特性。”西蒙很高兴埃里克提出了这个问题。“你函数内部的变量只对函数内部是可见的。看这里,我会画一个图。”

“这意味着当你不在函数内部时,你无法看到那些变量里面的内容。所以如果我们想让我们的主程序看到这些值,我们必须返回这些变量。通常,一个函数内部会有很多变量,但我们只想返回一个或两个作为结果。在这种情况下,我们可以返回用户输入的数字,它存储在变量choice中。”
西蒙停顿了一下,思考了一会儿。“但我们可以做得更好,”他说。“再看看你昨天的代码。还有什么在重复?”
埃里克看了看说,“那些int()函数,我们不得不使用- 1三次。这很烦人。”他还没有习惯列表索引从零开始的事实。
“好的,让我们也将它们添加到函数中,”西蒙建议。“我们将把用户的答案转换为整数,从列表中获取项目,并返回项目,而不是它的索引。这将使我们的函数更有用。调用它的主程序将得到用户的选择,而不仅仅是某个数字。让我给你展示,”西蒙在函数中添加了转换操作和return语句:
def menu(choices, title, prompt):
print(title)
print("--------------------------------")
i = 1
for c in choices:
print(i, c)
i = i + 1
choice = input(prompt)
answer = choices[int(choice) - 1]
return answer
“现在让我们测试一下,”Simon 说。“在我们的主程序中,就在函数下方,我们将调用它并打印我们得到的答案。但首先,我们需要我们的饮料和其他内容的列表。从昨天的程序顶部复制它们。”
Erik 在函数下方添加了三个列表。这次行没有右移,而是从第一个位置开始:
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
“很好,”Simon 说。“现在你准备好调用你的函数了。传递标题、饮料列表和input提示。然后将函数的结果分配给一个变量。例如,可以叫它choice。然后打印它。”
Erik 遵循 Simon 的指示,这是他得到的结果:
def menu(choices, title, prompt):
print(title)
print("--------------------------------")
i = 1
for c in choices:
print(i, c)
i = i + 1
choice = input(prompt)
answer = choices[int(choice) - 1]
return answer
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
choice = menu(drinks, "Erik's drinks", "Choose your drink: ")
print(choice)
你的回合! 创建你自己的函数
创建一个与 Erik 刚刚创建的类似的自定义函数。使用你的菜单列表、标题和提示。尝试在 Erik 之前运行它。
“我应该运行它吗?”Erik 问道。
“是的,现在准备好了。继续!”
Erik 点击了运行,程序以与他之前相同的方式询问他的饮料。Erik 回答后得到了他预期的结果:
Erik's drinks
--------------------------------
1 chocolate
2 coffee
3 decaf
Choose your drink: 2
coffee
>>>
“它工作了!”他说。“我会在这里添加其他菜单,”他开始写作。不到 10 分钟,他就准备好测试整个程序了。现在它看起来是这样的:
def menu(choices, title, prompt):
print(title)
print("--------------------------------")
i = 1
for c in choices:
print(i, c)
i = i + 1
choice = input(prompt)
answer = choices[int(choice) - 1]
return answer
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
drink = menu(drinks, "Erik's drinks", "Choose your drink: ")
flavor = menu(flavors, "Erik's flavors", "Choose your flavor: ")
topping = menu(toppings, "Erik's toppings", "Choose your topping: ")
print("Here is your order: ")
print("Main product: ", drink)
print("Flavor: ", flavor)
print("Topping: ", topping)
print("Thanks for your order!")
你的回合! 添加其他菜单
在你的程序中添加其他菜单。它们将使用相同的函数,但具有不同的参数——选择列表、标题和提示。尝试运行它并使用你的菜单选择进行测试。
它按预期工作了!Erik 运行了程序;再次输入 2, 2, 1;并得到了他的订单:
Here is your order:
Main product: coffee
Flavor: vanilla
Topping: chocolate
Thanks for your order!
>>>
Simon 说,“注意你的程序变短了。现在如果你需要更改某些内容,你只需要在一个地方更改。”
“我为什么要改变它?它已经很好工作了,”Erik 说。
“哦,总有方法可以改进你的代码!”Simon 回答。“让我们让它的标题更好一些。你注意到你的破折号行现在比标题长了吗?”
“真的吗?哦,是的,你是对的,”Erik 说。“这是因为我把标题改成了‘Erik 的饮料’。我可以让它更短,这很简单。”
“当然,你可以让它更短,但看——在drinks菜单中,标题更短,而在toppings菜单中,它更长。破折号行应该与你的标题字符串长度相同。”
“但我该如何做到这一点?也许删除那一行破折号会更好?”Erik 感到困惑。
“不,我喜欢你这里的行;它让你的收据看起来更真实。我想让你保留它。但我们必须计算标题的长度并使破折号行长度相同。你在这里将学习一个新函数和一个新操作。”
“我们将要使用的函数叫做len()。你只需在括号内放入一个字符串参数,它就会返回字符串的长度。让我给你演示一下。我们将使用 Mu 编辑器的一个其他功能,叫做REPL。它代表Read–Eval–Print–Loop,它让你可以交互式地使用 Python。我通常在我想快速测试某事时使用它。或者向某人展示某物,就像现在这样,”Simon 微笑着。
“点击 REPL
,”西蒙继续说。
艾瑞克点击了一下,穆编辑器窗口底部又打开了一个新窗口。
“你看——你这里有一个交互式的 Python 环境,”西蒙说。“你可以在这里输入任何 Python 代码,它会被执行。你甚至可以用它作为计算器。试着在In [1]:后面输入print('hello world')或者2 + 2。在 REPL 中,In代表输入,它后面的数字是你的命令编号。REPL 会打印Out[1]:然后是答案。你会看到的。”

艾瑞克输入了,西蒙确实是对的:
In [1]: print('hello world')
hello world
In [2]: 2 + 2
Out[2]: 4
“现在我们来计算字符串的长度。输入len('abcd')。”
艾瑞克输入后得到了这个:
In [3]: len('abcd')
Out[3]: 4
“现在你看到了字符串abcd的长度是 4,”西蒙说。“你也可以用字符串变量来做同样的事情。使用变量s,将字符串'hello'放入其中,并计算它的长度。我相信你现在知道怎么做了。”
艾瑞克在 REPL 窗口中输入了,并得到了这个结果:
In [4]: s = 'hello'
In [5]: len(s)
Out[5]: 5
“很好,”西蒙说。“现在你知道了,如果你有一个字符串,你总是可以获取它的长度。更进一步,你也可以这样获取列表的长度。创建一个数字列表,1, 2, 3,然后获取它的长度。比如叫它n。”
艾瑞克输入了:
In [6]: n = [1, 2, 3]
In [7]: len(n)
Out[7]: 3
“我们稍后会用到这个,但现在让我给你展示一个技巧,”西蒙继续说。“如果我让 Python 取一个数字,2,然后乘以 2,它会给我什么?”
“4?”艾瑞克不确定这算不算是一个技巧。这个问题太简单了。
“对。如果我取一个字母,‘A’,然后乘以 2,Python 会给我什么?”
“我不知道?2A,可能吧?”
“用交互式 Python 试一试!”西蒙建议。
艾瑞克输入后得到了结果:
In [8]: 2 * 'A'
Out[8]: 'AA'
“有趣!”艾瑞克感到惊讶。
“现在如果你用横线代替‘A’并乘以 10 会怎样?”
艾瑞克开始猜测西蒙要带他走到哪里,于是他输入了:
In [9]: 10 * '-'
Out[9]: '----------'
“现在将数字 10 替换为字符串'hello'的长度。”
艾瑞克现在明白了西蒙的想法:
In [10]: len('hello') * '-'
Out[10]: '-----'
你的回合! 使用 REPL 并实验len()函数
通过在编辑器中点击其图标来启动 REPL。然后重复艾瑞克的所有实验。
尝试将一个数字乘以由两到三个字母组成的字符串。你能猜到输出结果会是什么吗?
“我现在明白了!”他说。“我们取title参数,计算它的长度,然后打印出一条与长度完全相同的横线!”
“你现在可以修改你的函数了吗?”西蒙问道。
“是的,当然,我知道该怎么做!”艾瑞克已经开始输入了。他只改动了第三行,现在他的函数看起来是这样的:
def menu(choices, title, prompt):
print(title)
print(len(title) * '-') ❶
i = 1
for c in choices:
print(i, c)
i = i + 1
choice = input(prompt)
answer = choices[int(choice) - 1]
return answer
❶ 重复横线(-)多次
你的回合! 将你的函数修改为打印正确的横线
修改你的程序以打印正确的横线数量,类似于艾瑞克刚才所做的那样。试着使用不同的符号——一个等号(=),或者下划线(_),或者其他的。
艾瑞克测试了主程序,现在所有的横线长度都和它们的标题完全一样。
西蒙评论说:“现在你看到,不仅你函数返回的结果取决于参数,而且它打印的内容也可以取决于参数。
“分析你在函数中接收到的参数总是一个好主意,”西蒙继续说。“在这个例子中,我们检查了标题的长度。如果你的函数接收到一个长度为零的空字符串,它会做什么?”
“我不知道,”埃里克回答。“我想它会打印一个空字符串——什么都没有。”
“对,”西蒙说。“但是也许我们还可以打印一些合理的内容,即使标题是空的。也许只是‘埃里克的菜单’和一串破折号。在 Python 中,对于函数参数,我们有默认值。在你的函数中,我会把第一行改为这样。”西蒙编辑了埃里克的文件:
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
“在这种情况下,我们告诉 Python,‘如果有一个title参数,就接受它的值。’如果你在调用函数时没有传递title,函数将使用其默认值,即Erik’s Menu。通常建议你设置默认值。你总是可以在调用函数时将其更改为其他内容。
“我们来测试一下,”西蒙建议。“在你第一次调用drinks时,移除标题和提示。只留下drinks作为一个单独的参数。”
埃里克按照西蒙的建议做了,第一次函数调用看起来是这样的:
drink = menu(drinks)
他再次运行了程序,看到了第一个菜单:
Erik's Menu
-----------
1 chocolate
2 coffee
3 decaf
Choose your item:
下面是埃里克的全程序。
列表 3.1 使用函数的完整菜单程序
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
print(title)
print(len(title) * "-")
i = 1
for c in choices:
print(i, c)
i = i + 1
choice = input(prompt)
answer = choices[int(choice) - 1]
return answer
drinks = ["chocolate", "coffee", "decaf"]
flavors = ["caramel", "vanilla", "peppermint", "raspberry", "plain"]
toppings = ["chocolate", "cinnamon", "caramel"]
drink = menu(drinks)
flavor = menu(flavors, "Erik's flavors", "Choose your flavor: ")
topping = menu(toppings, "Erik's toppings", "Choose your topping: ")
print("Here is your order: ")
print("Main product: ", drink)
print("Flavor: ", flavor)
print("Topping: ", topping)
print("Thanks for your order!")
轮到你了! 在你的函数中使用默认参数
为你的title和prompt参数添加默认值。尝试不传递这些参数调用你的函数,并确保它使用默认值。
“当然,现在它没有告诉用户这是一个饮料菜单,但比空字符串要好。当你想快速测试某事时,这很有帮助。你总是可以稍后添加更多描述性的标题和提示。
“我想今天就这样吧,”西蒙说。“让我们回顾一下你学到了什么。今天的第一件事是什么?”
“我们看了我昨天写的程序,发现有些东西重复了三次。然后你告诉我我们可以写自己的函数。你给我介绍了单词def和参数。”
“很好,继续,”西蒙鼓励埃里克。“那些参数呢?”
“我在我的函数中使用了选择列表、标题和提示作为参数。”埃里克喜欢谈论他的函数——这是他第一次自己写!
“然后你向我展示了编辑器中的那个REPL东西,”埃里克继续说。“我喜欢它!然后我们计算了字符串的长度。”
“我们用它来做什么?”西蒙问。
“我们用它来打印我们的收据,现在它们看起来很漂亮。然后我们尝试为参数使用默认值。有点无聊,但它是有效的。”
西蒙说:“你刚才说的这件事非常重要。好的编程解决方案通常看起来很无聊,但它们是有效的。编程并不总是关于花哨的技巧和黑客手段。大多数时候,你必须做一些很无聊的事情,比如检查用户的输入,查找错误等等。但如果做这些无聊的事情能让你的程序运行,那么它们就是值得的。明天我们会看看你需要做什么来确保即使用户输入错误值,你的程序也能正常工作。但现在——好好休息一下!你今天做得很好!”
你今天学到的新知识
-
函数——可以重复使用(调用)的编程代码块。函数可以由你或其他人编写。如果是由其他人编写的,它通常是 Python 中的库或模块的一部分。
-
参数——当我们调用函数时传递给函数的变量。函数接收这些参数并使用它们来准备其输出。输出可以是打印出来,或者返回到主程序。
-
REPL——读取-评估-打印-循环,一种交互式运行 Python 的方法。它对于快速测试函数非常有用。
本章代码
你可以在这里找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch03。
4 用户错误:每个人都会犯错误
在本章
-
艾瑞克发现用户并不总是按照你的指示去做
-
艾瑞克学习如何使用循环重复向用户提问
-
西蒙帮助艾瑞克使菜单更加健壮
“昨天你做得很好,艾瑞克,”西蒙第二天说,“你写了一个非常好的函数,你添加了默认参数,并测试了它。”
“是的,”艾瑞克回答。“我认为这是一个好程序。我想向我的朋友们展示!”
“等等,等等,”西蒙说,“我认为它还没有准备好使用。”
“为什么?我认为它工作得很好!”
“哦,真的吗?让我试试。”西蒙看起来好像有什么想法。他重新启动了艾瑞克的程序,在第一个菜单中他输入了coffee:
Erik's drinks
--------------------------------
1 chocolate
2 coffee
3 decaf
Choose your drink: coffee
Traceback (most recent call last): ❶
File "/home/erik/mu_code/menu_function.py", line 18, in <module> ❶
drink = menu("Erik's drinks", drinks, "Choose your drink: ") ❶
File "/home/erik/mu_code/menu_function.py", line 9, in menu ❶
answer = choices[int(choice) - 1] ❶
ValueError: invalid literal for int() with base 10: 'coffee' ❶
>>>
❶ Python 的错误信息
“你在做什么?”艾瑞克愤怒地说,“你应该只输入数字,而不是单词!”
“但是你给了我一个列表并问我想要什么。我想喝咖啡,所以我输入了coffee。怎么了?”西蒙试图表现得无辜,但他无法掩饰他的笑容。
“嗯,对于像你这样愚蠢的用户,我会全部大写地打印出你应该输入一个数字!”艾瑞克抱怨。
“好吧,好吧,让我再试一次,”西蒙说。他重新启动了程序,在第一个菜单中,他输入了42:
Choose your drink: 42
Traceback (most recent call last): ❶
File "/home/erik/mu_code/menu_function.py", line 18, in <module> ❶
drink = menu("Erik's drinks", drinks, "Choose your drink: ") ❶
File "/home/erik/mu_code/menu_function.py", line 9, in menu ❶
answer = choices[int(choice) - 1] ❶
IndexError: list index out of range ❶
>>>
❶ Python 的错误信息
“又来了?你又把它搞坏了?”艾瑞克准备拍西蒙的键盘。 “你没看到只有三个选项吗?你为什么输入 42?”
“首先,因为 42 是我的幸运数字。其次,是的,那是我犯的错误。用户会犯错误,你知道的。说真的,我想向你展示你的程序应该为此做好准备。你可以打印任何你想要的内容,全部大写,但会有用户不会阅读它。会有用户犯错误。”
“我该怎么做?”艾瑞克仍然对他哥哥很生气,但他倾向于同意他的观点。他自己也犯过程序错误。
“你应该检查用户输入的内容,并告诉他们输入是否错误。让我们想想我们能做什么。
“用户在第一个菜单中应该输入什么?”西蒙问。
“他们应该输入 1,2,或者 3,”艾瑞克回答。
“好吧,所以我们可以检查他们的答案是不是 1,2,或者 3,然后我们传递给函数并从选项列表中选取那个项目。但如果不是,我们应该告诉用户出错了。”
“是的,我记得。我们可以在 Python 中使用if-else。” 艾瑞克建议。
“让我们试试,”西蒙说,“你打算怎么做?试着向我解释一下,就像我不知道if-else一样。”
“我会在我的函数中添加,‘如果用户的选项是 1,2,或者 3,那么就继续使用它。如果不是(else),打印用户应该输入那些数字’。”
“那么我们如何检查用户的选项是 1,2,还是 3?我们在 Python 中怎么做?”西蒙问。
“我不记得了……” 艾瑞克说。
“你需要使用一个比较运算符。它使用两个等号,就像这样:==。例如,要检查choice是否等于 1,你会写choice == '1'。”
“但我想要检查所有三个数字,”埃里克说。“我应该写if choice == '1' or '2' or '3'吗?”
“你差不多对了!”西蒙回答。“是的,你可以在 Python 中使用or,但在这个情况下,你应该将其写成三个独立的比较,并在它们之间放置or。就像这样,”他在一张纸上写下了这段代码:
if choice == '1' or choice == '2' or choice == '3':
“好的,明白了,”埃里克说。
“好的,让我们写代码,”西蒙说。
埃里克打开了他的编辑器,并将四行代码添加到return之前:
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
print(title)
print(len(title) * "-")
i = 1
for c in choices:
print(i, c)
i = i + 1
choice = input(prompt)
if choice == '1' or choice == '2' or choice == '3':
answer = choices[int(choice) - 1]
else:
print("Enter number 1, 2, or 3!")
return answer
“现在让我们试试,”西蒙说。
埃里克启动了程序,并在第一次菜单中键入了2。
“你为什么输入 2?”西蒙问。
“因为我想要咖啡,”埃里克回答。
“但我们应该测试你的程序对错误答案的处理!”西蒙说。“是的,我知道,这是你的程序,你不想让它出问题,但作为一个开发者,你必须尝试破坏你的程序。你必须想象用户可能会以所有可能的方式使用你的程序。我知道这很困难,也很不舒服,但你必须克服这一点,并尝试输入所有可能的错误值。”
“好吧,好吧,”埃里克说,并重新启动了他的程序。在第一次菜单中,他像上一次西蒙做的那样输入了42(埃里克的输入以粗体显示):
Erik's Menu
-----------
1 chocolate
2 coffee
3 decaf
Choose your item: 42
Enter number 1, 2, or 3!
Traceback (most recent call last): ❶
File "/home/erik/mu_code/menu_function.py", line 21, in <module> ❶
drink = menu(drinks) ❶
File "/home/erik/mu_code/menu_function.py", line 14, in menu ❶
return answer ❶
UnboundLocalError: local variable 'answer' referenced before assignment ❷
>>>
❶ Python 会告诉你错误在哪里。
❷ Python 会告诉你错误是什么
注意:你可以在本书的配套网站上了解更多关于 Python 错误信息的内容:pythonicadventure.com/troubleshooting/index.html。
“让我们看看这里发生了什么,”西蒙说。“首先,当你输入42时,你的程序打印出用户应该只输入 1、2 或 3 的消息。这是好的。但然后出了点问题。看,它说变量answer在赋值之前被引用了:
UnboundLocalError: local variable 'answer' referenced before assignment
“简单来说,这意味着你没有创建answer,但你尝试使用它。Python 会确切地告诉你:你尝试return answer,但 Python 对变量answer一无所知。”
“但为什么?”埃里克说。“我的代码里有一行answer =。”
“是的,你有它,但重要的是在哪里使用这一行。在你的代码中,你只在用户输入正确的选择时创建answer变量。如果用户输入其他内容,answer甚至不会被创建。
“换句话说,”西蒙继续说,“即使用户回答了一个错误的数字,或者甚至是一个单词,你仍然必须返回某个答案。这是一条非常重要的规则:在创建变量并给它赋值之前,永远不要使用变量。在这种情况下,如果用户出错,我们可以给answer赋什么值呢?一个空字符串''应该可以在这里工作。把它添加到你的函数中,并检查它是否有所帮助。”
埃里克将函数更改为以下内容:
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
print(title)
print(len(title) * "-")
i = 1
for c in choices:
print(i, c)
i = i + 1
choice = input(prompt)
if choice == '1' or choice == '2' or choice == '3':
answer = choices[int(choice) - 1]
else:
print("Enter number 1, 2, or 3!")
answer = '' ❶
return answer
❶ 将一个空行分配给answer。
轮到你了! 添加代码来检查用户的答案
在你的菜单函数中,添加埃里克刚刚添加的代码。确保在输入错误的情况下将一个空行分配给answer。测试它是否真的检查了你的答案。
他再次测试了程序,这次它没有给他错误。它打印了消息输入数字 1, 2 或 3!并跳转到下一个菜单。
“你认为你的程序做得对吗?”西蒙问。
“它打印了你应该输入 1, 2 或 3 的消息,”埃里克回答。“我认为是对的。”
“但你没有获取用户的饮料选择。如果他们输入了错误的数字,你应该给他们一个机会输入正确的数字。这不像学校的考试,你只有一个回答的机会。你应该不断询问用户,直到你得到正确的答案。”
“我该怎么做?”埃里克问。
“还有另一种循环,”西蒙向他的兄弟解释说,“它被称为while 循环。它会一次又一次地重复某件事,并且每次循环都会检查条件。当条件为真时,它会继续。如果它变为假,循环结束。
“有时候,如果我们已经知道了条件,我们会在循环的开始处检查它。在这种情况下,我们会说‘当某事为真时,做这件事。’但有时,就像我们的情况一样,我们在开始循环时还没有答案,因为我们还没有询问用户他们的选择。所以,我们启动一个无限循环并在循环内部检查条件——在我们收到用户的答案之后。如果条件变为真,我们就退出循环。我们称这为跳出循环。让我用草图展示一下,”西蒙说,并开始画图。

西蒙解释说,“在这个例子中,我们在开始循环之前已经知道了条件。我们检查条件并决定是否开始。它应该是True,否则循环甚至不会开始。我用了菱形来表示这个检查;这是程序员通常绘制决策点的方式。
“如果它是真的,那么我们就做某件事。重要的是,这个‘做某件事’应该改变条件,以及其他事情。否则,循环将永远继续,我们不想那样。
“在我们做了那‘某件事’之后,我们回到循环的开始处并再次检查条件。如果它仍然为真,我们就重复那‘做某件事’。如果不为真,我们就退出循环并继续我们的程序。
“看看这个简单的例子:我们想要从 5 开始倒数到 0。首先,我们将变量n设置为 5,并在循环的开始处检查它是否大于 0。是的,它大于 0,所以我们开始循环。记住,我们必须在某个时候改变条件,否则循环将永远不会结束。在我们的例子中,我们每次通过循环时都会从n中减去 1。最终,变量n将等于 0,循环将停止。
“在这个情况下,重要的是,在我们开始循环之前,我们已经知道了变量n的内容,并且我们知道它大于 0。
“如果我们不知道变量里有什么,或者变量甚至不存在怎么办?就像我们这个例子一样,我们只能在用户被要求从菜单中选择一个项目之后检查用户的答案。所以我们必须这样做,” 西蒙又画了一个图。

西蒙继续解释,“在这里,我们开始循环而不检查任何条件。我们先做些事情,然后才检查条件。有时这种循环被称为‘do-until’,意思是‘做某事直到条件为真。’当条件为真时,我们退出循环并继续执行程序的其余部分。”
在这个例子中,我使用了你的 input() 情况。你请求输入,然后检查这个输入。这是你的条件:如果输入有效,你应该退出循环。在 Python 中,我们使用 break 操作符来实现这一点。”
艾瑞克对这个长篇大论感到有些不知所措,但他感觉他知道该怎么做。他问他的哥哥,“所以你是说,我应该在我的 input() 行前面放一个 while,在我得到正确答案后添加一个 break 吗?”
“是的,” 西蒙回答,“完全正确!只是别忘了将所有属于循环的行向右缩进四个空格。你的编辑器会帮你的,别担心。”
艾瑞克开始编写他的程序。几分钟过后,他的函数看起来是这样的:
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
print(title)
print(len(title) * "-")
i = 1
for c in choices:
print(i, c)
i = i + 1
while True:
choice = input(prompt)
if choice == '1' or choice == '2' or choice == '3':
answer = choices[int(choice) - 1]
break
else:
print("Enter number 1, 2, or 3!")
answer = ''
return answer
“对吗?” 艾瑞克问他的哥哥。
“我告诉你你的编辑器可以帮你。点击检查
”
艾瑞克按照他哥哥的建议做了,看到了这个:

“你现在看懂了吗?” 西蒙说。“你忘记将右边的行缩进了。这就是为什么它说这里期望一个缩进的块。将所有属于循环的行都向右移动。”
艾瑞克修改了他的函数并再次检查了代码:
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
print(title)
print(len(title) * "-")
i = 1
for c in choices:
print(i, c)
i = i + 1
while True: ❶
choice = input(prompt)
if choice == '1' or choice == '2' or choice == '3':
answer = choices[int(choice) - 1]
break ❷
else:
print("Enter number 1, 2, or 3!")
answer = ''
❸
return answer
❶ 当循环开始时。
❷ 如果答案是正确的,退出循环。
❸ 缩进块的结束;循环结束
轮到你了! 在你的函数中添加一个 while 循环
添加一个像艾瑞克刚刚做的循环。尝试使用检查按钮来找出你的代码中是否有错误。故意制造一些错误,看看编辑器是否能找到它们。
现在,检查按钮显示了一个绿色的点赞图标
一会儿,所以艾瑞克知道这是可以的。他点击运行来测试程序。在第一个菜单中,他输入了 42,就像西蒙上次做的那样。程序报告说他应该选择 1、2 或 3,然后又回到了提示符。它没有崩溃;它运行正常!
“西蒙在我的程序里还做了什么其他错误的事情?” 艾瑞克试图回忆。“对了,他尝试输入‘咖啡’!我们也试试这个。”
他输入了 coffee,他的程序再次如他所预期地响应!最后,艾瑞克输入了 2 并进入了下一个菜单。这是他在编辑器窗口中看到的内容:
Erik's Menu
-----------
1 chocolate
2 coffee
3 decaf
Choose your item: 42
Enter number 1, 2, or 3!
Choose your item: coffee
Enter number 1, 2, or 3!
Choose your item: 2
Erik's flavors
--------------
在下一个菜单中,他输入了 4 以选择树莓派,得到了同样的信息:
Erik's flavors
--------------
1 caramel
2 vanilla
3 peppermint
4 raspberry
5 plain
Choose your flavor: 4
Enter number 1, 2, or 3!
Choose your flavor:
“为什么?” 他问西蒙。
“你的程序的行为完全符合你写的,” 西蒙回答。当然,他知道问题在哪里。
“你是怎么写你的条件的?”他问道。
埃里克说:“如果答案是 1、2 或 3——啊哈,我现在明白了!我输入了4,所以程序认为这是一个错误的答案!但我怎么修复这个问题?”
“看起来我们需要为每个菜单列表提供一个有效答案列表,”西蒙说。“你可以将其作为另一个参数传递。但我认为你已经足够了解如何创建一个更好的解决方案了。”
“是什么?”埃里克问。“又是关于循环的事情?”
“不仅仅是循环,”西蒙回答。“你也会对列表有新的了解。是的,我知道你已经厌倦了我的讲座,但让我们今天完成它。这样可以让你的程序再次正确工作——这难道不值得吗?”然后他向他的兄弟眨了眨眼。
埃里克感到疲倦,但那个带有风味菜单的 bug(是的,他已经学会了这个词!)真的很烦人,他想要修复它。“好吧,”他叹了口气,“我们来修复它。你想要告诉我关于列表的什么?”
“看看你的条件,”西蒙说。“你使用了一个简单的if-else检查,并且将输入与三个有效答案:1、2 和 3 进行了比较。但如果你的项目列表很长,比如 20 个项目,你的if-else块会太长。Python 中还有另一种方法。我们可以检查某个项目是否在列表中。在我们的情况下,我们可以检查用户得到的答案是否在 1、2、3 的列表中。我们可以为第一个菜单尝试一下。让我给你看看。”
西蒙拿起埃里克的键盘,并更改了他函数中if choice == . . .行的内容,改为以下内容:
. . .
while True:
choice = input(prompt)
if choice not in ['1', '2', '3']:
answer = choices[int(choice) - 1]
break
else:
print("Enter number 1, 2, or 3!")
answer = ''
. . .
“这需要一些解释,”他说。“看,用户输入了一个可以是 1 或 42 的字符串。我们测试这个字符串是否在允许答案的列表中,这个列表是 1、2 或 3。如果它不在列表中,我们打印错误信息并继续循环。如果它确实在允许答案的列表中,我们就将其转换并从菜单列表中选取那个项目。”
“是的,我明白了,”埃里克说。“但关于我出错的那第二个菜单怎么办?它不允许我选择数字4,因为它不在我的if语句中。用你的列表,我也会有同样的问题。我有五种风味,所以我需要在这里再有一个答案列表,对吧?”
“好问题!”西蒙说。“我正准备自己问这个问题。而且你是对的。每个菜单列表都应该有自己的允许答案列表。这不是一个大问题;当我们知道我们的菜单列表中有什么时,我们可以构建一个。让我先写出来,然后我会一步一步地解释。”西蒙在他之前添加的if块之前添加了以下几行:
. . .
while True:
choice = input(prompt)
allowed_answers = [] ❶
for a in range(1, len(choices)+1): ❷
allowed_answers.append(str(a)) ❸
if choice not in allowed_answers:
answer = choices[int(choice) - 1]
break
else:
print("Enter number 1, 2, or 3!")
answer = ''
. . .
❶ 允许答案的空列表
❷ 计算菜单的长度
❸ 添加允许答案
“首先,我们创建一个空列表用于允许的答案。然后我们使用len()函数测量菜单列表的长度。对于饮料,结果将是三个,对于口味,将是五个。然后我们使用range()函数创建一个从一到菜单长度的数字序列。对于饮料,序列将是 1, 2, 3。对于口味,序列将是 1, 2, 3, 4, 5。你明白我的意思了。注意,在range()函数中,我们不应该使用序列的最后一个元素,而是最后一个元素之后的那个元素,它不包括在序列中。这就是为什么我们必须将菜单长度加一,如下所示:
len(choices)+1
“最后,在for循环中,我们将序列中的每个数字转换为字符串,并将其添加到允许答案列表的末尾。这个函数叫做append()。”
西蒙完成了他的解释,并补充说:“是的,第一次确实有点复杂,但试着阅读一下 Python 代码,你会发现它就像普通的英语一样容易理解。”
“现在我们必须将我们的 1, 2, 3 列表更改为我们刚刚构建的允许答案列表。”西蒙做了这个修改,看了看代码,然后拍了拍自己的额头。“哦,我刚刚注意到!”
“什么?”艾瑞克以为他们已经完成了,但看起来好像还有其他的事情。
“我们还得改变我们的信息,”西蒙说。“因为我们的函数现在可以接受任何长度的菜单列表,我们应该告诉用户像‘输入一个 1 到 6 的数字’或‘从 1 到 12 的数字’,这取决于我们菜单的长度。记得我们是如何获取列表长度的吗?”
“用len()函数?”艾瑞克问。
“当然!”西蒙说,并在函数中做了最后的修改。现在它看起来是这样的:
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
print(title)
print(len(title) * "-")
i = 1
for c in choices:
print(i, c)
i = i + 1
while True:
choice = input(prompt)
allowed_answers = []
for a in range(1, len(choices)+1):
allowed_answers.append(str(a))
if choice in allowed_answers:
answer = choices[int(choice) - 1]
break
else:
print("Enter number from 1 to ", len(choices)) ❶
answer = ''
return answer
❶ len()用于计算选择数量。
轮到你了! 添加allowed_answers列表
在你的函数中添加一个允许的答案列表。测试它是否允许使用不同长度的菜单列表。
“现在我们免受用户错误的影响了!”西蒙说。“试试看它是否工作!”
艾瑞克再次运行程序,输入了42、coffee和weryiuryt587456,但程序并没有像以前那样崩溃。每次,它都会提醒他应该使用一个数字,并且这个数字应该在 1 到 3 或 5 之间,具体取决于菜单。
“这太酷了!它工作得很好,而且没有崩溃!”艾瑞克非常高兴自己创建了一个如此健壮的程序。
“还有一件事,”西蒙说。“这是今天的最后一件事,我保证!”
“好吧,”艾瑞克说。他开始喜欢编程这件事了。他喜欢他的程序现在看起来像是一个真正的程序——而且它真的工作了!即使它还不是在线或移动应用,它也像与商店的聊天一样工作。艾瑞克想象自己正在给最喜欢的咖啡店发短信,订购饮料,然后去取。
西蒙说:“你的程序只允许我输入 1 到 3 或 5 的数字,但如果我们想跳过某些内容怎么办?比如我不想在我的饮料上加任何配料?”
“在我的口味菜单中,我有‘plain’,这意味着‘无味’。我可以在配料中添加相同的选项,”埃里克回答。
“那会行得通,”西蒙说,“但一般来说,在每个菜单中,你应该给用户一个退出菜单的选项。通常,人们会使用类似‘点击 X 退出此菜单’这样的提示。我认为我们也应该把这一点加入到我们的菜单函数中。”
“我们怎么做到的?”埃里克问。他想了一会儿,然后说,“我知道!我们将把‘X’添加到允许答案的列表中!我这样做对吗?”
“绝对!”西蒙很高兴看到他的兄弟取得了如此好的进展。“记住,我们使用了名为append()的函数来向列表中添加项目?我们可以在向allowed_answers添加数字之后立即使用它。”
“让我试试,”埃里克说,并开始输入。
“当然,继续,”西蒙鼓励他的兄弟。“只是确保你在那个for循环之后添加它。最好在它之后添加一个空行;这样你就能看到它不是循环的一部分。”
这里是埃里克菜单函数的新版本:
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
print(title)
print(len(title) * "-")
i = 1
for c in choices:
print(i, c)
i = i + 1
while True:
choice = input(prompt)
allowed_answers = []
for a in range(1, len(choices)+1):
allowed_answers.append(str(a))
allowed_answers.append('X') ❶
if choice in allowed_answers:
answer = choices[int(choice) - 1]
break
else:
print("Enter number from 1 to ", len(choices))
answer = ''
return answer
❶ 'X' 现在是一个允许的答案。
“不错,”西蒙说,“我还会添加小写字母‘x’,因为大多数人会这样输入。现在,如果用户输入‘x’我们应该怎么做?”
“退出菜单循环?”埃里克说。
“对!但我们将要返回给主程序什么?通常,我们返回用户从菜单中做出的选择:咖啡、巧克力或者任何其他东西。如果用户输入‘x’怎么办?我们应该返回什么?”
“没有吗?”埃里克建议。
“是的,我们返回一个空字符串,”西蒙说,“如果用户输入‘x’,你将一个空字符串('')赋值给answer,并以相同的方式返回answer,就像答案在菜单中一样。”
西蒙继续说,“重要的事情:你应该在尝试将其转换为数字之前,但在检查它是否在allowed_answers列表中之后进行这个检查。你看到在哪里放置这个检查了吗?”
“是的,就在这一行之后:if choice in allowed_answers。”
“太好了!继续添加。在这种情况下,你将有一个嵌套的if语句——一个if语句在另一个if语句内部。这是很常见的——有时你会看到三层嵌套的if语句,甚至更多。只要确保你的缩进正确。这就是 Python 告诉计算机在条件为真或假时应该做什么的方式。”
埃里克对他的代码进行了更多的修改,最终得到了这个版本:
列表 4.1 埃里克菜单的最终版本
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
print(title)
print(len(title) * "-")
i = 1
for c in choices:
print(i, c)
i = i + 1
while True:
choice = input(prompt)
allowed_answers = []
for a in range(1, len(choices)+1):
allowed_answers.append(str(a))
allowed_answers.append('X')
allowed_answers.append('x')
if choice in allowed_answers:
if choice == 'X' or choice == 'x':
answer = ''
break
else:
answer = choices[int(choice) - 1]
break
else:
print("Enter number from 1 to ", len(choices))
answer = ''
return answer
轮到你了! 添加退出选项
将'x'选项添加到你的允许答案列表中。然后添加一个嵌套的if语句来检查它。测试它是否工作。如果你在所有菜单中都输入'x'会发生什么?在这种情况下你会得到什么顺序?
他测试了程序,在所有三个菜单中都输入了x,并得到了他预期的结果:
Erik's Menu
-----------
1 chocolate
2 coffee
3 decaf
Choose your item: x
Erik's flavors
--------------
1 caramel
2 vanilla
3 peppermint
4 raspberry
5 plain
Choose your flavor: x
Erik's toppings
---------------
1 chocolate
2 cinnamon
3 caramel
Choose your topping: x
Here is your order:
Main product:
Flavor:
Topping:
Thanks for your order!
“一个空订单!”他说。
“对,正是如此,”西蒙确认。“我喜欢你的程序,”他继续说。“它运行得很好——这是第一点,也是最重要的。它用户友好,并且给用户提供了操作说明——这是第二点。它检查输入,并阻止用户输入错误值——这是第三点。”
“让我们快速回顾一下你今天学到的内容,”西蒙说。“第一件事是什么?”
“首先你又破坏了我的程序,又一次!”艾瑞克回答。这次他并不很生气,因为他知道他和西蒙一起修复了程序。“而且你告诉我,我应该总是考虑我的用户可能会以错误的方式使用程序。”
“对,处理那些不愿意遵循你指示的顽固用户是程序员工作的一部分,”西蒙说。
“然后我们编写了一个菜单循环,检查用户输入的内容,并且不允许他们使用不允许的答案。然后你告诉我关于append()函数,它可以向列表中添加内容。”
艾瑞克继续说,“然后我编写了嵌套的if检查,现在我的代码看起来就像电影中展示的真实程序了。”
西蒙微笑着说,“相信我,电影中展示的程序很少是真实的程序。但你说得对,你的程序变得越来越复杂了。它使用了不同的 Python 运算符,所有这些循环,还有if语句和列表。”
“然后我们在菜单中添加了‘x’选项,现在任何用户都可以得到一个空订单!”艾瑞克咯咯地笑了起来。
“是的,为什么不呢?”西蒙问道。“你不应该强迫你的用户总是下订单。你应该给他们一个取消订单或退出菜单的选项。”
西蒙继续说,“程序现在真的很好了。明天我会让你停止做程序员,成为咖啡店经理。”
“我不是已经是一名经理了吗?”艾瑞克问道。
“是的,你是对的,”西蒙微笑着说。“现在想象一下,你,咖啡店经理,刚刚收到了一种新的咖啡饮品风味成分。你想要把这个风味添加到菜单上。还有几样新的配料。你会怎么做?”
“我会把这些配料添加到配料列表中。没什么大不了的,”艾瑞克回答。
“是的,但如果你只是个经理而不是程序员呢?你对这个程序一无所知,你也不懂 Python,但你想要把那些风味和配料添加到菜单上。作为程序员,你应该给经理提供一个简单的方法来添加菜单上的内容。”
“你建议我怎么做?”艾瑞克问道。他已经知道西蒙心里有主意了。
“我认为我们应该把菜单放在文件里,从那些文件中读取你的列表。”
“就像在 Word 文档中一样吗?”艾瑞克问道。
“是的,几乎是这样,”西蒙说。“你的程序将打开这些文件并从中读取。我认为最好每个菜单有一个文件。一个文件将包含所有的饮品,另一个文件包含所有的风味,还有一个文件包含所有的配料。然后你的经理可以直接编辑这些文件,而不是编辑你的 Python 代码。听起来不错吗?”
“是的,很有趣,”埃里克说。他想知道他的 Python 程序会如何以与 Word 相同的方式打开文件。
“太好了,”西蒙说,“那就是我们明天要做的。现在先休息一下。”
您今天学到的新知识
-
用户会犯错—您已经了解到用户并不总是遵循您在程序中给出的指示。您必须为此做好准备,并检查他们的输入是否存在错误、错误的类型等问题。
-
缩进—当您在 Python 中创建一个块(例如
while)时,您必须确保块中的所有代码都是缩进的,这意味着它向右移动。 -
如何退出菜单—您必须为用户提供一种从每个菜单退出的方式。例如,如果他们不想订购配料,他们可能想要跳过那个菜单。
本章的代码
您可以在此处找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch04。
5 与文件一起工作:成为一名商店经理
在本章中
-
Erik 学习咖啡店经理需要什么
-
Erik 从文件中读取他的菜单
-
Erik 编写他的第二个 Python 函数
“上次,你提到咖啡店经理的事情,”Erik 第二天开始和哥哥说话。“是关于更改菜单的吗?我忘了。”
“是的,我说过如果咖啡店经理可以在不进入 Python 代码的情况下更改菜单,那可能是个好主意,”Simon 说。
“是的,好主意,”Erik 说。“不是每个人都懂 Python。”
“我的想法是为每个菜单创建简单的文本文件——可以叫它们drinks.txt、flavors.txt和toppings.txt。然后你的程序可以从这些文件中读取并创建列表。”
“你为什么给它们都加上.txt结尾?”Erik 问。“它们不应该用.docx结尾,这样经理就可以在 Microsoft Word 中编辑它们吗?”
“好主意,”Simon 说。“是的,经理可能更熟悉 Word,但我们需要一个纯文本文件,没有字体、标题或目录。它和你的 Python 代码类似——这些文件应该只有纯文本行,经理应该使用纯文本编辑器来处理它们。当我用.txt结尾命名它们时,我告诉操作系统——无论是 Windows、macOS 还是 Linux——这个文件应该用纯文本编辑器打开,而不是像 Word 这样的文字处理器。在这些系统中,总是有一个文本编辑器可以编辑这些文件。你也可以安装另一个应用程序来做到这一点,就像我们安装 Mu 编辑器一样。此外,Python 从纯文本文件中读取比从.docx文件中读取要容易得多。
“让我们创建这些文件,”Simon 继续说。“你可以用你的 Mu 编辑器来做这件事。只是别忘了在保存文件时添加.txt扩展名。否则,它会自动添加.py。
“创建一个新文件,将你的饮料分别输入到单独的行中,并将其保存为drinks.txt。然后为口味和配料做同样的事情。”
Erik 开始工作。几分钟之后,他有了三个文件:
列表 5.1 drinks.txt
coffee
chocolate
decaf
列表 5.2 flavors.txt
caramel
vanilla
peppermint
raspberry
plain
列表 5.3 toppings.txt
chocolate
cinnamon
caramel
轮到你了! 创建你自己的菜单文件
按照 Erik 刚刚做的样子创建文本文件,确保它们是纯文本文件。尝试在你的菜单中使用你最喜欢的口味和配料。
“现在怎么办?”他问他的哥哥。
“现在让我们学习如何在 Python 中处理文件。为此,我们将创建一个新的程序。我们会练习一下,然后把这个代码添加到我们的主程序中。我通常在学习新东西时这样做——我在添加到主应用程序之前,先在一个独立的简单程序中尝试它。”
Simon 继续说,“在编辑器中创建另一个文件,并将其保存为files.py。”
Erik 已经做过好几次了,所以这只需要几秒钟。
“处理文件是一个难题,所以我将从一张图开始,”Simon 说。

“当你用电脑中的文件工作时,你使用它的名字。你告诉你的编辑程序打开一个名为drinks.txt的文件。然后你的编辑程序会读取文件并显示其内容。然后你编辑文件并保存它,这意味着你在电脑磁盘上写入文件。到目前为止,一切顺利吗?”西蒙问道。
“是的,”埃里克说,“但是磁盘只用于非常老的电脑。在我的电脑里,它被称为固态硬盘——它不再是磁盘了。我的朋友亚历克斯告诉我的。”
“你完全正确!”西蒙很高兴听到他的兄弟这么说。“是的,现在大多数电脑里都是固态硬盘——它不是磁盘,也不是在旋转的。”
“但是让我们回到我的文件上来,”埃里克说,“我看到你在你的图片顶部写了‘电脑’和‘Python’。那是什么意思?”
“这意味着,”西蒙说,“在 Python 中,如果你想处理一个文件,你必须创建一个特殊的对象,这通常被称为文件句柄。你使用这个对象来读取和写入文件。你使用一个名为open()的函数来创建这样的对象。你调用open()函数,并将文件名作为参数传递。在我们的例子中,它将是open("drinks.txt")。该函数返回文件句柄,你将其放入变量中。在这个例子中,变量名为f,但你可以在这里使用任何名字。”
“为什么这么复杂?”埃里克问道,“为什么我们不能直接使用文件名?”
“是的,第一次看起来确实有点复杂。原因在于文件名只是一个字符串,记得吗?当我们想要从文件中读取时,我们想要读取的是具有该名称的文件,而不是字符串。文件名和文件本身是不同的事物。当我们使用open()函数时,我们在文件名和文件本身之间建立了一个连接。我们告诉 Python,‘请找到电脑里名为drinks.txt的文件,并使用它作为文件。’现在不用太担心这个。有时候,理解某件事物的最好方式就是开始使用它。”
“好吧,”埃里克说。他对这一切仍然有些困惑,但他想看看他是如何从刚才创建的文本文件中读取菜单的。
“现在让我们写一个简单的 Python 程序来处理文件,”西蒙说,“去你编辑器里打开files.py程序的标签页。看看我的图示:你必须调用open()函数;传递文件名,比如drinks.txt;并将结果存储在变量f中。你能写出来吗?”

“让我试试,”埃里克说,然后他写了这样一段:
f = open("drinks.txt")
“好!”西蒙说,“现在你有一个名为 f 的文件对象,你可以从中读取内容。要这样做,你需要调用一个名为 read() 的 方法。方法和函数看起来和行为有很多相似之处,但方法应用于 对象。我们稍后会讨论对象,但你现在只需要知道,要调用一个对象的方法,你使用对象的名字,然后放一个点,然后是方法名和括号,就像调用函数一样,就像这样:f.read()。方法也可以返回一些东西,就像函数一样,所以你必须将结果存储在某个地方。你认为当你调用这个 read() 方法时,它会返回什么?”
“我想知道文件里有什么,”艾瑞克回答。
“绝对正确!”西蒙说,“将它保存在一个名为 drinks 的变量中,然后尝试打印它。”
艾瑞克写下了以下内容:
f = open("drinks.txt")
drinks = f.read()
print(drinks)
“现在运行它,”西蒙建议。
艾瑞克点击运行并得到了以下输出:
coffee
chocolate
decaf
>>>
你的回合! 从文件中读取
编写相同的简短程序并尝试从 drinks.txt 文件中读取。确保你可以打印出整个文件的内容。
“它工作了!”他非常高兴。他的 Python 程序打开了一个文件,读取了它,并在页面上打印了它!“现在我知道如何从 Python 打印我的菜单了。让我为其他两个文件也写一个。”
“对,”西蒙说,“但这不是我们想要的。”
“为什么?”艾瑞克无法理解。
“记住,在你的程序中,你不仅打印出你的菜单。你还让用户从菜单中选择,然后你找到列表中的那个项目,对吧?”
“是的,但这里不是列表吗?”艾瑞克问,“它看起来像列表。”
“它看起来像列表,但不是列表。它是一个字符串,”西蒙说,“当你调用 read() 方法时,你将整个文件的内容复制到一个名为 drinks 的变量中。所以这个变量只是一个大字符串。如果你不相信我,你可以在输出窗口这里测试它。你看到输出窗口里的这三个尖括号吗?你可以在这里输入任何 Python 命令来继续使用你的程序——就像我们使用 REPL 时一样,记得吗?在这里输入 type(drinks),你就会看到这个变量的类型。”
艾瑞克这样做并看到了以下内容:
>>> type(drinks)
<class 'str'>
>>>
“你看,Python 说它是一个字符串,”西蒙说,“而我们需要一个列表。”
“我们该怎么办?”艾瑞克问。
“幸运的是,Python 开发者知道我们可能需要这个,并为文件对象创建了一个新的方法。它被称为 readlines()。试着将你的 read() 改为 readlines(),看看会发生什么。别忘了在再次运行程序之前点击停止。”
艾瑞克更改了他的程序如下:
f = open("drinks.txt")
drinks = f.readlines()
print(drinks)
他点击了停止,然后再次运行,得到了以下输出:
['coffee\n', 'chocolate\n', 'decaf\n']
>>>
“试着再次检查它的类型,”西蒙建议。
艾瑞克切换到输出窗口并再次输入 type(drinks):
>>> type(drinks)
<class 'list'>
>>>
你的回合! 检查 Python 类型
重复艾瑞克刚才做的检查。你看到字符串和列表之间的区别了吗?
“它是一个列表!”他说,“但那些 slash-n 字符是什么?我的饮料文件里没有这些。”
“你在文件中看不到它们,但它们确实存在。这些都是不可见的字符。当你看到这个反斜杠-n时,它是一个称为换行符的单个字符。它告诉计算机应该在下一行的开头打印下一个项目。没有它,你所有的饮料都会像这样打印出来:coffeechocolatedecaf。你不想这样,对吧?”西蒙微笑着。
“当然不是!”艾瑞克说,“但我们不需要它们在列表中,对吧?我认为菜单列表应该看起来像我主程序里的那样,对吧?”
“你完全正确。而且,再次强调,Python 开发者为这个创建了非常实用的方法。它被称为strip(),它从字符串的两端移除不可见字符。我们只需要将其应用于列表中的每个项目。你认为我们应该在这里使用什么?”
“一个循环?”艾瑞克建议。
“对,一个循环!”西蒙说,“我们将遍历列表,并使用strip()方法移除那些换行符。”
西蒙停顿了一下,思考着。然后他继续说,“有几种方法可以做到。有些更短,但更难理解。让我们使用更容易阅读和跟随的方法。实际上,这是编程中的一个好规则:在比较不同的实现方式时,总是选择更容易阅读和跟随的那一个。如果有人正在阅读你的代码,他们会为此感谢你。即使是你自己——如果你三个月后再次阅读自己的代码。”
他迅速画了一个图表。

“让我们从文件中读取到一个临时列表。然后我们将遍历那个临时列表,转换每个项目,并将其追加到新列表中。我们将把这个新列表称为drinks。然后我们将对风味和配料重复相同的步骤。我们可以为它们使用相同的临时变量。让我来帮你,”西蒙说。他开始在艾瑞克的程序中输入。完成之后,它看起来是这样的:
f = open("drinks.txt")
temp = f.readlines()
drinks = []
for item in temp:
new_item = item.strip()
drinks.append(new_item)
print(drinks)
他点击运行,他们看到了结果:
['coffee', 'chocolate', 'decaf']
>>>
轮到你了! 移除换行符
使用strip()方法从菜单项中移除换行符。
“看起来好多了,不是吗?”他问艾瑞克,“现在去为其他的菜单文件做同样的事情。”
艾瑞克开始编写他的程序,当他快完成flavors.txt文件时,他惊呼,“等等!我在重复自己了!你告诉我不应该重复自己。”
西蒙微笑。他很高兴他的小弟弟这么快就掌握了这个概念。“我们怎样才能不重复自己?”他问。
“写一个函数?”艾瑞克说。
“是的,完全正确!看看这段代码:哪些是相同的,哪些是变化的?哪些将成为参数,这个函数将要返回什么?”
艾瑞克开始大声思考,“我打开不同的文件,所以文件名应该是一个参数,对吧?”
“正确,”西蒙确认道,“你想要返回什么?”
“我想我会返回一个包含菜单选项的列表——当然,在移除那些换行符之后。但我应该把这个列表叫什么呢?”
“你可以叫它任何你想要的名字,因为它从外面是不可见的。例如,你可以叫它 result。然后你可以在函数的末尾写 return result。当你调用函数时,那个 result 列表会被 分配 到主程序中的变量,比如 drinks 或 flavors,这取决于你正在读取的文件。在函数内部,变量将始终被称为 result,但在 外部 函数,在主程序中,你可以将结果分配给任何变量。”

“继续写那个函数,”西蒙鼓励他的兄弟。“记得怎么做吗?从 def 开始,给函数起一个像 read_menu 这样的名字,将 filename 作为参数传递,并复制我们刚刚写的代码。”
“好吧,我会试试,”艾瑞克说。他开始编写他的第二个 Python 函数。
西蒙稍微帮助了他的兄弟,他们一起写了以下内容:
列表 5.4 files.py
def read_menu(filename):
f = open(filename)
temp = f.readlines()
result = []
for item in temp:
new_item = item.strip()
result.append(new_item)
return result
drinks = read_menu("drinks.txt")
print(drinks)
flavors = read_menu("flavors.txt")
print(flavors)
toppings = read_menu("toppings.txt")
print(toppings)
艾瑞克保存了文件并点击了运行。当然,他得到了预期的结果:
['coffee', 'chocolate', 'decaf']
['caramel', 'vanilla', 'peppermint', 'raspberry', 'plain']
['chocolate', 'cinnamon', 'caramel']
>>>
轮到你了! 创建一个 read_menu 函数
按照艾瑞克的方式创建一个 read_menu 函数。确保你没有在文件名中打错字,但如果打错了呢?尝试更改文件名并看看 Python 会给出什么错误。别忘了修复文件名,让你的程序再次工作。
“这是我的第二个函数,而且它工作了!”他自豪地对他的兄弟说。
“是的,你已经在构建自己的函数库了——太棒了!” 西蒙说。“现在让我们把你的新函数复制到主程序中。不要复制 print() 行——我们只是用它们来测试。我认为那个文件叫做 menu_function.py,对吧?”
“对,”艾瑞克说。“但我在那个文件中应该把函数放在哪里?开始还是结束?”
“Python 中的规则是,你应该在开始使用它之前定义你的函数。正因为如此,函数通常放在文件的开始部分,在主程序之前。你可以在你的第一个函数之后放置它。”
“好吧,”艾瑞克说。他开始工作,这是他得到的结果:
列表 5.5 艾瑞克的带文件菜单
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
. . .
# this function didn't change
. . .
return answer
def read_menu(filename):
f = open(filename)
temp = f.readlines()
result = []
for item in temp:
new_item = item.strip()
result.append(new_item)
return result
drinks = read_menu("drinks.txt")
flavors = read_menu("flavors.txt")
toppings = read_menu("toppings.txt")
drink = menu(drinks)
flavor = menu(flavors, "Erik's flavors", "Choose your flavor: ")
topping = menu(toppings, "Erik's toppings", "Choose your topping: ")
print("Here is your order: ")
print("Main product: ", drink)
print("Flavor: ", flavor)
print("Topping: ", topping)
print("Thanks for your order!")
轮到你了! 将你的函数复制到主程序中
将新的 read_menu() 函数复制到你的主程序中并尝试运行。
他测试了程序,它工作得和之前一样。
“这很好,”西蒙说。“现在尝试向配料文件中添加一些内容。看看它是否改变了菜单。”
艾瑞克打开了 toppings.txt 文件,在文件末尾添加了‘香草粉’,并保存了文件。他再次运行了程序,最后菜单中多了一行:4 香草粉。
轮到你了! 添加另一个项目
向其中一个菜单添加一个新项目。更改其中一个项目。别忘了更改菜单文件后保存它们。检查程序是否打印了更新的菜单。
“这很好,我喜欢!”埃里克说。“现在任何能编辑文本文件的人都可以更改菜单!等等……,”他有了个主意。“所以我可以把这些菜单里的任何东西都放进去!冰淇淋、三明治,或者任何东西!酷,我喜欢!我应该告诉我朋友亚历克斯这件事——他喜欢乐高迷你人偶。也许他可以用这个程序和朋友交换人偶!”
“完全正确!”西蒙说。“我很高兴你对如何使用你的程序有很多想法。太好了!我也有一些想法,但我们最好明天再讨论。我们明天还得创建主菜单。”
“你的意思是什么?”
“嗯,目前每次你想下单时,你都得启动你的程序。你下单,打印出来,然后程序就结束了。如果程序能返回到询问顾客名字的初始对话会更好。”
“是的,没错,”埃里克同意。“它应该像自动售货机一样,你下单,按‘完成’,然后它会回到‘欢迎光临我们的商店’的第一个屏幕。是的,让我们这么做!”
“让我们回顾一下今天的进展,”西蒙建议。“我们今天做了什么?”
“首先,你说咖啡店经理会想要编辑我们的菜单文件。然后我写了三个文件,分别用于饮料、口味和配料菜单。”
“非常好,接下来是什么?”
“然后我打开了文件并从中读取。我一开始是逐行读取,但后来出现了那些奇怪的‘反斜杠-n’字符。然后我们使用了strip()方法来移除它们。”
“好,”西蒙说。“你还记得我跟你说的关于对象的事情吗?”
“其实并没有。你说文件在 Python 中是一个对象,它和它的名字并不相同。你还说带有对象的函数被称为‘方法。’”
“是的,没错,”西蒙说。“对象很复杂。现在我们只使用它们和它们的方法,但以后我们会了解更多。我们也会创建自己的对象和方法,就像我们处理函数一样。”
“没错,”埃里克说。“你提醒了我——我写了我的第二个 Python 函数,而且它工作了!”
“确实如此。你现在正成为一个严肃的程序员,”西蒙说,然后微笑。“现在我们先休息一下。明天,我们会让你的程序变得更好。”
你今天学到的新东西
-
打开文件的意义——你学到了程序内文件名和文件句柄之间的区别。
-
\n是什么以及如何从字符串中移除它——你学到了\n符号代表“开始新行”。在你的菜单项中你不需要它,所以你使用了strip()函数来移除它。 -
从函数返回的内容被分配到主程序中的变量——你学到了函数内部的变量在主程序中是不可见的。为了传递它的值,你必须从函数中返回那个变量,并将其值分配给主程序中的另一个变量。
本章的代码
您可以在此处找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch05.
6 主菜单:下一位顾客!
在本章中
-
艾瑞克创建了一个主菜单来服务众多顾客
-
艾瑞克学习了 Python 字典
-
西蒙解释了自顶向下的开发方法
“昨天,我们决定要创建一个主菜单,”西蒙提醒艾瑞克。
“是的,你说如果我想用这个程序服务众多顾客,我就必须为每个顾客重复菜单——询问他们的名字和想要点的东西。”
“完全正确!”西蒙说。“你打算用什么来做这件事?”
“一个循环,也许?就像我们处理菜单一样。重复直到顾客输入正确的数字或输入x。”
“你完全正确!”西蒙非常高兴他的兄弟这么快就明白了这个编程想法。“我们将询问顾客的名字,就像我们第一个程序一样。然后我们将获取他们的订单,包括所有口味和配料。”
“是的,”艾瑞克继续说,“然后我们会给他们选择下单或取消的选项?我看到一些网站上都有这个。”
“对。当顾客点击下单时,我们保存订单并打印给咖啡师。如果他们取消,我们就忽略它。在两种情况下,我们都回到主菜单的开始处,询问下一个顾客的名字。”
西蒙拿了一张纸,开始画图。“我们首先应该可视化地规划这个算法。当我们都同意它应该如何表现时,你就可以开始编写代码了。在开始编写任何代码之前,用平实的语言和图表描述程序总是一个好习惯。”
“我们在这里做的第一件事是获取订单,”西蒙说。“你看,我把这个设置为‘获取订单’。”

“我们的所有菜单和口味、配料在哪里?为什么你没有把它们放进去?”艾瑞克问道。
“我决定使用一个名为‘获取订单’的块,它包含所有菜单。首先考虑大块,然后分别处理每个块的细节是很常见的。这也是程序员使用函数的另一个原因。他们首先在大块中思考程序,然后分别用单独的图表描述每个块。想象一下,如果我们把程序的所有细节都包含在一个图表中,那将是不可能理解的算法!”
“让我们继续,”西蒙说。“我们获取订单,并要求顾客确认。如果他们取消,我们就回到第一个菜单:‘欢迎光临艾瑞克的咖啡店’。但如果顾客确认订单,那么我们应该保存并打印它。”
“好的,我明白了,我们应该打印出来准备饮料,”艾瑞克说。“但为什么我们要保存它?你说的保存订单是什么意思?”
“首先,在一天结束时检查你在咖啡店为多少朋友服务,你不觉得这很好吗?”
“但我知道了,”艾瑞克说。“那天我准备了五种饮料。”
“但我们谈论的是一个真正的咖啡店,别忘了。他们每天都在工作,为成百上千的顾客服务。我的几个朋友在不同的咖啡店工作过,我可以向你保证,他们非常清楚每天为多少顾客服务。
“另一个原因是,”西蒙继续说,“咖啡店经理应该知道店里有什么,以及他们需要为库存订购什么。记得我们讨论过更改菜单吗?假设他们没有及时订购焦糖口味,不得不从菜单中删除它。为什么他们没有订购它?因为他们没有计算顾客订购了多少份焦糖口味。如果我们保存所有订单并分析它们,我们就可以在我们没有足够库存之前订购口味和配料。”
“我没有想过这一点,”埃里克说,“是的,保存订单是个好主意。但我们怎么做到呢?”
“有几种方法,”西蒙说,“我们可以使用文件,或者我们可以使用数据库。当然,所有严肃的应用程序都使用数据库。我认为我们应该从文件开始,然后,如果你足够勇敢,我们也可以使用数据库。”
“是的,我想我的程序像那些严肃的应用程序一样!”埃里克说,“我还想尝试数据库!”
“好,”西蒙说,“但现在让我们先完成主菜单。我们很快就会保存订单。说到订单,”西蒙说着又拿了一张纸,“这就是‘获取订单’块里面的内容。

你的机会来了! 创建你自己的图表
如果你决定经营不同类型的商店,为你的get_order()函数创建一个图表。
“我们已经编写了这个函数。我们只是没有称它为函数。你看,我们已经在这些对话中创建,以询问顾客的名字、饮料、口味和配料。我们还没有做的事情只有一件。你看到了吗?”
“返回订单?”埃里克问道,“我们没有做,但我不确定这里是什么意思。”
“这就是你的订单看起来像的,同意吗?”

“我们使用get_order()函数收集所有这些信息,但不是返回四个单独的值,如名字、饮料、口味和配料,我想返回一个单一的东西,我会称之为订单。而这个单一的东西包含几个作为整体一起的值。”
“我知道,你在这里想用列表!”埃里克分享了他的洞察。
“这是一个选项,但我心里想的是更好的东西。在 Python 中,我们有字典。什么是正常的字典?”
“这是一本包含单词及其含义的书,”埃里克回答,“或者如果是一本英德词典,就是翻译。”
“没错!”西蒙说,“你有一个词,以及与这个词相关的值。这可能是一个词及其含义或翻译,就像这样,”然后他画了一个图表。

“在 Python 中,字典使用我们称之为键的单词来获取与它们相关的值。让我们看看你的订单。你有一个名为name的键,其值是Erik。你还有一个名为drink的键,其值是decaf,等等。整个字典被称为order,这就是我们将从这个函数中返回的结果。

“让我们再次用 REPL 练习,”西蒙建议。“点击 REPL 进入交互会话。”
艾瑞克点击了 REPL 图标,切换到了包含交互会话的窗口。艾瑞克的看起来是这样的,但你的版本可能看起来略有不同——这是正常的:
Jupyter QtConsole 4.7.7
Python 3.8.12 (default, Mar 12 2022, 19:58:23)
Type 'copyright', 'credits' or 'license' for more information
IPython 8.1.1 -- An enhanced Interactive Python. Type '?' for help.
In [1]:
“我们将从创建一个名为order的空字典开始。在 Python 中创建字典时,我们使用花括号{},使它们看起来与使用方括号[]的列表不同。输入order = {}然后按 Enter。这将是你订单的内容。”
艾瑞克输入了:
In [1]: order = {}
In [2]:
“现在我们可以向你的订单中添加项目。让我们从名字开始。输入order['name'] = 'Erik'。然后尝试使用简单的print()函数打印订单。”
艾瑞克输入了:
In [2]: order['name'] = 'Erik'
In [3]: print(order)
{'name': 'Erik'}
In [4]:
“但你说过字典应该使用花括号。为什么这里我们使用方括号?”艾瑞克问。
“好问题。”西蒙对这个 Python 特性太熟悉了,以至于他找不到一个很好的方法立即解释。他开始说:“嗯,我们使用花括号来创建字典。但当我们想要从字典中获取一个项目时,我们使用方括号来访问它。在这方面,它与列表相似——当你想要从列表中获取一个项目时,你使用方括号。区别在于,对于列表,你使用索引,它们是整数。对于字典,你使用键,通常是字符串。如果你有一个普通的字典,并且使用它的索引——比如 546——来获取一个单词的意义或翻译,那会不方便,不是吗?相反,你使用单词本身,比如‘dog’,找到它会很快。”
“是的,没错,”艾瑞克说。“我现在应该添加饮料、口味和配料吗?”
“好主意,继续!”西蒙说。
艾瑞克在他的交互会话中继续:
In [4]: order['drink'] = 'decaf'
In [5]: order['flavor'] = 'vanilla'
In [6]: order['topping'] = 'chocolate'
In [7]: print(order)
{'name': 'Erik', 'drink': 'decaf', 'flavor': 'vanilla',
'topping': 'chocolate'}
In [8]:
“注意这里,”西蒙说,“你的键和值总是以冒号:分隔成对出现。”
轮到你了! 使用 REPL 学习字典
打开 REPL 并处理字典。你可以重复艾瑞克的命令或创建你自己的字典。尝试使用不同的键。尝试存储一个数字而不是一个字符串。它工作吗?
“这很酷,我喜欢!”艾瑞克说。“但我能以更好的方式打印它,就像我之前打印的那样吗?”
“当然,”西蒙说。“我认为你应该为这个写一个新的函数。但我们最好还是回到编辑器。”
“那将是我的第三个函数,”艾瑞克说。
“你还在计数吗?”西蒙微笑着说。“我相信你很快就会忘记你写了多少个函数。”
“现在让我们在你的编辑器中开始编写主菜单程序,”西蒙继续说。“我们将在这里使用自顶向下的设计方法。”
“那是什么?”艾瑞克问。
“就像我刚才给你展示的那样:首先,我们为整个程序开发算法。我们决定大块是什么以及我们将如何从一个转到另一个。这通常包括像是否让用户确认或取消订单这样的决定。”
“我们可以开发主程序并使用像get_order()或print_order()这样的函数。即使我们还没有这些函数也无关紧要。在我们编写真正的函数之前,我们可以编写非常简单的函数,比如只打印一条消息,比如‘我是一个 print_order()函数。’有些人把它们称为占位符。然后,当我们看到主菜单工作得很好并且调用了正确的函数时,我们可以编写真正的函数。”
“让我来帮你。”西蒙拿起键盘,“首先,我们将创建一个新文件并将其保存为main_menu.py。然后我们将使用def关键字和括号创建一个名为main_menu()的新函数。”
“另一个函数?”埃里克问道。
“是的,在编程中我们通常为每件事创建函数。主程序通常非常短,它调用那些函数之一。然后那个函数调用其他函数,依此类推。将主菜单也写成函数是一个好习惯。”
西蒙继续说,“再看看图表。你看到那些指向‘开始’的箭头了吗?它们通常意味着在你的算法中你将重复某些操作。一旦我们回到‘开始’,我们就会一遍又一遍地通过相同的算法。要在程序中重复某些操作,我们使用……什么?”

“一个循环!”埃里克回答。
“完全正确!”西蒙确认,“我们已经使用了两种类型的循环:for循环和while循环。你打算在这里使用哪一种?”
“我认为应该是一个while循环,”埃里克说,“这个主菜单看起来和我们在饮料菜单中做的一样:重复问题并检查用户的回答。”
“我同意,”西蒙说,“看看你把菜单作为函数写的代码。记住,我们那里用了while True:,并且检查了用户输入的内容。你认为我们应该在这里检查什么?给你一个提示:在图表中,我们不得不做决定的地方通常画成菱形形状。”

“我看到了!”埃里克说,“这就是我们询问用户是否要确认或取消订单的地方。”
“好的,让我们开始写它,”西蒙建议。
埃里克写道:
def main_menu():
while True:
“接下来是什么?”他问道。
“看看图表,”西蒙说。
“获取订单?”埃里克说。
“对!记住,get_order()函数将返回一个包含订单的字典。这个字典将包含顾客的名字、饮料、口味等等。我们将把这个字典放入主菜单中的order变量。”西蒙在埃里克的代码中添加了一行:
def main_menu():
while True:
order = get_order()
“接下来是什么?”他问他的兄弟。
“现在我们必须询问顾客是否确认订单,”埃里克说。
“很好。但我认为在询问之前我们必须显示订单,”西蒙又添加了几行代码:
def main_menu():
while True:
order = get_order()
print("Check your order:")
print_order(order)
confirm = input("Confirm? Press Y to confirm, N to cancel: ")
“看这里,”西蒙对埃里克说,“我使用了从get_order()接收到的order变量作为下一个函数print_order()的参数。这在编程中非常常见:我们调用一个函数来做某事,它返回一个结果,然后我们使用那个结果作为另一个函数的输入。”

“我明白了,”埃里克说。“就像在电影院里:收银员打印一张票并给你。然后你拿着票,去入口,把票给入口处的检票员,他们会检查。”
“是的,很好的类比,埃里克!让我们继续:我们刚刚收到了关于客户是否想要确认订单的答案。现在,正如你刚才说的,我们必须检查这个答案并决定下一步做什么。就像在电影院里,他们会检查你的票是否正确,并决定是否让你进去。让我们添加这些行。看看这个图。如果用户回答‘是’,我们应该做什么?”

“保存并打印订单,”埃里克看着西蒙的草图回答。
“好的,如果用户想要取消并回答‘不’呢?”
“我们应该什么都不做,只是返回到开始处。但我不知道怎么做到。你那里只有一个箭头。”
“Python 中有一个简单的词来表示这个箭头——continue。它的意思是,‘不要执行循环的其余部分,并从循环的开始处继续。’很简单,对吧?”西蒙补充了这些行到函数中:
def main_menu():
while True:
order = get_order()
print("Check your order:")
print_order(order)
confirm = input("Confirm? Press Y to confirm, N to cancel: ")
if confirm == "Y" or confirm == "y":
save_order(order)
print("Thanks for your order:")
print_order(order)
else:
continue
“我注意到你添加了两个额外的函数:save_order()和print_order(),”埃里克说。“但我们还没有写它们。”
“让我们写它们!”西蒙兴奋地说。“我们现在将写非常简单的函数。它们将只打印类似‘保存订单...’的东西,这样我们就可以看到它们被调用了。稍后,我们会改进它们,让它们做更有用的事情。”
西蒙在main_menu()函数下方添加了以下函数:
def get_order():
return {}
def print_order(order):
print(order)
return
def save_order(order):
print("Saving order...")
return
他向埃里克解释说,“get_order()函数就是你已经写好的。我们很快就会把你的代码转到这里,但现在它什么也不做。没有菜单和对话框,但它必须返回订单。记住,订单将是一个包含像‘name’和‘drink’这样的键的字典,但现在函数只返回一个空字典,即一对大括号。到目前为止,一切顺利?”
“是的,”埃里克回答道。“那么你的意思是,我们将从那个旧文件中复制我的先前函数到这个文件中,对吗?”
“没错,”西蒙说。“有时候当程序变得更大时,将函数分组到单独的文件中是个好主意。但在这个案例中,把所有东西都放在一个文件里更容易。
“print_order()函数,”西蒙继续说,“只是打印它从参数中获取的订单。在这种情况下,我们将使用标准的 Python print()函数,但稍后我们会让它更漂亮。你已经做到了,记得吗?”
“当然!”埃里克说。“我认为我们可以让它看起来像一张真正的咖啡店收据。”
“好主意,”西蒙说。“save_order()函数除了打印‘保存订单...’之外什么都不做。现在还过得去——我们稍后会写它。”
“现在我们准备好调用main_menu()函数并测试我们的算法了。请将main_menu()的调用添加到末尾并运行它。你的主程序将只包含这个函数调用。”
埃里克添加了函数调用,所以整个程序现在看起来是这样的:
列表 6.1 带有简单函数的主菜单
def main_menu():
while True:
order = get_order()
print("Check your order:")
print_order(order)
confirm = input("Confirm? Press Y to confirm, N to cancel: ")
if confirm == "Y" or confirm == "y":
save_order(order)
print("Thanks for your order:")
print_order(order)
else:
continue
def get_order():
return {}
def print_order(order):
print(order)
return
def save_order(order):
print("Saving order...")
return
main_menu()
轮到你了! 创建一个主菜单
“编写一个类似于埃里克刚刚创建的主菜单函数。你可以随意更改对话框消息。”
他点击了运行,看到了这个输出:
Check your order:
{}
Confirm? Press Y to confirm, N to cancel:
他输入了y并得到了这个输出:
Saving order...
Thanks for your order:
{}
Check your order:
{}
Confirm? Press Y to confirm, N to cancel: y
“为什么它又给我‘检查你的订单’?”埃里克问道。
“因为这是一个循环!”西蒙说。“在你回答确认问题y之后,它返回到循环的开始。因为我们还没有添加你的菜单对话框,所以它打印了一个空的订单。一切按预期工作。现在尝试回答问题n。”
埃里克输入了n并得到了这个输出:
Confirm? Press Y to confirm, N to cancel: n
Check your order:
{}
Confirm? Press Y to confirm, N to cancel:
“你看到区别了吗?”西蒙问道。
“我看到这次没有打印‘保存订单...’,这意味着它遵循了你图示右侧的短箭头。”

“太棒了!”西蒙很高兴看到埃里克真的理解了他的算法图。
“我认为我们今天取得了很好的进展——我们的主菜单正在工作。明天我们将编写实际执行我们想要的功能的函数。现在,让我们总结一下今天所学的内容。”
“我们又使用了while循环!”埃里克说。
“对!而且你在制作菜单时使用了你所学的知识,”西蒙确认说。
“此外,我们还学习了字典。它们就像普通的字典一样,但你可以存储任何东西,而不仅仅是单词描述。”
“是的,正是这样!在我们的简单order字典中,我们保存名称、饮料和口味。但在一个更复杂的字典中,你可以保存数字——例如价格——甚至列表和其他字典。字典在 Python 中非常有用,你将经常使用它们。”
“此外,我也喜欢你创建简单函数来测试主菜单的方式,”埃里克说。“你说这是‘自顶向下’的,对吗?”
“正是这样,”西蒙说。“还有另一种‘自底向上的’方法,正如你所猜测的。在这种情况下,人们首先创建函数,正确测试它们,然后将它们组合成一个大的程序。在某种程度上,我们在你创建和测试你的第一个menu()函数时也使用了这种方法。现在我们将使用你的函数在我们的大型程序中。”
“现在是休息的时候了,”西蒙继续说,“明天,我们将处理我们放在主菜单中的函数。”
今天你学到的新知识
-
自顶向下的方法——首先,你开发你应用程序的“大图景”,并使用仅打印一些东西的简单函数,而不是做实际的工作。当主算法正常工作时,你开发实际的函数。
-
字典—在 Python 中,字典可以存储键值对的组合。您可以为键分配值,并且可以通过它们的键快速找到它们。
-
流程图符号—程序员通常在开始编写代码之前使用图表来讨论他们的算法。通常,一个矩形表示某个过程,而菱形形状表示一个带有是/否问题的决策点。还有表示输入、使用文档、使用数据库等符号。我们将在后面介绍它们。
本章代码
您可以在此处找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch06.
7 创建函数:获取订单并打印
在本章
-
Erik 创建了获取和打印订单的实际函数
-
Erik 使用字典存储和打印客户的订单
-
Erik 的程序现在按计划工作!
-
Erik 和 Simon 计划编写一个保存订单的函数
“昨天,我们创建了主菜单,对吧?”Simon 开始与 Erik 交谈。“我们甚至测试了主菜单的功能。”
“是的,但它没有做任何有用的事情,”Erik 说。
“对!”Simon 说。“记住,我们讨论了‘自顶向下’的方法?我们创建了空函数只是为了测试主菜单。现在是我们让它们做些实际的事情的时候了。打开你创建菜单的 Python 文件。它叫做menu_files.py。”
Erik 打开了那个文件,现在他的编辑器中有两个标签:一个带有main_menu.py文件,另一个带有menu_files.py文件。
“切换到main_menu.py文件,让我们看看我们必须在那里编写哪些函数,”Simon 说。“有三个函数:get_order()、print_order()和save_order()。让我们从get_order()开始。我们的图在哪里?”

“让我们从get_name开始。这里我们做什么?”Simon 问。
“我们只需问‘你的名字是什么?’,”Erik 回答。
“对,然后呢?”
“然后我们把它保存在一个变量里,比如name。”
“几乎对了,”Simon 说。“记住,我们决定order将是一个字典。我们将把与该订单相关的所有内容都保存在这个字典中。例如,要保存客户的名字,而不是name = ‘Erik’,我们应该写order['name'] = 'Erik'。只是,我们将使用input()函数,就像你在你的第一个程序中做的那样。”
“让我试试,”Erik 说。他写下了这个函数:
def get_order():
order['name'] = input("What's your name: ")
return {}
“现在试试,”Simon 说。
Erik 点击了运行。程序询问他的名字,他输入了‘Erik’。但随后他收到了几行错误信息:
What's your name: Erik
Traceback (most recent call last):
File "/home/erik/mu_code/main_menu.py", line 41, in <module>
main_menu()
File "/home/erik/mu_code/main_menu.py", line 3, in main_menu
order = get_order()
File "/home/erik/mu_code/main_menu.py", line 27, in get_order
order['name'] = input("What's your name: ")
NameError: name 'order' is not defined
>>>
“那是什么?”他问 Simon。
“看,Python 会告诉你问题出现在哪里。读最后一行。”
“名称‘order’未定义,”Erik 读到。
“很简单,”Simon 解释道。“你试图在字典中放入一些东西,但你还没有创建它。这很容易解决。让我们创建一个空字典。记住,我们用花括号来表示。就在input()那一行之前写上order = {}。”
Erik 将他的函数改为这个:
def get_order():
order = {}
order['name'] = input("What's your name: ")
return {}
他再次运行了它,这次没有出现任何错误:
What's your name: Erik
Check your order:
{}
Confirm? Press Y to confirm, N to cancel: y
Saving order...
Thanks for your order:
{}
What's your name:
“现在好多了,”他说。
“是的,更好了,但看:它仍然打印出一个空的订单。你创建了一个订单,甚至输入了你的名字,但你的函数返回了一个空字典。看到这一行:return {}?”
“但那正是你写的!”Erik 确信这不是他的错。
“是的,我这样写是为了测试主菜单功能。但现在我们必须返回实际的order字典。改为return order,然后看看它是否会打印出你的名字。”
Erik 改变了函数:
def get_order():
order = {}
order['name'] = input("What's your name: ")
return order
然后,他再次运行了它。这次它打印出了他的名字:
What's your name: Erik
Check your order:
{'name': 'Erik'}
Confirm? Press Y to confirm, N to cancel:
“是的,现在它打印出我的名字了!”
“恭喜你!”西蒙说,“现在你知道如何使用字典了。”
你的机会! 创建一个get_order()函数
在main_menu.py文件中开始编写你自己的get_order()函数。添加第一个输入以获取客户的名字。通过运行主菜单程序来测试它。
你有什么选择?
“让我们继续,”西蒙继续说,“现在我们必须添加你的menu()函数来列出饮料和口味。但我们还需要read_menu()函数来从文件中读取你的菜单。从menu_files.py文件中复制menu()和read_menu(),然后在这里main_menu.py中粘贴它们。在def get_order():行之前粘贴它们。”
“如果我在这行之后粘贴它们会怎样?”埃里克想知道为什么他的哥哥给了他如此严格的命令。
“那么它就不会工作了。”西蒙给出了一个简单的答案,然后微笑。“好吧,如果你真的想知道,我们将在get_order()函数中使用这两个函数。首先,我们必须从文件中读取菜单内容:你的饮料、口味和配料。然后我们调用menu()函数三次以获取客户的选择。在我们可以使用这些函数之前,我们应该定义它们。”
“换句话说,我们应该告诉 Python 这些函数存在以及它们做什么。这就是为什么它们需要粘贴在def get_order():行之前。顺便说一句,这就是我们为什么用def这个词来开始一个函数——我们定义它。”
“好吧,”埃里克说。他开始复制函数。几秒钟后,他的main_menu.py文件看起来是这样的:
列表 7.1 主菜单已准备就绪
def main_menu():
while True:
order = get_order()
print("Check your order:")
print_order(order)
confirm = input("Confirm? Press Y to confirm, N to cancel: ")
if confirm == "Y" or confirm == "y":
save_order(order)
print("Thanks for your order:")
print_order(order)
else:
continue
def menu(choices, title="Erik's Menu", prompt="Choose your item: "):
print(title)
print(len(title) * "-")
i = 1
for c in choices:
print(i, c)
i = i + 1
while True:
choice = input(prompt)
allowed_answers = []
for a in range(1, len(choices) + 1):
allowed_answers.append(str(a))
allowed_answers.append("X")
allowed_answers.append("x")
if choice in allowed_answers:
if choice == "X" or choice == "x":
answer = ""
break
else:
answer = choices[int(choice) - 1]
break
else:
print("Enter number from 1 to ", len(choices))
answer = ""
return answer
def read_menu(filename):
f = open(filename)
temp = f.readlines()
result = []
for item in temp:
new_item = item.strip()
result.append(new_item)
return result
# . . .
# No changes in get_order() and save_order() functions
# . . .
main_menu()
“正确!”西蒙说,“现在你知道规则了:在使用之前定义它。当你没有定义order字典时,你已经看到了你得到的错误。”
“现在我们可以使用这些函数在get_order()中,”他继续说,“看看你的menu_files.py程序。我们首先做了什么?”
“我们从文件中读取菜单,”埃里克回答。
“好的。我们在这里做,但是要在函数内部。”
埃里克向get_order()函数中添加了三行:
def get_order():
order = {}
order["name"] = input("What's your name: ")
drinks = read_menu("drinks.txt")
flavors = read_menu("flavors.txt")
toppings = read_menu("toppings.txt")
return order
他必须在每一行前面添加四个空格,这样它们就都缩进到同一级别。
“现在用三个menu()函数也是一样吗?”他问西蒙。
“当然,继续!”
埃里克更改了他的函数,变成了这样:
def get_order():
order = {}
order["name"] = input("What's your name: ")
drinks = read_menu("drinks.txt")
flavors = read_menu("flavors.txt")
toppings = read_menu("toppings.txt")
drink = menu(drinks, "Erik's drinks", "Choose your drink: ")
flavor = menu(flavors, "Erik's flavors", "Choose your flavor: ")
topping = menu(toppings, "Erik's toppings", "Choose your topping: ")
return order
他为自己的工作感到自豪,看着西蒙。
“几乎正确,”西蒙说,“你复制得对,但你需要稍微修改一下代码,以便将答案存储在order字典中。这应该是一个简单的修改——你知道如何做。”
“啊,我明白了,”埃里克说。他修改了函数。现在看起来是这样的:
列表 7.2 main_menu.py
def get_order():
order = {}
order["name"] = input("What's your name: ")
drinks = read_menu("drinks.txt")
flavors = read_menu("flavors.txt")
toppings = read_menu("toppings.txt")
order["drink"] = menu(drinks, "Erik's drinks",
"Choose your drink: ")
order["flavor"] = menu(flavors, "Erik's flavors",
"Choose your flavor: ")
order["topping"] = menu(toppings, "Erik's toppings",
"Choose your topping: ")
return order
西蒙鼓励他,“继续,运行它!”
埃里克运行了程序。
What's your name: Erik
Erik's drinks
-------------
1 coffee
2 chocolate
3 decaf
Choose your drink: 1
Erik's flavors
--------------
1 caramel
2 vanilla
3 peppermint
4 raspberry
5 plain
Choose your flavor: 2
Erik's toppings
---------------
1 chocolate
2 cinnamon
3 caramel
4 vanilla powder
Choose your topping: 3
Check your order:
{'name': 'Erik', 'drink': 'coffee', 'flavor': 'vanilla',
'topping': 'caramel'}
Confirm? Press Y to confirm, N to cancel: y
Saving order...
Thanks for your order:
{'name': 'Erik', 'drink': 'coffee', 'flavor': 'vanilla',
'topping': 'caramel'}
What's your name:
“哇!”他真的很高兴。“我写了一个超过 70 行的程序,而且它运行得很好!”
“是的,你做到了——它真的工作,”西蒙确认并微笑。
你的机会! 将菜单选择添加到你的程序中
在main_menu.py文件中添加与前面程序中相同的menu()和read_menu()函数。通过运行程序并输入你的选择来测试程序。试着输入错误的选择,并确保menu()函数不允许你这样做。
打印它!
“但是还缺了点什么。这个订单看起来不够专业。它不像真正的咖啡店,”Simon 继续说。
“我明白了。我们应该修改print_order()函数,对吧?”Erik 建议。
“是的,没错。去你的main_menu.py文件中的print_order()函数。”
Erik 的print_order()函数看起来是这样的:
def print_order(order):
print(order)
return
“这里我们使用 Python 提供的默认打印函数,”Simon 继续说。“Python可以打印你的字典,但它看起来并不漂亮。对于调试来说是可以的,但对于真正的订单和收据,我们必须让它更美观。你已经做到了,对吧?”
“你是说,当我打印了一行破折号时?是的,它比这个更漂亮。”
“让我们做类似于你在menu_files.py文件末尾所做的事情。你可以从那里复制以print开头的这些行。只要记住保持正确的缩进,并确保你使用的是字典而不是简单的变量。而且别忘了我们现在有了顾客的名字。我认为你应该在你的函数中使用它。准备好了吗?”
“是的,”Erik 回答。他开始编写函数。最后他得到了这个:
def print_order(order):
print("Here is your order, ", order["name"])
print("Main product: ", order["drink"])
print("Flavor: ", order["flavor"])
print("Topping: ", order["topping"])
print("Thanks for your order!")
return
你的机会! 添加一个print_order()函数
在main_menu.py文件中添加一个print_order()函数。你可以自由地使用装饰,比如破折号(-)、下划线(_)或等号(=),让你的打印订单看起来像你在其他地方看到的那样。试着找到餐厅、咖啡店或冰淇淋店的收据,看看你能不能让你的看起来相似。
他再次运行了程序,得到了更漂亮的输出:
Here is your order, Erik
Main product: coffee
Flavor: vanilla
Topping: caramel
Thanks for your order!
Confirm? Press Y to confirm, N to cancel:
“是的,这好多了!”Simon 说,“你可以添加装饰,比如破折号和垂直线——由你决定。但你作为一个程序员做得很好。你写了几个非常有用的函数,你把它们组织得很好,并且测试了它们。做得好,Erik。我真的很为你感到骄傲!”
“此外,我们学习了字典,并且我使用了它们。”Erik 感觉到总结时间到了,他应该提到当天学到和使用的所有内容。
“是的,没错,”Simon 确认。“字典在 Python 中非常重要。我们在程序中经常使用它们。以后,你会学到更多关于它们的知识。”
“你说我们还需要以某种方式保存我们的订单。我们明天会做吗?”Erik 问。
“是的,当然,”Simon 说。“你知道 JSON 吗?”他问。
“Jason?是的,我们数学课在一起。他怎么样?”
“不,不是那个 Jason,”Simon 笑着。“JSON 是一种文件格式,我们可以用它来保存你的咖啡店订单。我们明天再谈,好吗?”
“好的,”Erik 说。然后他就出发了。
今天你学到的新东西
- 变量和函数定义—在 Python 中,在使用变量和函数之前,我们必须定义它们。对于变量,这就像给变量分配一个空值一样简单。对于一个字典,它是
order = {},而对于一个字符串,它是name = ""。函数应该使用def关键字来定义。
本章代码
您可以在此处找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch07.
8 使用 JSON:保存订单
在本章
-
埃里克学习了 JSON 格式和文件
-
埃里克学习了 Python 模块
-
埃里克创建了一个订单列表
-
西蒙和埃里克编写了一个保存订单到 JSON 文件中的函数
“你昨天提到了 Jason 的事,”埃里克问西蒙。“但你说是另一个 Jason,不是来自我数学课的那个。”
“是的,那是另一个 Jason,”西蒙微笑着说。“它是 JSON,J-S-O-N,我们用来存储数据的文件格式。”
“像我们以前用来存储菜单的文件吗?”
“是的,类似,”西蒙回答。“但这个格式非常适合存储结构化数据。”
“那是什么?”埃里克问。
“有时你只想存储一段文本或一张图片。它们通常没有固定的结构。文本只是文本。图片可以是大的或小的,可以是黑白或彩色,但它没有任何结构——它只是一堆像素。这就是我们所说的非结构化数据。”
“但在你的情况下,每个订单都有一个结构。每个订单都有顾客的姓名和你要准备的饮料的所有组成部分——不多也不少。它总是有主要饮料、口味和配料。因为你有写的菜单,顾客在你可以打印或保存订单之前必须回答所有这些问题。此外,顾客不能添加任何东西到订单中。”
“像另一个配料吗?”埃里克问。
“是的,”西蒙说。“你的订单是结构化数据的一个例子。”
埃里克没有想过他会创造出这样一个严肃的名字。
“你的订单存储在一个字典中,”西蒙继续说,“你肯定知道每个订单都有一个字典键(还记得那些是什么吗?)叫做 name、drink、flavor 和 topping。”西蒙拿出了一张他的图表。

“JSON 格式是为了存储这种类型的有结构化数据而创建的。让我们先练习一下。像以前一样,我们首先创建一个简单的程序,然后尝试一些简单的操作。然后我们将在这个简单程序中学到的知识应用到我们的主程序中,使其能够将订单保存到文件中。今天要做的事情很多,所以我们可能明天会做其中的一些。”
西蒙继续说,“现在打开你的编辑器,创建一个新文件。例如,将其保存为 dict_json.py。”
埃里克打开了编辑器窗口,点击了新建。然后他点击保存,输入 dict_json.py,并再次点击保存。他已经熟悉了这个过程。
“现在,”西蒙说,“创建一个示例订单。”
“什么是示例订单?”
“你的订单是一个字典,对吗?”西蒙开始解释。“在你的主程序中,你创建了一个空字典,然后开始用从客户那里得到的价值填充它。这里,我们想跳过这一步,想象一下我们的 order 字典已经填满了客户的选择。让我为你开始,”西蒙说。他在埃里克的编辑器中输入:
order = {
"name": "Erik",
“你可以继续,”他说。“别忘了关闭花括号。”
埃里克完成了order字典并关闭了花括号。现在它看起来是这样的:
order = {
"name": "Erik",
"drink": "coffee",
"flavor": "caramel",
"topping": "chocolate"
}
“我注意到你在字典中缩进了行,”他对他的兄弟说,“这是 Python 中字典的规则吗?”
“不,”西蒙回答,“在这种情况下,我只是为了让它看起来更好。并且让它更易于阅读。我可以把所有项目放在一行中,或者从行的开头开始,但我认为这样看起来更好。”
“现在,”他继续说,“我们有一个字典。我们想把它保存到一个文件中。我想我应该提醒你一下关于字典的文件操作。”
“是的,”埃里克说,“那是很久以前的事了。我不记得太多。”
“当然,”西蒙说,“你还将学习一些关于文件的新知识。首先,我们必须打开一个文件。要打开一个文件,我们需要调用一个名为open()的函数——当然,并且将文件名作为参数传递。你关于函数及其参数的一切都知道,对吧?”
“open()函数返回一个文件句柄。它是一个特殊对象,我们的程序可以使用它来处理该文件。”
西蒙在埃里克创建的字典下方又写了一行:
order = {
"name": "Erik",
"drink": "coffee",
"flavor": "caramel",
"topping": "chocolate"
}
f = open("orders.json", "w")
“这是第一个新东西。看到最后一行的这个w了吗?这意味着我们将要向文件写入。”
“但当我们打开菜单文件时,我们没有使用任何字母,”埃里克记得。
“你说得对!”西蒙说,“我们没有使用任何字母——顺便说一下,它们被称为模式——因为当我们不使用任何字母时,Python 会打开文件进行读取。这次,我们想向这个文件写入,所以我们需要告诉 Python。”
“我看到你把文件命名为‘orders.json’。是因为你想使用你之前提到的那个 JSON 格式吗?”
“是的,正是这样,”西蒙回答,“这不是强制性的,但将.json扩展名添加到 JSON 文件中是一种约定。与‘写入’模式的不同之处在于,如果该文件不存在,Python 将创建一个具有该名称的文件。”
“下一步是什么?”埃里克问,“我们如何写入这个 JSON 文件?上次我们使用了‘方法’——它们是这样称呼的吗?”
“是的,你记得正确,”西蒙说,“但这次我们会做不同的事情。这一切都是因为我们将要写入结构化数据,而不仅仅是纯文本。我们将使用一个名为json的 Python 模块。”
“模块是什么?”埃里克立刻问。
“我现在就解释给你听,”西蒙说。他微笑了。“记住,你最近写了几个 Python 函数。例如,从文件中读取菜单项并返回列表的那个函数。想象一下,你的一个朋友想为咖啡店或类似的东西编写自己的程序。”
“是的,”埃里克说,“我最近和艾米丽谈过,她说她想要为冰淇淋店创建一个程序。”
“太好了!”西蒙说。“你可能想帮助她并分享你编写的函数。这将节省她一些时间,所以她的程序可以更早准备好。在程序员中,分享他们的工作以帮助他人是非常常见的。在 Python 中,你可以将你想要分享的函数组合到一个文件中,并将其交给艾米丽。她可以将该文件复制到她的电脑上,然后将其导入到她的程序中。在她导入之后,她可以在她的应用程序中使用你的函数。包含函数的文件被称为模块。”
“如果我不想分享我的函数怎么办?”埃里克问道。“我花了好几天时间编写它们!”
“是的,你做到了,”西蒙说。“而且你做得很好。但记住,很多人花了很多天时间在 Python 中编写其他函数,甚至 Python 本身。他们与其他程序员分享了他们的工作,所以你可以完全免费使用 Python 和其他函数。这样,我们互相帮助完成我们的项目。如果我们不得不从头开始编写一切,那会慢得多。这就是为什么人们使用别人的代码,并与他人分享他们的代码。这通常被称为开源社区。”
“回到 JSON,”西蒙继续说。“我们将使用其他人编写的名为 json 的模块。这个模块可以读取 Python 字典并将它们转换为 JSON 文件。在你的文件开头添加一行:import json。它应该是文件的第一行。”
埃里克照做了。以下是他的更新后的文件:
import json
order = {
"name": "Erik",
"drink": "coffee",
"flavor": "caramel",
"topping": "chocolate"
}
f = open("orders.json", "w")
“现在我们必须将你的示例订单转换为 JSON 并将其写入我们刚刚打开的文件,”西蒙说。“在 json 模块中,这个函数叫做 dump。我们将在你的程序中调用它,但我们必须告诉 Python 它应该在 json 模块中查找这个函数,所以我们这样调用它:json.dump()。你只需要传递两个参数:字典和文件对象。将这个函数添加到文件的末尾。你的字典是 order,你的文件对象是 f。”
埃里克将这一行添加到程序的末尾:
json.dump(order, f)
西蒙继续说,“还有一件事是我们之前没有用文件做过的。我们应该关闭文件。这很重要,所以让我们看看另一个图表。”
“这里有计算机的三个主要组件:处理器、内存和驱动器。你的 Python 程序正在处理器上运行。你的文件存储在驱动器上的文件系统中。文件系统是你可以在 Mac 上的 Finder 中看到的。在 Windows 的资源管理器中,它是文件夹和文件。当你想在 Python 中处理文件时,你打开它,就像你已经做的那样。这会在你的程序中创建一个文件对象。当你向文件写入时,你是在向计算机内存写入。然后,当你想让文件真正写入驱动器上的文件系统时,你需要关闭它。”

“这太复杂了!”埃里克感到困惑。“我们为什么不能直接写入驱动器?”
“电脑确实很复杂,你是对的!”西蒙同意道。“原因在于计算机工程师试图让电脑运行得更快。写入驱动器很慢——比写入内存慢得多。想象一下你在文本编辑器中编写程序。如果它立即将你输入的每个字母保存到驱动器上,那将会非常慢。你不喜欢使用慢电脑,对吧?”
“我讨厌慢电脑!”埃里克回答道。
“为了让电脑运行更快,”西蒙继续说,“工程师们决定将数据存储在内存中,并在必要时将其保存到驱动器上。他们使用了很多技巧来使电脑运行更快,当然,我并不了解所有这些。也许我会在大学里学到更多关于它们的知识。
“让我们回到我们的程序并关闭文件。记住,文件对象使用方法。在这种情况下,我们调用f.close()。这将确保我们的订单被写入文件。在你的程序最后一行之后添加它。”
在这样一番长篇大论之后,这很简单。埃里克迅速添加了它,并得到了这个结果:
import json
order = {
"name": "Erik",
"drink": "coffee",
"flavor": "caramel",
"topping": "chocolate"
}
f = open("orders.json", "w")
json.dump(order, f)
f.close()
“现在运行它,”西蒙说。
埃里克点击运行,看到窗口底部的熟悉的>>>。他问西蒙:“现在怎么办?”
“没有发生什么?”西蒙微笑着。他感受到了埃里克的困惑。“那是因为你没有告诉 Python 打印任何东西。但即便如此,幕后还是发生了一些事情。Python 打开了一个名为orders.json的文件,将你的订单写入其中,然后关闭了它。现在我们必须打开它来检查是否正确写入。使用纯文本编辑器打开文件。你是在 Mac 上,所以它将是来自你的应用程序文件夹的 TextEdit。在 Windows 上是记事本,在 Linux 上是 gedit 或 Kate。启动编辑器并打开文件。它在你的主文件夹中,位于mu_code下,文件名为orders.json。”
埃里克启动 TextEdit,找到了文件,并打开了它。确实,他看到了他的订单。

你的回合! 将你的示例订单保存到 JSON 文件中
编写埃里克刚才编写的程序。尝试创建一个稍微不同的示例订单。运行程序,并使用文本编辑器检查生成的 JSON 文件。尝试创建不同的订单并再次运行程序。你的 JSON 文件是否已更改?(你可能需要在文本编辑器中重新加载文件。)
“你看到了吗?”西蒙问道。“这是你存储在文件中的示例订单。让我添加一些内容,你就会明白为什么 JSON 文件非常适合存储 Python 字典。”
西蒙拿起键盘,将json.dump()调用改为如下:
json.dump(order, f, indent=4)
他再次运行了程序,重新打开了orders.json文件。这看起来对埃里克来说很熟悉。

你的回合! 让它变得美观
在你之前的程序中添加indent=4参数,再次运行程序,并检查你的 JSON 文件是否已更改。
“它看起来就像我的字典!”埃里克惊呼。
“我告诉过你,”西蒙说。“我们将使用 JSON 来存储你的订单。我说的是 ‘orders’,这意味着现在我们必须学习如何保存多个订单并将它们存储在文件中。我们已经知道 Python 如何按顺序存储多个项目——你用那个为你的菜单做了这件事。”
“一个列表!”埃里克说。
“正确!Python 中的列表可以包含不同类型的东西:字符串、数字,甚至是字典。在这种情况下,我们将有一个字典列表。每个字典将包含一个订单,我们将逐个将它们添加到列表中——一个新客户、一个新订单、一个新字典。我将画一个图来展示。

“让我们创建一个订单列表,”西蒙继续说。“在代码中复制现有的订单,并将其命名为 order1,例如。
“然后更改订单的内容:名称、饮料和其他选项。”
埃里克花了一些时间编写代码,并在第一个订单下面创建了以下附加订单:
order1 = {
"name": "Alex",
"drink": "choco",
"flavor": "vanilla",
"topping": "caramel"
}
“很好,”西蒙说。“现在创建一个名为 orders 的空列表。注意,它是复数形式——orders。这和创建一个空字典非常相似——你已经做过这件事了——只是用方括号代替花括号。”
埃里克在第二个订单下面添加了以下行:
orders = []
“现在我们将两个订单都添加到列表中,”西蒙说。“信不信由你,你刚才创建的 orders 列表也是一个对象。在 Python 中,一切都是对象,每个对象都有你可以使用的方法。你只需要知道每个对象有哪些方法。例如,对于所有列表,都有一个名为 append() 的方法。它将你传递给它的元素添加到列表的末尾。
“看,我会用它将 order 和 order1 添加到 orders 列表中。”西蒙在创建 orders 列表的那一行下面添加了这两行:
orders.append(order)
orders.append(order1)
“但你怎么知道这里应该使用 append()?”埃里克问道。
“我在学习 Python 时在 Python 文档中读到过,”西蒙微笑着回答。“你可以使用 Google 搜索‘Python 列表方法’,你将找到你需要知道的一切。
“现在我们可以尝试将这个列表保存为 JSON 格式,”西蒙继续说。“我们需要更改的唯一一件事是我们要将什么‘写入’到文件中。我们使用 order 作为 dump() 函数的参数来写入一个订单。现在让我们将其替换为 orders(复数!)并看看会发生什么变化。”
埃里克将 json.dump() 的那一行更改如下:
json.dump(orders, f, indent=4)
他运行了程序,再次打开了 orders.json 文件。

轮到你了! 保存一个列表
添加另一个示例订单,并将其命名为 order2。创建一个订单列表,并将列表保存到同一个 JSON 文件中。使用文本编辑器检查结果。尽可能添加更多订单并将它们写入文件。你能保存多少订单没有限制?
“那么,你觉得怎么样?”西蒙问道。“看起来像你的订单吗?”
“是的,这完全像 Python!”埃里克说。“但为什么我们要将我的订单写在单独的文件中?如果看起来像 Python,为什么我们不直接将我的订单写入我们的 Python 程序中?”
“好问题!”西蒙非常高兴埃里克想要理解这些事情。“首先,我们总是希望将程序与数据分开。记住,当你运行你的 Word 应用程序时,你不会将你的文档写入 Word 程序。你将它们保存在单独的文件中。这正是我们在这里所做的。你的程序可以将订单保存在不同的文件中,例如,为不同的日子。你唯一需要做的就是更改输出文件的名称,例如 orders.Monday.json、orders.Tuesday.json 等等。”
“第二个原因是,这种格式之所以被称为 JSON,是有原因的。它代表 JavaScript 对象表示法。它是由使用 JavaScript 编程语言的人发明的,然后其他语言开始使用它。所以你可以用 Python 在 JSON 文件中编写你的指令,然后你的某些朋友可以在 JavaScript 中创建另一个程序,从该文件中读取你的指令,例如在网页上打印出来。”
“是的,我听说我班上的一些人说他们知道 JavaScript,”埃里克说。
“太好了!你可能想要创建一个编程团队并一起工作,”西蒙说。
“但是让我们继续我们的程序。现在我们将从 JSON 文件中读取订单并将它们保存到一个新的列表中。让我们称它为 saved_orders。”
“既然我们已经把它写到了文件里,为什么还要读取它?”埃里克感到困惑。
“也许我没有解释得恰当,”西蒙回答道。“在这个程序中,我们正在练习一些与 JSON 文件相关的操作,这样我们就能很好地了解它们,并在我们的主程序中使用它们。程序员经常这样做:他们创建简单的程序来测试概念和想法。让我向你展示我们主程序的计划,这样你就能更好地理解我们的目标。”
西蒙又拿了一张纸,开始画图。
“首先,我们检查名为 orders.json 的文件是否存在。如果存在,我们就打开它并从中读取我们之前的订单。”
“为什么我们需要我们之前的订单?我们已经准备好了,”埃里克问道。
“是的,但记住,我们可能想要统计今天、昨天或上个月我们服务了多少客户,或者统计我们使用了多少份焦糖,并决定是否需要购买更多。如果你想要认真管理你的咖啡店业务,你需要所有这些订单。这就是为什么所有企业都会长时间保留这些记录。”
“如果我们没有这个文件怎么办?”埃里克问道。
“这意味着我们刚刚开业并开始工作,”西蒙说。“在这种情况下,我们将创建一个空列表并开始接收订单。当我们打开它进行写入时,文件会自动创建。”

“看看这个图表:这里,我们有我们的 orders 列表,要么是装满了我们前几天的订单,要么是空的。当我们开始接收订单时,我们将它们保存到这个列表中。我们一天的工作结束后,我们关闭文件,这样就在驱动器上保存了所有我们的订单。第二天,我们再次打开文件并继续接收订单。所有新的订单都将添加到前一天的订单中。”

你的机会! 绘制你自己的图表
尝试在不查看书籍的情况下为你的程序绘制图表。绘制图表将帮助你理解程序是如何工作的。
“这是真正的咖啡店工作方式吗?就像 星巴克 一样?”埃里克问。
“是的,差不多吧,”西蒙说。“当然,他们使用数据库来确保可靠性和安全性。他们的订单记录也比我们的更复杂。但整个过程非常相似。”
“现在你已经知道了宏伟的计划,让我们继续我们的简单程序并从文件中读取。我们将读取之前的订单到名为 saved_orders 的新列表中,然后我们将其打印出来以查看是否正确读取。为此,json 模块中有一个名为 load() 的函数。它的工作方式与 dump() 相同:首先,我们打开一个文件,但这次是为了读取而不是写入。然后我们调用 json.load() 并将文件对象作为参数传递。该函数 返回 从文件中读取的对象,我们将该对象赋值给一个变量。在我们的例子中,它将是一个订单列表,这些订单是字典,正如你将记得的那样。”
“听起来很复杂吗?让我来帮你。在 Python 中要短得多。”西蒙开始向埃里克的代码中添加行。这是他在程序末尾添加的内容:
f = open("orders.json", "r")
saved_orders = json.load(f)
print(saved_orders)
他点击了运行,埃里克看到了输出。

你的机会! 从 JSON 文件中读取
将前面的行添加到你的程序中,并尝试从你创建的 JSON 文件中读取。你得到的是与示例订单相同的订单吗?
“你今天学到了很多东西,”西蒙说。“我们休息一下,直到明天。然后我们将这些函数添加到我们的主程序中,它将变成一个真正的咖啡店应用程序。你能快速回顾一下我们今天做了什么吗?”
“我们从我的 Python 字典创建了一个 JSON 文件,它看起来非常像 Python。然后你解释了所有关于文件、内存和驱动器的内容。我们还创建了一个字典列表并将其保存在文件中。”
“我们还学习了关于 Python 模块以及如何导入它们的内容,”西蒙补充说。“到目前为止,一切顺利,”他说。“我们明天继续。我们离完成你的应用程序的第一个版本非常近了。”
今天你学到的新东西
-
JSON (JavaScript Object Notation)——一种用于存储结构化数据并可用于在程序之间交换信息的格式。
-
Python 模块——一组可以被其他程序员使用的 Python 函数。通常,它们被分组在文件中。在使用它们之前,你必须
导入模块。 -
字典列表—列表可以包含不同类型:字符串、数字、字典,甚至其他列表。
-
文件操作—你可以打开文件进行读取和写入。你可以将数据写入文件,但数据是写入计算机内存的。你应该关闭文件以将数据保存到计算机驱动器上。
-
开源社区—分享他们编写的程序并互相帮助编写更好代码的人。
本章的代码
你可以在这里找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch08。
9 完成菜单:一个真正的程序
在本章中
-
埃里克和西蒙在主程序中创建了加载和保存函数
-
西蒙为主菜单和
get_order()函数添加退出函数 -
西蒙解释为什么他认为埃里克刚刚创建了一个真正的程序
-
兄弟俩讨论未来的计划
“现在让我们来点实际的,”西蒙说。“昨天,我们玩了一些样本订单和简单的程序。今天,是时候在你的实际程序中使用你学到的知识了。”
“是的,让我们这么做!”埃里克说。
“打开你写的main_menu.py文件,那里是主菜单。我们应该在那里添加一些函数来处理 JSON 文件。让我们先回顾一下我们首先需要做什么。”西蒙拿出了昨天的图表。

“我们必须编写一个函数,该函数将从 JSON 文件中加载订单列表。但首先,它必须检查该文件是否存在。如果文件不存在,我们将创建一个空列表并从该函数返回它。如果文件存在,我们将从中读取,将 JSON 文件转换为 Python 列表,并返回该列表。
“让我帮你。”西蒙觉得这可能对埃里克来说有点困惑。“通常,在处理文件的函数中,我们传递文件名作为参数。”西蒙在文件的底部开始编写函数,就在带有main_menu()的最后一行之前:
def load_orders(filename):
“现在我们必须检查文件是否存在。有一个特殊的功能来做这件事,我们可以在os模块中找到它。”
“‘os’是什么?”埃里克问。
“OS 代表‘操作系统’。计算机中的操作系统管理所有文件和程序。它与你的屏幕、键盘、扬声器和你的摄像头一起工作。在典型的计算机上,操作系统可以是 Windows、macOS 或 Linux。
“我们将向操作系统询问是否在这个计算机上存在一个具有该名称的文件。”西蒙添加了一行:
def load_orders(filename):
if os.path.exists(filename):
“看,我们在这里使用了os模块。这意味着我们必须以与你在昨天的样本程序中导入json模块相同的方式导入它。在这个程序中,我们还没有导入json或os模块,所以让我们都导入它们。”
西蒙将光标移到文件的起始位置,并添加了两行:
import os
import json
他将光标返回到文件末尾的load_orders()函数,并继续解释。“如果文件存在,我们将其打开以供读取。然后我们使用json.load()函数从文件读取到orders列表,并返回该列表。”他向该函数添加了三行:
def load_orders(filename):
if os.path.exists(filename):
f = open(filename, "r")
orders = json.load(f)
return orders
“如果它不存在,我们只需创建一个空列表并返回它:”
def load_orders(filename):
if os.path.exists(filename):
f = open(filename, "r")
orders = json.load(f)
return orders
else:
orders = []
return orders
“现在加载函数已经准备好了!”他说,并看着埃里克。
“我认为我自己写不出来,”埃里克说。
“当然,第一次做的时候看起来很复杂。但是看,你可以把它当作普通英语来读,不是吗?”
艾瑞克再次查看函数,并试图阅读它。“如果包含filename的文件存在,那么就打开文件。将其保存在名为f的对象中。然后从那个f文件中加载之前的订单到orders中。嗯,是的,我可以读取它了。”他感到惊讶。他现在可以阅读 Python 并理解它了!
“下一个函数比较简单,”西蒙继续说。“如果你看看你练习样本订单的文件,我想你可以自己写出来。看,在两个append()操作之后,有三行是我们需要的。我们已经有了一个名为save_order()的函数,它除了打印“保存订单……”之外什么都不做。让我们用真正的函数替换它。我认为它应该被称为save_orders()——复数形式,因为我们现在知道如何将订单列表保存到 JSON 文件中,对吧?”
西蒙写出了函数的开始部分:
def save_orders(orders, filename):
他解释说:“我们将订单列表作为第一个参数传递。然后我们传递我们想要存储它的文件名。现在你可以添加昨天程序中的这三行。”
艾瑞克查看了dict_json.py文件,并从中复制了三行。现在函数看起来是这样的:
def save_orders(orders, filename):
f = open(filename, "w")
json.dump(orders, f, indent=4)
return
“我们现在可以测试它了吗?”他问西蒙。
“我们几乎准备好了,”西蒙回答。“看看我们的文件底部——我们只是调用main_menu()函数。这就是在这个函数中我们要做的事情,”他迅速画了一张图。

“这就是我们在main_menu()函数中要做的。我们只需要稍作编辑,以便为多个客户服务并将他们的订单保存到orders列表中。我们必须将这个列表传递到main_menu()函数中,那里有一个地方可以保存输入的订单。”
他将光标移到文件的开头,并将orders参数添加到main_menu()定义中:
def main_menu(orders):
“现在,”他继续说,“每次客户下新的订单时,它将被添加到orders列表中。在我们将列表作为参数添加之前,main_menu()不知道在哪里添加新的订单。现在我们可以使用append()方法,在客户确认订单后立即将其添加到orders中。
“我们不会在这里使用save_order()函数。当您关闭程序时,我们将保存所有订单。”西蒙将main_menu()函数更改为如下:
def main_menu(orders):
while True:
order = get_order()
print("Check your order:")
print_order(order)
confirm = input("Confirm? Press Y to confirm, N to cancel: ")
if confirm == "Y" or confirm == "y":
orders.append(order) ❶
print("Thanks for your order:")
print_order(order)
else:
continue
❶ 将新订单添加到列表中
“我们还将更改主程序以执行三个步骤:加载之前的订单,获取新订单(主菜单),并保存所有订单。”西蒙在文件底部添加了这三行,看起来是这样的:
orders = load_orders("orders.json")
main_menu(orders)
save_orders(orders, "orders.json")
“我现在可以试一下吗?”艾瑞克问。
“当然,开始吧!”西蒙说。
艾瑞克运行了程序。他在第一个提示符下输入了他的名字,然后选择了他的饮料成分。当程序要求他确认订单时,他输入了Y。程序返回到“你的名字是什么:”提示符。
“好的,”艾瑞克说,“它工作了。但我怎么检查我的订单?它们是否保存在文件中?”
“让我们检查一下,”西蒙说。
兄弟俩打开orders.json文件,惊讶地发现它仍然包含昨天实验中的旧订单——甚至西蒙也感到困惑。
“让我们看看,”他说。“我们打开文件,我们从它那里读取,我们获取订单……但我们从未写入文件,因为我们还在主菜单接收订单!我们从未达到那个save_orders()函数!嗯,让我想想怎么修复它。”
埃里克微笑了。他那个无所不知的哥哥不知道该怎么办。但这种情况只持续了一瞬间。
西蒙说,“我明白了。我们没有给用户提供退出主菜单的方式。我们不断询问用户的姓名,但如果我们想结束程序怎么办?”
“我看到你按下 Control-C 想要停止我的程序,”埃里克说。
“是的,我确实这样做了,但这不是结束程序的正常方式。当我这样做时,程序被中断了。通常,当你这样做时,Python 会给你一个错误信息。当程序被中断时,它不会做任何其他事情——它只是停止在它所在的地方。它不会将我们的命令写入文件,也不会关闭文件。按 Control-C 是一个结束程序的不良方式。”
西蒙停顿了一下,然后继续说,“我们应该给用户提供一个正常的结束程序的方式。”
“就像 Word 菜单中的退出一样?”
“是的,就像这样。让我们告诉用户,如果他们想要退出,他们应该在询问他们名字时输入X,并且只输入X。我们有一个客户真实全名是X的概率非常低——实际上几乎为零。所以让我们这样做:如果客户在get_order()函数中输入名字X,我们不会询问任何其他问题,并将返回一个空订单,就像这样:order = {}。这个订单然后传递到main_menu()函数,它决定接下来发生什么:如果订单为空,它将订单保存到文件中并退出。如果它不为空,它将订单添加到列表中并继续工作。让我们画一个图。”

“这将是我们更新的get_order()函数。让我帮你写它,”西蒙说。他拿起埃里克的键盘,开始编辑get_order()函数:
def get_order():
order = {}
name = input("Enter your name or enter 'X' to exit: ")
if name == "X" or name == "x": ❶
return {}
else:
order["name"] = name
drinks = read_menu("drinks.txt")
flavors = read_menu("flavors.txt")
toppings = read_menu("toppings.txt")
order["drink"] = menu(drinks, "Erik's drinks",
"Choose your drink: ")
order["flavor"] = menu(flavors, "Erik's flavors",
"Choose your flavor: ")
order["topping"] = menu(toppings, "Erik's toppings",
"Choose your topping: ")
return order
❶ 同时检查 'X' 和 'x'
“在这个函数中,我只是遵循了我们共同创建的图。”
“现在轮到主菜单了,”西蒙继续说。“这里还有一个图。”他开始画图。

“正如我们讨论的那样,如果get_order()返回一个空订单,我们就从主菜单退出。之后,我们的程序将订单保存到文件中。”西蒙将main_menu()函数编辑成以下内容:
def main_menu(orders):
while True:
order = get_order() ❶
if order == {}:
print("You entered 'X', exiting...")
return
print("Check your order:")
print_order(order)
confirm = input(
"Confirm? Press Y to confirm, N to cancel, X to finish: ")
if confirm == "Y" or confirm == "y":
orders.append(order)
print("Thanks for your order:")
print_order(order)
elif confirm == "X" or confirm == "x":
return
else:
continue
❶ 如果订单为空,我们必须退出。
你的回合! 编辑主菜单函数
按照西蒙的方式编辑你的主菜单函数。如果你需要帮助,本章的完整程序代码在这里:github.com/pavelanni/pythonicadventure-code/tree/main/ch09。
“就这样了,”西蒙说。“让我们测试一下。这次输入一个不同的名字,这样你就可以很容易地看到它被添加到了 JSON 文件中。”
埃里克启动了程序。当程序询问他的名字时,他回答了“Jason”。他输入了其余的订单,并键入Y以确认订单。程序再次询问他的名字。
“现在让我们输入x看看它是否正确退出,”西蒙建议。
埃里克键入x并按下了 Enter 键。
程序说:“你输入了'x',正在退出。”。“。”。“。”然后它返回到熟悉的>>> Python 提示符。
“现在检查一下orders.json文件,”西蒙说。
埃里克启动了他的文本编辑器并打开了 JSON 文件。就在文件的底部,他找到了他最近的来自“Jason”的订单。
“它工作了!”他说。“它将所有订单都保存在文件中,现在我可以看到它们了!”
“是的,你可以,”西蒙说,他笑了。他很高兴看到这个完整的、可以接收订单并将其存储在文件中的程序。而且这是他弟弟写的!
“恭喜,埃里克!”西蒙说。“我想你现在可以说你已经创建了一个真正的应用程序。看,它有输入和输出。它有数据结构和算法。它检查错误。它有数据存储。最重要的是,它工作得很好,非常有用——它收集订单。我绝对是认真的——这是一个好程序。我为你感到非常骄傲。”
和往常一样,西蒙画了一个图表。这次,是他所说的“真实”程序。

“是的,我也喜欢我的程序,”埃里克说。“它做我想做的事,看起来也不错。它打印订单的方式几乎和我看到星巴克的一样。是的,几乎……也许我可以添加几行或星星来让它更好。我还有一些关于要添加到这个程序中的其他想法。”
“你还想添加什么?”西蒙问。
“首先,我想让它成为一个网络应用程序——带有菜单和按钮。它应该在线,这样我就可以带着我的 iPad 使用它。”
“好主意!”西蒙说。“我们下周开始工作。我也有一些想法,”他说着笑了。
“你为什么笑?”埃里克问。
“我记得你第一天认为你已经完成了程序。”
“哈,是的,我也记得那件事,”埃里克说。“当然,那时候程序还没有完全准备好。你还有其他什么想法?”
西蒙说:“我想在我们的数据结构中添加一些东西。例如,我们可以添加订单创建的日期和时间。这样,我们就能看到我们每天或每月服务了多少客户。”
“是的,那会很好,”埃里克同意了。
“那么,也许我们应该在订单输入后立即将其保存在数据存储中。这样,即使程序失败并崩溃,我们也能确保保留所有之前的订单。”
“但你说过这会让它变慢,”埃里克记得。
“只是有一点小问题。但这是值得的——否则,我们可能会丢失所有订单。我在考虑使用数据库来做这件事。”
“还有,”西蒙继续说,“如果我们想让你这个程序成为一个真正的商业应用,我们需要像‘打印所有订单’和‘统计我们使用了多少份香草口味’这样的功能。”
“当然,我想要它,”埃里克说,“但首先我想把它做成一个网络应用,让它变得美观。”
“当然,我们可以下周开始工作。”
你今天学到的新知识
-
如何检查文件是否存在——我们使用了
osPython 模块和os.path.exists()方法来完成这个操作。你传递文件名,它返回True或False。 -
按住 Control-C 并不是结束程序的好方法——当我们想要停止表现异常的程序时,我们会使用 Control-C。好的程序应该始终给你一个正常结束它们的方法。
-
什么是真正的程序——我们了解到真正的程序有输入和输出、数据结构和算法、数据存储和错误检查。
本章的代码
你可以在这里找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch09。
10 学习 Flask:你的第一个网络应用程序
在本章
-
艾瑞克创建了第一个简单的网络应用程序
-
西蒙解释了 Flask 是什么以及它是如何工作的
-
埃米莉和艾瑞克在处理一个网络表单
-
第一个咖啡店基于 Web 的菜单准备好了!
“你说过你想创建一个网络应用程序吗?”西蒙在下周问艾瑞克。
“是的,当然!”艾瑞克说。“否则,我怎么能在我的 iPad 上使用我的程序呢?”
“好的,但要做好准备:这不是一件容易的任务。它将需要你全部的注意力。如果你不完全理解我们在这里做的所有事情,不要担心。当你需要时我会帮助你。”
“我知道——你是个好哥哥,”艾瑞克说。他说话的方式几乎没有讽刺意味。
“我们将使用我们的好朋友,Mu 编辑器,来构建我们的网络应用程序。它为此有一个特殊的模式。启动编辑器,点击左上角的模式。”
艾瑞克做了,并看到了一个菜单。

“滚动到页面底部,”西蒙说。“找到 Web 模式并点击它。然后点击确定。”

艾瑞克这样做后,西蒙指向右下角并说:“看到这个齿轮旁边的单词‘Web’了吗?这表明我们已经切换到了 Web 模式。现在让我们看看我们能用它做什么——点击新建。”
艾瑞克确实这样做了,Python 代码立即出现在编辑器窗口中。

“很有趣,”他说。“Mu 已经为我写了一些东西。我可以运行它吗?”
“当然,继续。首先,你必须保存它。命名为first_app.py。”
艾瑞克点击运行,并在保存对话框中输入first_app.py。他看到了窗口底部的输出。

“那是什么?”他问。
“这告诉你你的第一个网络应用程序正在运行。看到这条消息:‘运行在 http://127.0.0.1:5000/’吗?这意味着你可以去你的浏览器并输入这个地址:http://127.0.0.1:5000/。有时,这个地址被称为URL或统一资源定位符——当你与网络一起工作时,你经常会听到这些术语。”
艾瑞克在他的浏览器中打开了一个新标签页并输入了地址。他对他所看到的东西感到惊讶。

“等等,这些都是我的编辑写的吗?”他问西蒙。
“是的,但看,Mu 建议你自己写剩下的部分,”西蒙说。他指向页面上的代码示例。“Mu 建议你将代码从第一个灰色窗口复制到你的程序中。去试试吧。”
这很简单。艾瑞克迅速复制了文本并将其粘贴在现有代码下方。
“现在 Mu 告诉你创建一个新文件,”西蒙继续说。“从第二个窗口复制文本,并将其保存为名为greeting.html的新文件。”
艾瑞克知道如何做这件事。他在 Mu 编辑器中点击新建,移除了 Mu 放入其中的程序。他从第二个灰色窗口复制并粘贴了文本。然后他点击保存。
西蒙帮他,“使用下拉菜单将‘.py’更改为‘Other (.*)’。否则,Mu 会认为你试图保存一个 Python 程序。我们需要告诉它这是一个不同类型的文件。在“另存为”字段中,键入greeting.html。”

“现在停止程序并再次运行它,”西蒙说。“别忘了切换到first_app.py标签页。”
埃里克切换到应用程序标签,点击停止,然后再次点击运行。他看到窗口底部的输出与之前相同。
“现在回到你的浏览器,按照它建议的操作去做。看,它说‘访问/hello/你的名字’并告诉你使用你自己的名字。在地址栏中,在5000之后添加/hello/Erik。”
现在看起来像真正的黑客行为。埃里克输入了西蒙建议的内容并按下了 Enter 键。

“哇!它和我说话了!”他真的很印象深刻。
“这已经是你的程序了,它会和你说话,”西蒙说。“很简单,对吧?”
“等等,”埃里克说,“如果我们要处理这些网页内容,我应该叫艾米丽。她告诉我她学过 HTML,这是我们做网页需要的东西,对吧?”
“绝对正确,”西蒙同意。“继续,给她打电话。一起工作总是好的。”
轮到你了! 创建你的第一个网络应用程序
在 Mu 编辑器中切换到 Web 模式,并像埃里克一样从浏览器页面复制示例来创建你的第一个网络应用程序。试着用你的名字运行它。试其他名字。向你的朋友展示并让他们使用他们的名字。
艾米丽住在附近。她大约 15 分钟后到达,对项目非常兴奋。她说,“埃里克,给我看看你的 HTML!”
埃里克给她展示了greeting.html文件。他说,“嗯,这不是我的代码,是从这个 Mu 编辑器来的。”
“哇,很有趣,”艾米丽说。“我从未在 HTML 中看到过这些花括号。”
“没错,”西蒙说,“因为这不是纯 HTML——这是一个模板。我们在这里使用了一个叫做 Flask 的程序,它是 Mu 的一部分,它使用模板来生成 HTML。”
“我明白了,”艾米丽说。“但我知道这些<h1>和<p>标签。”
“标签?‘标签’是什么?”埃里克问。
“标签是你放入文本中以改变其外观的小段代码。看这里,你在‘Hello!’之前放置<h1>,并在其后放置</h1>,它看起来更大。这在 HTML 中被称为标题,就像章节标题。”
“那<p>呢?”埃里克问。
“它意味着‘段落’,”艾米丽解释道。“在 HTML 中,你可以按照你想要的方式编写你的文本:一行很长,很多短行,或者每行一个单词。但是如果在开头有<p>并在结尾有</p>,它将在浏览器中显示为一个段落。”
“还有很多其他的标签,”她继续说。“你可以使你的文本加粗或斜体,改变颜色,等等。”
“艾米丽,你知道关于 HTML 表单的任何东西吗?”西蒙问。
“课堂上他们告诉我们可以在 HTML 中创建表单来输入文本或使用菜单,”艾米丽回答。“但我自己还没有尝试过。”
“菜单就是我们想要的!”埃里克惊呼。
“我会帮你的,”西蒙说。“首先,我们应该使用必需的 HTML 标签。我们应该在文件的开始处有<html>,在结尾处有</html>。此外,我们应该在文本周围使用<body>标签。再次强调,我们使用<body>来打开文本,使用</body>来关闭它。这就是为什么带有斜杠/的标签被称为闭合标签。”
<html>
<body>
</body>
</html>
“标签就像 Python 列表中的括号,”艾瑞克说。他想向艾米丽展示他已经知道了 Python。
“你说得对,”西蒙确认道。“这些标签包含一些文本并解释其含义。一些文本是标题,一些是段落。但现在我们想要创建一个菜单。为了做到这一点,我们首先需要一个<form>标签,然后在其内部使用<select>标签。让我们创建一个非常简单的菜单。”他开始写起来。
列表 10.1 带有两个选项的第一个菜单表单
<html>
<body>
<form>
<select>
<option>Coffee</option>
<option>Decaf</option>
</select>
<input type="submit" value="Submit">
</form>
</body>
</html>
轮到你了! 创建你的第一个网页表单
创建一个forms.html文件,并将其保存到mu_code/templates目录下。复制前面的代码并在浏览器中测试它。尝试更改选项并添加更多选项。
西蒙写完之后点击了保存。他将文件保存为forms.html,位于mu_code/templates目录下。“你在这里看到了多少被标签包含的元素?艾米丽,你应该对此更熟悉。”
艾米丽开始计数。“首先,<html>标签,然后是<body>标签。在<body>内部我们有一个<form>,在<form>内部我们有一个<select>。这是菜单,对吗?然后在<select>中,我们有两个<option>元素。”
“没错。艾米丽,你做得很好,”西蒙说。“别忘了<input>元素,它是表单的一部分。它没有闭合标签——它独立存在。我们用它来创建提交按钮。”
“让我们看看它在浏览器中的样子,”西蒙继续说。
“你能在浏览器中打开文件吗?”艾瑞克问。“我以为浏览器只用于网站。”
“当然,你可以,”艾米丽回答。“我们在 HTML 课程中一直这么做!你只需在浏览器中使用文件菜单,然后点击打开文件。然后你找到你的文件。就这样。”
艾瑞克做了艾米丽说的,打开了forms.html文件。他看到了一个菜单,非常类似于他在许多网站上看到和使用过的。他点击了菜单,它打开了。

“我不知道你可以这么容易地创建表单,”艾米丽说。
“是的,创建这样一个简单的表单很容易,但还有一些缺失的部分,”西蒙说。
“看起来不错,”艾瑞克说。“缺少什么?”
“是的,看起来不错,但它不做任何事情,”西蒙说。“我们必须从用户那里获取数据,然后将这些数据传递给程序。我们如何传递数据?”西蒙问道。然后他回答了自己的问题,“我们应该使用变量和值,这与 Python 非常相似。让我在这个表单中添加一些内容:”
列表 10.2 带有变量和action目标的更新后的表单
<html>
<body>
<form action="/order" method="post">
<select name="drink"> </select>
<input type="submit" value="Submit"> ❶
<option value="">- Choose drink -</option> ❷
<option value="coffee">Coffee</option> ❸
<option value="decaf">Decaf</option> ❸
</form>
</body>
</html>
❶ 定义菜单将返回的变量
❷ 添加一个选项告诉用户要做什么
❸ 定义菜单选项将返回的值
“首先,”Simon 说,“我们定义从这个菜单返回的变量。在这个菜单中,变量叫做drink。”
“在第一个选项中,我们告诉用户该做什么。这个选项将首先显示在菜单中,并作为提示使用。正如你所看到的,它的值是空的。如果用户没有选择饮料,他们会看到这个提示。没有这个信息,你无法准备他们的订单,对吧?”
“最后,接下来的两个选项定义了这个菜单选项可以返回的值。这和我们选择文本菜单中的项目时做的是类似的,记得吗?”
“我能试试吗?”Erik 问。
“当然,你可以再次打开文件。或者只需在浏览器中重新加载它。”
Erik 重新加载了文件,并从菜单中选择了脱咖啡因。然后他点击了提交。

“那是什么?”他问,显然很困惑。
“哦,我忘了告诉你,”Simon 说。“看,我稍微修改了form标签。”
列表 10.3 action目标
<form action="/order" method="post">
“每个表单都应该有一个action,”他开始解释。“当用户提交表单时,我们的应用程序会执行一些操作。当用户做出选择——咖啡或脱咖啡因——他们应该将这个信息传递给某个函数。这个函数应该知道如何处理这个信息:将其存储在文件或数据库中,打印订单,等等。”
“就像我们以前程序中做的那样?”Erik 问。
“你一直在谈论你的‘以前’的程序。你能给我看看吗?”Emily 要求。
“对不起,Emily,”Simon 说。“我应该早点解释的。我和 Erik 合作编写了一个收集咖啡店订单的程序,类似于星巴克,在那里你可以点饮料,添加风味和配料,等等。Erik 编写了一个显示菜单并询问客户他们想点什么的程序。当他们选择饮料、风味和配料时,程序会打印订单。但现在程序只能在终端的文本模式下工作。Erik 需要你的帮助将这个程序转换为 Web 应用程序。”
“这听起来是个很酷的项目!”Emily 说。“我希望 Erik 也会教我 Python。”
“当然,”Simon 说。“教别人是最好的学习方法。”
“回到我们的形式,”他继续说。“这个action属性告诉浏览器:‘在用户提交表单后,打开这个地址并传递表单中的信息。’在我们的例子中,这个地址叫做/order。”
“别担心,听起来可能有点可怕——我会告诉你如何使用它,”Simon 补充道。他注意到 Emily 和 Erik 脸上的困惑。
“我仍然不明白,”Erik 说。“你说的这个地址在哪里?”
“看看你的第一个应用程序,”Simon 说。“看到这个greet函数了吗?这个函数是由 Mu 编辑器为我们写的——或者更确切地说,是由它的作者写的,”Simon 说。“你看到现在熟悉的以def开始的函数定义,但是,就在它上面,你可以看到一些新的东西:@app.route('/hello/<name>')。在 Python 中,它被称为装饰器,但我们今天不会学习装饰器。”
@app.route('/hello/<name>')
def greet(name='Stranger'):
return render_template("greeting.html", name=name)
“对我们今天来说重要的是,你可以用它来告诉你的程序使用哪个函数来处理哪个地址。”
“啊哈,这正是我在浏览器中输入的地址 /hello/Erik!我现在明白了,”艾瑞克说。“让我们给艾米丽展示一下它是如何工作的。”他在编辑器中打开了包含 first_app.py 文件的标签页,并点击了运行。然后他点击了浏览。他的浏览器打开了一个显示穆的问候语的页面。
“艾米丽,看,我可以在这些数字后面直接输入地址,127.0.0.1:5000。看看它显示给我们什么!”艾瑞克输入了 /hello/Emily 并按下了回车键。浏览器显示了结果。

“哇,我喜欢!”艾米丽说。“我可以试试吗?”她将“Emily”改成了“Erik”。当然,浏览器显示了“Hello Erik,你好吗?”的页面。
“很有趣!”她说。“在我们的 HTML 课程中,我们可以更改页面,但我们必须编辑 HTML。这要容易得多!”
“没错,”西蒙说。“这就是我们所说的动态页面——根据你输入的内容而变化的页面。你可以在地址中输入你的信息,比如 /hello/Emily,或者你可以使用表单。然后页面就会根据你输入的信息生成。我相信你一定见过很多这样的页面——例如,当你在网上输入评论或与人聊天时。你点击提交或直接按回车键,页面就会更新,对吧?现在你将学习如何自己制作这样的页面。”
“让我用图表来展示,”他说。西蒙开始画图。

“你浏览器顶部的这个字母和数字的组合是地址或URL。我用蓝色标出了它。通常,你会在这里看到网站的名称,比如 google.com。在我们的例子中,它使用的是数字,这是网站的 IP 地址。我们正在使用你的电脑,对于世界上每台电脑,地址 127.0.0.1 都意味着‘这台电脑’。但现在你不用担心这个。”
西蒙指向地址中“order”这个词的第一个红色圆圈,说:“这就是你应该关注的。看,它是地址的一部分。当我们打开这个地址时,我们看到的是带有饮料菜单的表单。当你点击提交”时,他顺着图中的箭头指向,“表单知道它应该找到负责 /order 地址的函数。你看,它就在这里的 form action 字段中。”
“然后,”他顺着箭头指向 Python 块,“表单找到了可以与之一起工作的 Python 函数,因为我们使用了 @app.route('/order') 装饰器。你看,这三者是有联系的;你只需要在地址、表单和 Python 程序中使用相同的名字。”
“我注意到函数也被称作‘order’——这是第四个我们使用它的地方吗?”艾瑞克问道。
“你真有一双敏锐的眼睛!”西蒙说,并微笑着。“不,在这种情况下,函数可以有不同的名字。我可以叫它‘new_order’或‘get_order’。”
“但现在我们必须编写实际的函数。我会在这里帮助你。它看起来可能有点吓人,但别担心。我自己也在学习这个 Flask 系统,通常我会遵循在线教程并从中取例。别以为我都是自己记住的这些事情。”
西蒙编写这个函数时,正在查看浏览器中打开的示例。他说:“在第一行,你看到的是GET和POST这两个词。这是我们与网络服务器一起使用的方法。当我们想从网络服务器获取东西时,比如一个网页,我们使用GET。我们使用POST来发送信息到网络服务器。就像这个案例一样——我们想要发送或POST客户选择的饮料。换句话说:当你用浏览器加载一个页面时,你使用GET;当你点击表单中的提交时,你使用POST。当你开始使用它时,你会更好地理解它,别担心。”
“在那之下,我们开始使用一个单词,POST:if request.method == 'POST':。看看它说了什么:如果方法是POST,这意味着有人填写了表单并点击了提交,我们就读取他们在表单中输入的信息并打印出来。
“记住我们在表单中使用了<select name="drink">吗?这就是我们在下一行,在方括号中使用的名字:drink = request.form['drink']。稍后,我们将添加其他用于口味和配料的选择菜单。在表单中,它们将具有flavor和topping这样的名字。在这里,在代码中,我们将使用request.form['flavor']和request.form['topping']。
“在print行中,我们只是打印从表单接收到的任何内容。你会在编辑器中看到它。
“在最后一行,我们告诉我们的网络服务器打印这个带有菜单的页面。它就像我们之前程序中使用的菜单循环——你从客户那里获取信息,打印出来,然后返回菜单以获取另一个订单。你重复这个循环,直到你完成订单输入。”
下面的列表显示了西蒙编写的函数:
列表 10.4 处理订单的函数
@app.route('/order', methods=('GET', 'POST')) ❶
def order():
if request.method == 'POST': ❷
drink = request.form['drink'] ❸
print("Drink: ", drink) ❹
return render_template("forms.html") ❺
❶ 我们将使用此表单的方法
❷ POST 方法意味着我们在提交信息。
❸ 从表单的名为drink的字段获取客户选择
❹ 打印我们收到的选择
❺ 显示 forms.html 模板。
“让我再补充一点,”西蒙说。他在import的第一行添加了request。“这个名为request的模块是 Flask 的一部分。如果我们使用它,我们必须导入它。”现在第一行看起来是这样的:
from flask import Flask, render_template, request
轮到你了! 编写你自己的order()函数
将前面的order()函数添加到你的first_app.py程序中。别忘了更改import行。
然后尝试运行它。在浏览器中打开一个新标签页,并使用http:// 127.0.0.1:5000/order地址。如果你遇到问题,继续阅读并跟随艾米丽和埃里克的做法。
“我现在可以运行它了吗?”埃里克问道。经过这样长时间的讲解,他有点累了。另一方面,艾米丽好像在听西蒙施展魔法一样,她喜欢所有这些编程魔法,迫不及待地想尝试程序。
“我可以运行它吗?”艾米丽问道。
“当然,”埃里克说。“只需点击保存然后运行。”
“现在点击浏览,”西蒙说。
艾米丽确实做到了,页面在新浏览器标签页中打开了。

“现在我们必须在地址中添加/order,记住?”西蒙帮她解释。
艾米丽确实做到了,浏览器中的地址行变成了:http://127.0.0.1:5000/ order。页面显示了他们刚刚一起创建的菜单。

“继续。选择你的饮料并点击提交,”西蒙说。
艾米丽选择了无咖啡因咖啡并点击了提交。她得到了与选择饮料菜单相同的页面。
“它工作了吗?”她问道。她看起来很困惑。
“让我们检查一下,”西蒙说。“回到编辑器。看看底部的窗口。

“你看到这条线:Drink: decaf吗?这是我们程序打印的内容,”西蒙说。“这意味着它工作了!”
“但我以为它会将订单打印在页面上,”艾米丽说。
“它会,相信我,”西蒙说。“我们还没有写那部分。我们正在接近,现在。”
他拿起键盘,在print()行之后添加了一行到first_app.py文件中。西蒙解释说,“当我们第一次在浏览器中打开/order地址时,这意味着我们使用的是GET方法。我们想要获取页面,对吧?我们还没有什么要POST的。在这种情况下,我们使用显示饮料菜单的forms.html模板。
“但我们在选择了一款饮料并点击提交后,我们使用的是POST方法。我们想要发送这些信息到程序。在这种情况下,我们从表单中收集数据——饮料选择——并使用另一个模板。我称之为print.html,因为我们想要打印订单。”
order()函数现在看起来是这样的:
@app.route('/order', methods=('GET', 'POST'))
def order():
if request.method == 'POST':
drink = request.form['drink']
print("Drink: ", drink)
return render_template("print.html", drink=drink) ❶
return render_template("forms.html")
❶ 使用print.html模板打印订单
“但我们没有名为print.html的文件,”艾米丽说。
“好的,我现在就创建它。”西蒙在编辑器中创建了一个新的文件,并将其保存为print.html,位于templates目录下:
列表 10.5 打印订单的模板(第一版)
{% extends "base.html" %}
{% block content %}
<h1>Thanks for your order!</h1>
<p>Your drink: <strong>{{drink}}</strong></p>
{% endblock %}
艾米丽看了他的代码说,“哦,这个我可以理解!你打印标题‘感谢您的订单’,然后打开一个新的段落并打印‘您的饮料’,然后在粗体中打印饮料本身。并且这个双大括号中的drink工作方式和我打印‘Hello Emily’时使用我的名字的方式是一样的,对吗?”
“完全正确!”西蒙大声说道。“你完全正确,艾米丽!”
轮到你了! 编辑你的网页表单以在页面上打印饮料选择
将前面的模板复制到你的print.html文件中。你可以随意更改标题和文本。通过添加带有return的行来更改first_app.py文件,并保存它。尝试运行你的程序。
“我可以试试吗?”埃米莉问。
“当然,继续点击运行,”西蒙说。
埃米莉点击运行并从菜单中选择咖啡。她看到了更新后的页面。

“是的,它工作了!”她惊呼。
“这就像在我们之前的程序中调用print_order()函数一样,”艾瑞克说。
“是的,正是这样!”西蒙说。
“但我怎么回到订单页面呢?”艾瑞克问。
“你看到浏览器中的地址仍然是指向/order吗?”西蒙问,“这意味着如果你用鼠标点击地址行并按 Enter 键,你会重新加载订单页面。只是不要点击刷新按钮,否则会创建另一个订单。”
埃米莉按照西蒙说的做了,她又看到了订单页面。
“但有一个更好的方法,”西蒙说,“你之前在打印页面上找的是一个按钮,比如回到订单页面,对吧?”
“是的,这样会更容易,”埃米莉同意了。
“我们可以使用另一个表单来做这件事,”西蒙说,“这会非常简单。”他在templates/print.html文件中添加了几行。
“看,我创建了一个只有提交按钮的另一个表单。我只是把它改名为‘新订单’。看,它的action字段指向/order。这意味着当我们点击新订单按钮时,它会带我们到/order页面,并再次显示饮料菜单。试试!”
列表 10.6 带有新订单按钮的更新版打印模板
{% extends "base.html" %}
{% block content %}
<h1>Thanks for your order!</h1>
<p>Your drink: <strong>{{drink}}</strong></p>
<form action="/order">
<input type="submit" value="New order" />
</form>
{% endblock %}
埃米莉照做了,点击提交后,她看到了带有新按钮的页面。

她点击了按钮,然后回到了订单页面。
艾瑞克指出,“它就像我们的主菜单带有一个循环。订单,然后确认,然后打印,然后回到订单菜单。”
“你说得对!”西蒙确认道,“让我用图来展示。

“看看这个图。第一步:你从菜单中选择 Decaf。这会将值decaf分配给表单中的drink变量。
“第二步:那个值decaf通过request.form传递到我们的 Python 程序中。现在 Python 程序中的drink变量有了decaf的值。
“第三步:我们将 Python 中的drink变量的值——即decaf——传递到print.html模板中的drink变量。
“第四步:我们调用render_template()函数,并传入drink变量,它会被其值decaf所替换。现在decaf已经在网页上打印出来了。
“最后,第五步:我们点击新订单,返回到订单页面。
你的机会来了! 修改print.html模板
将新订单按钮添加到templates/print.html模板中。测试一下。你能返回到订单页面吗?
“埃米莉和艾瑞克,你们今天做得很好,”西蒙说,“最重要的是,你们在整个漫长的解释过程中都没有睡着。”
“我差点就做到了,”艾瑞克说。
“是的,我注意到了,”西蒙说,然后微笑。“但说真的,创建 Web 应用是比处理文本菜单和对话框难 10 倍的事情。我钦佩你的耐心!”
“但是这很值得,”埃米莉说,“程序现在可以工作了!”
“我们还需要添加一些风味和配料,”艾瑞克说。
“对!”西蒙说。“我们必须创建所有的菜单,但要以 Web 表单的形式。我非常确信艾米丽会帮你做这件事。”
“当然,”艾米丽说。“看起来我们需要在模板中添加更多的select表单。艾瑞克,你能给我看看你之前的程序吗?我们明天就可以开始制作网络应用程序了。”
“当然,”艾瑞克说。“我们明天聚在一起工作吧。”
“我很乐意帮忙,”西蒙说。“请告诉我你什么时候开始。”
你今天学到的新知识
-
Mu 编辑器的 Web 模式——除了标准的 Python 模式外,Mu 编辑器还有一个 Web 模式。它有一个简单的网络应用程序示例。
-
HTML 表单——这是一种将用户信息输入到网络应用程序中的方法。你可以有菜单、文本字段和按钮。当你点击提交时,表单会将信息发送到在
action字段中配置的特殊地址。从该地址,信息可以被程序处理。 -
Flask——一个帮助我们创建网络应用程序的程序。它由开源社区开发,拥有良好的教程和示例。许多在线网站和网络应用程序都在使用它。
本章的代码
你可以在这里找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch10。
11 订单网页表单:网络上的咖啡店
在本章
-
艾米丽和埃里克为他们的商店创建了一个真实的网页表单
-
西蒙解释如何从表单传递值到程序
-
埃里克和艾米丽学习了表单元素,如
input和select -
西蒙解释文件是如何工作的
第二天,埃里克、艾米丽和西蒙聚在一起继续工作 Coffee Shop 应用程序。
“你准备好认真工作了么?”西蒙问。“今天,我们必须着手制作我们的实际网页表单。昨天我们尝试了一个非常简单的菜单。现在我们必须添加两个更多的菜单和顾客可以输入他们名字的文本字段。
“让我们看看我们昨天的代码。我们已经在那里创建了一个菜单——在 <select> 和 </select> 标签之间的代码。这个菜单已经有了两个选项:咖啡和低咖啡因。
列表 11.1 templates/forms.html 中的第一个菜单选项
{% extends "base.html" %}
{% block content %}
<form action="/order" method="post">
<select name="drink">
<option value="">- Choose drink -</option>
<option value="coffee">Coffee</option> ❶
<option value="decaf">Decaf</option> ❷
</select>
<input type="submit" value="Submit">
</form>
{% endblock %}
❶ 咖啡菜单选项
❷ 低咖啡因菜单选项
“现在我们必须给 drink 菜单添加另一个选项。埃里克,我想你在那个菜单里有‘巧克力’,对吧?”
“是的,咖啡、巧克力和低咖啡因,”埃里克确认。
“很好。那么口味和配料呢?”
“对于口味,我们有焦糖、香草、薄荷、树莓和平常的,”埃里克说。“对于配料,我们有巧克力、肉桂和焦糖。”
“美味,”艾米丽说。
“这里有一个任务给你,”西蒙说。“首先,给 drink 菜单添加第三个选项。其次,为口味和配料添加两个更多的菜单。这需要一些复制、粘贴和输入——还有注意力。谁在键盘上?”
“是的,”艾米丽说。“我能做到。我对 HTML 和所有这些标签都很熟悉。”
“非常好,继续!”
艾米丽开始编辑文件。埃里克在旁边观看,时不时地提问。
“你为什么把 <option> 行移动到右边?HTML 关于缩进的规则和 Python 一样吗?”
“不,在 HTML 中,你不需要缩进行,”艾米丽回答。“我可以从第一个位置开始写它们,但这样看起来更好。在我的课上,我们的老师说过这对可读性有好处。”
“是的,西蒙也这样告诉我,”埃里克说。“在 Python 中,你必须使用缩进来表示代码块,但在某些情况下,你可以自由选择是将代码放在一行还是使用新行和缩进。西蒙说你应该编写可读性高的代码——至少对你自己来说。”
在埃里克的帮助下,艾米丽列出了口味和配料的所有选项,并创建了以下 HTML 模板:
列表 11.2 templates/forms.html 中的附加菜单选项
{% extends "base.html" %}
{% block content %}
<form action="/order" method="post">
<select name="drink">
<option value="">- Choose drink -</option>
<option value="coffee">Coffee</option>
<option value="chocolate">Chocolate</option>
<option value="decaf">Decaf</option>
</select>
<select name="flavor">
<option value="">- Choose flavor -</option>
<option value="caramel">Caramel</option>
<option value="vanilla">Vanilla</option>
<option value="peppermint">Peppermint</option>
<option value="raspberry">Raspberry</option>
<option value="plain">Plain</option>
</select>
<select name="topping">
<option value="">- Choose topping -</option>
<option value="chocolate">Chocolate</option>
<option value="cinnamon">Cinnamon</option>
<option value="caramel">Caramel</option>
</select>
<input type="submit" value="Submit">
</form>
{% endblock %}
轮到你了! 创建你的菜单
在 templates/forms.html 文件中创建新的菜单。请随意使用不同的选项。从你当地的咖啡店或冰淇淋店获取一些灵感。
西蒙看了看他们的代码,说:“看起来不错。但在我们从 HTML 切换到 Python 之前,让我们给表单添加一个东西。记住,我们还问了顾客的名字。”
“就像在星巴克一样!”艾米丽说。
“正是这样,” 西蒙说。“让我在你的表单中添加一行。它被称为 input,类型为 text。我们将用它来输入名字。你看,我就在 form 行之后添加了它。
列表 11.3 在 templates/forms.html 中输入名字的文本字段
{% extends "base.html" %}
{% block content %}
<form action="/order" method="post">
<input type="text" name="name" placeholder="ENTER NAME">
<select name="drink">
<option value="">- Choose drink -</option>
<option value="coffee">Coffee</option>
. . .
你的机会! 添加一个 input 字段
为顾客添加一个 input 字段。别忘了放一个占位符作为用户的提示。
“再看看 HTML 代码,” 西蒙继续说。“艾米丽添加的所有三个菜单,以及 input 行,都在 <form> 开标签和 </form> 关闭标签之间。艾米丽在可读性方面做得很好,对它们进行了缩进。
“现在我们可以清楚地看到它们都在表单内。这意味着它们都是 同一表单 的部分。所以,当你点击提交时,它们将一起传递给程序。”
“在程序中,我们将能够使用 request.form 字典来读取它们,就像我们在我们的简单程序中所做的那样。”
“就像 request.form['drink'] 一样?” 艾瑞克问道。
“是的,没错,” 西蒙回答。“看,艾米丽给他们起了名字,flavor 和 topping,所以我们可以用这些作为程序中字典的键。让我们再次打开 first_app.py 并看看我们需要在哪里做修改。记得我们在这里做了什么吗?我们获取表单字典,并将 drink 键的值复制到 drink 变量中。例如,如果顾客选择了 chocolate,它将被分配给 drink 变量。这个变量将被传递给名为 print.html 的表单,以在页面上显示——或者说,有些人说是 渲染——在页面上。”
列表 11.4 first_app.py 中 order() 的第一个版本
. . . .
@app.route("/order", methods=("GET", "POST"))
def order():
if request.method == "POST":
drink = request.form["drink"]
print("Drink: ", drink)
return render_template("print.html", drink=drink)
return render_template("forms.html")
西蒙继续说,“现在我们正在从表单中获取更多值,我们应该收集它们并将它们传递给模板。艾瑞克,你能添加这些行吗?”
“我想是这样的,” 艾瑞克说。
当西蒙说,“别忘了 name!”时,艾瑞克添加了关于口味和配料行的代码。
艾瑞克添加了它,他的代码现在看起来是这样的:
列表 11.5 在 first_app.py 中传递给打印模板的更多菜单选项
. . . .
@app.route("/order", methods=("GET", "POST"))
def order():
if request.method == "POST":
name=request.form["name"]
drink=request.form["drink"]
flavor=request.form["flavor"]
topping=request.form["topping"]
return render_template("print.html",
name=name,
drink=drink,
flavor=flavor,
topping=topping)
return render_template("forms.html")
你的机会! 编辑你的 Flask 应用程序
编辑你的 first_app.py 程序,并添加像艾瑞克一样的变量。现在不要运行它,因为你还没有 print.html 文件。
“干得好,艾瑞克!” 西蒙说。“嗯,在我们测试之前,可能还为时过早这么说,” 他微笑着。
“现在只剩下一件小事了,” 西蒙继续说。“我们必须将这些变量添加到 print.html 模板中。让我们在编辑器中打开它。看,除了主要饮料外,我们还要打印口味和配料。哦,别忘了我们现在知道顾客的名字。打印一个问候语会很有礼貌。艾米丽,你能编辑这个 HTML 吗?”
“当然,” 艾米丽说。她开始编辑,同时向艾瑞克解释。
“首先,让我们将‘感谢您的订单’替换为‘你好’和顾客的名字。我将它留在这里作为 <h1> 以使其加粗并放大。
“然后我们在 <h2> 中放入‘这是您的订单’,”她继续说。“它将稍微小一点。之后,我们将打印订单:主要饮料、口味和配料,每个都在一个单独的段落中。我会继续使用 <strong> 标签来加粗打印。在页面底部,我们有一个简单的表单,只有一个按钮,可以带我们回到订单页面。我希望你记得昨天的内容。”
列表 11.6 打印订单的模板 templates/print.html
{% extends "base.html" %}
{% block content %}
<h1>Hello {{name}}!</h1>
<h2>Here is your order:</h2>
<p>Drink: <strong>{{drink}}</strong></p>
<p>Flavor: <strong>{{flavor}}</strong></p>
<p>Topping: <strong>{{topping}}</strong></p>
<form action="/order"> ❶
<input type="submit" value="New order" /> ❶
</form> ❶
{% endblock %}
❶ 这个只有一个按钮的表单,可以带我们回到订单页面。
轮到你了! 创建一个 templates/print.html 模板
创建一个打印模板,就像艾米丽刚刚做的那样。注意变量:你应该使用从程序传递过来的变量。如果你更改了first_app.py文件并使用了不同的变量,这里也要相应更改。然后尝试运行应用程序。
“我们现在可以试试吗?”艾瑞克问道。
“你保存所有文件了吗?”西蒙问道。“看看编辑器中的标签。如果你没有保存它们,Mu 会用一个小红圈显示它们。”
“一切正常,”艾瑞克说。
“然后切换到 Python 选项卡并点击运行。”
艾瑞克这样做,然后点击浏览。他的浏览器打开了一个新的标签页,上面是熟悉的“来自 Mu 的问候”页面。艾瑞克在地址栏中添加了 /order,他看到了菜单。

“让我试试!”艾米丽说。她输入了她的名字,并选择了巧克力、香草和焦糖。她点击提交,订单被打印出来。

“哇,它真的工作了!”她兴奋地看到他们刚刚一起编写的真实网络应用程序。
“太棒了,”西蒙说。“现在我们必须添加我们为文本版本编写的函数。”
“什么函数?”艾米丽感到困惑。她认为这已经完成了。“艾瑞克,你能解释一下吗?”
“我们在这里写的是我们应用程序的用户界面。我们之前也这样做过,但是在文本模式下。这个网络应用程序看起来好多了。但除了打印订单,我们还想将其保存到文件中。例如,我们可能想计算我们用焦糖服务了多少订单。”
“我明白了,为了知道什么时候为商店购买更多的焦糖,”艾米丽说。
“完全正确!你像一位商店经理一样思考,艾米丽!”西蒙说。
他继续说:“在你加入我们之前,我们创建了一个函数,用于将订单保存到文件中。我们的程序在我们告诉它完成时保存订单。下次我们启动程序时,它会将所有之前的订单加载到一个列表中,并准备好添加新的订单。”
西蒙停顿了一下,思考着。“但是,对于这个新的网络应用程序,我们得稍微不同一点。”
“为什么?”艾瑞克问道。
“因为我们的应用程序一直在运行,我们不会停止它。这种应用程序被称为网络服务。它不会停止。它一直在运行。更重要的是,如果它出了问题并且由于某种原因崩溃,另一个程序会重新启动它,以确保它始终准备好响应请求——在我们的案例中,是接受订单。”
“对我们来说,” 西蒙继续说,“这意味着我们必须在收到每个订单后立即保存它。让我快速找到正确的方法。”
他打开了一个新的浏览器标签页,搜索了几分钟。“我现在知道了,” 他最后说,“艾瑞克,让我们打开我们之前的程序,并将 load_orders() 和 save_orders() 函数复制到我们的网络应用中。”
“我们将在哪里使用它们?在 order() 函数中?” 他问西蒙,“你上次告诉我,我应该在使用它们之前将它们放入代码中,对吧?”
“是的,绝对正确,” 西蒙说,“将它们放在非常开始的地方,甚至在 app = Flask(name) 行之前。这样,我们将文件函数与网络应用函数分开。另外,别忘了添加 import json 和 import os。”
艾瑞克从他们几天前工作的 main_menu.py 文件中复制了函数。
西蒙检查了他的工作并说,“很好。我们只需要在加载订单后,还有最重要的是在保存它们之后关闭文件。”
“但我们之前没有这样做。为什么我们现在要这样做?‘关闭文件’到底意味着什么?” 艾瑞克问。
西蒙说,“我们之前讨论过这个问题。让我找到这个图解。艾米丽,我想你也会想了解它。”
“这像是在 Word 中关闭文件一样吗?” 艾米丽问。
“是的,但是,在这种情况下,我们应该从我们的 Python 程序中这样做。理解这一点很重要,直到你关闭文件,Python 都会将订单保存在内存中。如果我们的电脑发生故障并关闭,所有订单都会丢失。所以,我们必须关闭文件以确保所有订单都已保存到驱动器上。看看这个图解。”

轮到你了! 尝试解释文件是如何工作的
使用这个图解,并尝试向别人解释文件是如何工作的。你解释之后理解得更好了吗?
“我们之前没有这样做,因为我们的程序在结束时自动关闭了所有文件。在这个网络应用中,我们将每次收到新订单时都关闭包含订单的文件。”
“但你说过这会减慢程序的速度,” 艾瑞克说。
“是的,这确实会稍微慢一点。但工程学就是关于权衡——这是我机器人老师一直告诉我们的。我们的程序变得越来越真实,所以我们应该考虑保存重要数据——即使这会使程序稍微慢一点。”
西蒙在两个函数中都添加了 f.close() 方法,所以函数现在看起来如下所示:
列表 11.7 在 first_app.py 中加载和保存订单的函数
import os
import json
from flask import Flask, render_template, request
def save_orders(orders, filename):
f = open(filename, "w")
json.dump(orders, f, indent=4)
f.close()
return
def load_orders(filename):
if os.path.exists(filename):
f = open(filename, "r")
orders = json.load(f)
f.close()
return orders
else:
orders = []
return orders
. . . .
西蒙补充说,“当然,在这里使用数据库会更好。也许我们稍后会添加它。”
“现在我们必须从文件中加载订单,” 西蒙说。他在 Flask 行之前添加了一行:
列表 11.8 使用 first_app.py 中的 load_orders() 创建订单列表
. . . .
orders = load_orders("orders.json")
app = Flask(name)
. . . .
“现在我们有一个订单列表,我们将向其中添加新的订单。这让我有了一个想法。” 西蒙开始输入,同时解释着。
“我们有一个列表,我们用它来保存订单,它是一个 Python 字典列表,对吧?所以,当我们从表单中获取新订单时,我们最好将其复制到一个字典中,而不是单独的变量中。我们将这个字典追加到列表中,然后将其保存到文件中。甚至更好,我们可以将字典传递给 HTML 模板以显示它。这将简化我们的程序!”
他打完字后,order() 函数现在看起来是这样的:
列表 11.9 在 first_app.py 中将新订单改为字典
@app.route("/order", methods=("GET", "POST"))
def order():
if request.method == "POST":
new_order = {"name": request.form["name"],
"drink": request.form["drink"],
"flavor": request.form["flavor"],
"topping": request.form["topping"]
} ❶
orders.append(new_order) ❷
save_orders(orders, "orders.json") ❸
return render_template(
"print.html", new_order=new_order
) ❹
return render_template("forms.html")
❶ 从表单创建一个新的字典
❷ 将新订单追加到列表中
❸ 将列表保存到 JSON 文件中
❹ 将字典传递给模板
你的回合了! 编辑应用程序
使用前面的三个列表并编辑你的 first_app.py 程序。确保你理解在创建新订单并将其传递给模板时发生了什么。
“最后,我们需要更改 HTML 模板,”西蒙说。“艾米丽,你能帮我一下吗?”
“当然,但我不知道该怎么做,”她回答。
“看,我们以前是将变量传递给模板以在页面上显示:name、flavor 等等。现在,我们不再传递四个变量,而只传递一个包含所有四个的 new_order 字典。在 Python 中,我们称之为 字典。你给它一个键,它就返回一个值。例如,你给它 flavor,它就返回 caramel。”
“是的,艾瑞克已经解释了 Python 中的字典。但我们如何在 HTML 中使用它们呢?”
“就像我们在 Python 中做的那样:用方括号!让我给你演示一下,”西蒙说。他在 Hello 行中将 name 替换为 new_order[name]。
“我现在明白了,”艾米丽说。她开始更改其他变量。以下列表显示她在完成更改后的 print.html 模板:
列表 11.10 在 templates/print.html 中使用字典
{% extends "base.html" %}
{% block content %}
<h1>Hello {{new_order["name"] }}!</h1>
<h2>Here is your order:</h2>
<p>Drink: <strong>{{ new_order["drink"] }}</strong></p>
<p>Flavor: <strong>{{ new_order["flavor"] }}</strong></p>
<p>Topping: <strong>{{ new_order["topping"] }}</strong></p>
<form action="/order">
<input type="submit" value="New order" />
</form>
{% endblock %}
你的回合了! 编辑 templates/print.html 文件
编辑你的打印模板,并从使用变量改为使用字典。注意字典键——如果你在 Python 程序中更改了它们,你应该在这里更改。现在尝试运行应用程序。
“太好了!”西蒙说。“准备好测试了吗?”
“是的!”艾瑞克说。他打开了常用的地址,http://127.0.0.1/order。
他输入了 Alex 并选择了 chocolate、vanilla 和 cinnamon。他得到了订单。

“现在我们必须检查是否将这个订单保存到了文件中,”西蒙说。
他在 mu_code 文件夹中找到了 orders.json 文件,并用文本编辑器打开了它。“是的!”他说。“我们的程序已经保存到文件中!看——第一个订单是我们测试 save_order() 函数的那一天。但第二个订单是你刚刚添加的。非常好!”

“但菜单文件怎么办?”艾瑞克问。
“有什么菜单文件吗?”
“记住,”埃里克解释道,“你告诉我商店经理可能不知道 Python。所以,我们用纯文本文件创建了我们的菜单。现在我们的菜单是 HTML 格式,但我认为商店经理可能也不知道 HTML。我们能否用这个网络应用做同样的事情,让商店经理在想要添加菜单项时编辑文本文件而不是 HTML?”
“好主意,埃里克!”西蒙回答。“我认为使用 HTML 模板是可能的,但我需要检查 Flask 文档。但在你走之前,让我们快速回顾一下今天学到的内容。”
“你解释了计算机中文件的工作方式。我之前不知道这些,”埃里克说。
“我学到了 HTML 表单中的input字段,”艾米丽说。“能够在实际应用中工作,而不是在课堂练习中,感觉真是太棒了。”
“我同意,”西蒙说。“我认为学习某样东西的最佳方式就是参与一个真实的项目。我们今天已经做了很多,所以让我们休息一下,明天再开始着手埃里克的点子。”
今天你学到的新知识
-
网络服务——网络服务是一个持续运行并响应请求的程序,就像咖啡店的新订单一样。
-
为什么关闭文件很重要——当你与文件一起工作时,Python 会将更新保存在内存中。如果你想确保你的数据被永久保存到驱动器上,你应该调用
f.close()。
本章代码
你可以在这里找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch11.
12 数据库:我们需要良好的存储
本章内容
-
西蒙建议为应用程序创建一个单独的文件夹
-
埃里克和艾米丽将应用程序文件移动到新的文件夹结构
-
埃里克重用了读取文本文件的函数
-
埃里克和艾米丽学习数据库
-
西蒙帮助艾米丽和埃里克将应用程序改为使用数据库
“让我们让我们的项目更正式一些,”西蒙在朋友们下次见面时说。
“昨天,你提到了数据库的事情,”埃里克说,“你是这个意思吗?”
“是的,也包括这个,”西蒙回答,“但首先,我建议我们修改文件夹结构。让我们整理一下我们的文件。看看我们的文件和文件夹。”
他拿了一张纸,开始画图并解释。
“当我们开始使用 Mu 编辑器时,它把所有的程序都放在了它的默认文件夹中,即mu_code。然后我们发现它还在mu_code文件夹内的templates文件夹下提供了所有必要的网络应用程序模板。如果你在文件管理器中查看——macOS 上的 Finder 或 Windows 上的资源管理器——你会发现它还有更多的文件夹,比如static、images、fonts等等。记住,我们已经自己创建了一些模板,order.html和print.html,并将它们放在了templates下。如果你查看static文件夹,你会找到css文件夹——我想你知道它是用于样式表的——那里有几个文件。

“你看,我们的应用程序在不同的文件夹中有几个文件。我建议我们为咖啡店应用程序创建一个自己的文件夹,并将所有需要的文件复制到这个文件夹中。这也有助于我们将基于网络的咖啡店应用程序与我们之前的版本分开。将不同的项目保存在不同的文件夹中始终是一个好的做法。”
西蒙继续说,“让我们为所有我们的项目创建一个文件夹——包括未来的项目。通常,我把它命名为Projects,类似于我们已有的Downloads、Pictures和Documents文件夹。在Projects下,我们将创建另一个文件夹,命名为coffeeshop。我们将把所有与我们的基于网络的咖啡店项目相关的文件移动到它里面。我还建议将first_app.py重命名为app.py。这就是人们通常称呼他们的 Flask 应用程序的方式。让我给你展示一下我的意思。”西蒙在他之前创建的图表上添加了另一个图表。

“你知道怎么操作吗?”西蒙问艾米丽和埃里克。
“是的,”艾米丽说。他们开始创建文件夹并移动文件。过了一会儿,他们向西蒙展示了他们的coffeeshop项目文件夹。
“这看起来怎么样?”埃里克问道。
轮到你了! 创建你的应用程序文件夹
为你的项目创建一个文件夹(你可以使用Projects或其他名称)。为你的应用程序创建另一个文件夹。根据你的应用程序命名它——可以是icecream或pizzaplace。将与网络应用程序相关的文件从mu_code移动到这个新文件夹中。

“让我看看,”西蒙看着他们的文件夹说,“你把包含所有内容的static目录和templates目录都移动了,很好。你甚至把fonts和images也移动了——我们现在不用它们,但没关系。”
“我有个问题,”艾瑞克说,“你为什么有时说‘目录’有时说‘文件夹’?我想它们的意思是一样的,对吧?”
“抱歉,我并不严格遵守术语,”西蒙说,“你说得对,这两个词的意思是相同的:用来存放文件的东西。它们用于组织属于某个项目的文件,比如coffeeshop;或者用于特定类型的文件,比如templates。从历史上看,在 Unix 和 Linux 系统中,它们被称为目录。MacOS 基于 Unix,在我的机器人俱乐部里,我们使用 Linux,所以我习惯于称它们为目录。在 Windows 中,它们被称为文件夹——这完全一样。只需习惯不同的术语,这没什么大不了的。”
“还有,昨天你说我们可以把菜单保存在文本文件里,就像我们之前的版本一样,”艾瑞克说,“为了那些不知道 HTML 的商店经理记住?”
“谢谢提醒我,艾瑞克,”西蒙说,“我今天要给你展示如何使用数据库。但这是一个严肃的主题。我们先从这些菜单文件开始热身吧。你能把它们从mu_code复制到你的新目录里吗?”
“是的,稍等一下,”艾瑞克说。他将drinks.txt、flavors.txt和toppings.txt文件复制到了coffeeshop项目文件夹中。
轮到你了! 移动菜单文件
将菜单文件移动到您刚刚创建的新文件夹中。
“现在看看我们的旧程序,看看我们是如何处理这些文件的,”西蒙说,“打开menu_files.py文件,刷新一下你的记忆。试着向艾米丽解释一下——这是更好地理解事物的最佳方式。”
“是的,我很想了解你之前对这个程序做了什么,”艾米丽说。
艾瑞克看着代码,开始解释,“我们创建了一个名为read_menu()的函数,它会读取包含饮料、口味或配料文本文件的列表,并创建一个菜单选择列表。我们将这个列表传递给menu()函数。menu()函数打印列表,并要求顾客选择饮料,然后是口味,然后是配料。所有这些完成后,我们打印出订单。”
列表 12. 1 menu_files.py: 从文件中读取菜单项
def read_menu(filename):
f = open(filename)
temp = f.readlines()
result = []
for item in temp:
new_item = item.strip()
result.append(new_item)
return result
drinks = read_menu("drinks.txt")
flavors = read_menu("flavors.txt")
toppings = read_menu("toppings.txt")
drink = menu(drinks)
flavor = menu(flavors, "Erik's flavors", "Choose your flavor: ")
topping = menu(toppings, "Erik's toppings", "Choose your topping: ")
print("Here is your order: ")
print("Main product: ", drink)
print("Flavor: ", flavor)
print("Topping: ", topping)
print("Thanks for your order!")
“我明白了,”艾米丽说,“什么是列表?它像 JavaScript 中的数组吗?”
“是的,完全正确,”西蒙确认道,“这和之前一样:按顺序排列的一组项目。在不同的编程语言中,它有不同的名称,但本质上是相同的。最重要的是,你可以通过列表中的位置来选择项目:比如第三项或第五项。这些数字被称为列表的索引。”
“我们可以再次使用这个函数吗?”艾瑞克问道。
“当然!” 西蒙说。“这就是函数被创建的原因——为了重用!让我们将这个 read_menu() 函数复制到我们的新 app.py 文件中。将其放在 load_orders() 函数之后。还要复制紧随函数之后的三次函数调用。我们在这里也需要创建这些列表。”
埃里克将 menu_ files.py 中的 read_menu() 函数及其后面的三行代码从 coffeeshop 目录的 app.py 中复制过来。
“现在我们有这三个文件的菜单选择列表:drinks.txt、flavors.txt 和 toppings.txt。我们必须在我们的订单页面上显示这些选择。好事是,我们的 order.html 文件不是一个普通的 HTML 文件,而是一个 模板。我们可以在模板中使用 变量 并传递我们想要显示的 值。记住,我们在 print.html 模板中就是这样做的,当时我们传递了 new_order 字典。让我提醒你。这是 app.py 中的 Python 代码:
return render_template(
"print.html", new_order=new_order
)
“这里是 order.html 模板中使用 new_order 字典的部分,” 西蒙继续说。
. . .
<h2>Here is your order:</h2>
<p>Drink: <strong>{{ new_order["drink"] }}</strong></p>
<p>Flavor: <strong>{{ new_order["flavor"] }}</strong></p>
<p>Topping: <strong>{{ new_order["topping"] }}</strong></p>
. . .
“现在我们应该使用 order.html 模板,并将带有选择的列表传递给它。在模板中,我们将使用它们来构建菜单。找到使用 order.html 模板的 render_template() 函数调用,并以与上面几行传递 new_order 字典相同的方式传递这三个列表。”
埃里克找到了那行代码(它是程序的最后一行)并将其更改为以下内容:
return render_template("order.html", drinks=drinks, flavors=flavors, toppings=toppings)
“好,” 西蒙说。“现在,在我们将列表传递给模板之后,我们必须更改模板。艾米丽,你会感到惊讶的。它不会看起来像普通的 HTML。我们将在模板中使用 循环。”
“像在 Python 中一样吗?” 埃里克说。
“是的,它们看起来非常相似,并且工作方式也类似,” 西蒙说。“我们不会将选项作为字符串打印到我们的 select 菜单中,而是从列表中获取它们。我们的循环将看起来像 for d in drinks——就像你在你的 menu() 函数中所做的那样。我想这是因为模板语言是由创建 Flask 的同一人创建的——Armin Ronacher。所以它非常接近 Python,就像你可以想象的那样。顺便说一句,模板语言被称为 Jinja——是的,就像忍者。
“让我帮你处理 order.html 模板,” 西蒙继续说。“我收集了一些示例,所以我会在这里使用它们。”
“所以你自己并不了解 所有 东西?” 埃里克对他的兄弟眨了眨眼。
“当然不是,” 西蒙严肃地回答。“我自己也在学习。当我需要什么的时候,我会上网搜索示例和文档,并使用它们。幸运的是,Flask 和 Jinja 文档写得很好,非常有帮助。此外,还有很多人分享的示例和教程:”
西蒙将 templates/order.html 文件更改为以下内容:
列表 12.2 order.html:在模板中使用菜单列表显示菜单项
. . .
<form action="/order" method="post">
<input type="text" name="name" placeholder="ENTER NAME">
<select name="drink">
<option value="">- Choose drink -</option> ❶
{% for d in drinks -%} ❷
<option value="{{ d }}">{{ d | capitalize }}</option> ❷
{% endfor %} ❷
</select>
<select name="flavor">
<option value="">- Choose flavor -</option>
{% for f in flavors -%}
<option value="{{ f }}">{{ f | capitalize }}</option>
{% endfor %}
</select>
<select name="topping">
<option value="">- Choose topping -</option>
{% for t in toppings -%}
<option value="{{ t }}">{{ t | capitalize }}</option>
{% endfor %}
</select>
<input type="submit" value="Submit">
</form>
. . .
❶ 指令选项
❷ 一个 for 循环包括菜单中的选项。
轮到你了! 修改 order.html
以相同的方式更改你的templates/order.html模板。复制文本时要小心——注意我们在模板中使用的是双花括号。不要忘记所有的破折号和百分号。
他们测试了页面,它和以前一样工作。西蒙建议,“艾米丽,试着在其中一个菜单文本文件中添加一些内容,看看页面是否更新。”
艾米丽打开了toppings.txt文件,在文件末尾添加了vanilla powder。她保存了文件并重新启动了应用程序。果然,他们在菜单中看到了新的配料项目。

轮到你了! 测试你的页面
使用新的模板测试你的页面。它看起来和以前一样吗?使用文本编辑器在其中一个文件中添加一个新项目。再次测试菜单,并检查新项目是否显示。
“西蒙,我看到你在模板中添加了那个capitalize单词,”她说,“现在它显示的是Vanilla powder,首字母大写的‘V’。真酷!”
“是的,”西蒙说,“你可以用模板做很多有趣的事情。这只是其中之一。如果你想的话,我们稍后可以学习其他内容。但现在我们将学习数据库!
“你知道为什么我们需要数据库吗?”西蒙问,“我们使用 JSON 文件来存储我们的订单,它工作得相当不错,对吧?但想象一下,如果我们想在订单列表中找到所有香草口味的订单,或者所有名为 Alex 的客户的所有订单。或者找出这个月我们是否比上个月使用了更多的薄荷味。我们自己可以编写这样的函数,对吧?但结果是,像这样的问题非常常见,很久以前人们就开始创建特殊的程序,这些程序不仅存储像订单和客户这样的数据,而且还可以回答这样的问题。这些问题通常在数据库世界中被称为查询。多年前,一些聪明的人创造了一种名为结构化查询语言的语言,简称SQL。人们通常读作sequel。
“我们今天不会学习 SQL,”西蒙继续说,“但我告诉你这个的原因是我们将使用一个名为 SQLite 的数据库——如果你想的话,可以称之为sequelight。”
“我们是不是要用数据库而不是 JSON 文件来加载和存储订单?”艾瑞克问。
“是的,”西蒙回答,“但我们不再需要加载订单了。记住,我们当时必须将订单加载到列表中,以便能够向该列表添加新订单,并在完成后将列表保存到 JSON 文件中?现在数据库会将其所有订单保存在自己的文件中。数据库知道如何将新记录添加到订单列表中,所以我们不必担心这一点。”
“我喜欢,”艾瑞克说,“我们现在该做什么?”
西蒙开始画图。“我们将使用一个名为orders.db的数据库文件。我们不再需要load_orders()函数。”他用红笔划掉了它。
“我们不需要订单列表——数据库会为我们保存。我们还将重写我们的save_orders()函数。每次我们有一个新订单时,我们不需要在文件中保存所有订单。我们只需将新订单保存在数据库中——所有之前的订单已经在那里了。我建议将函数重命名为save_order()。”
“看看这个图,看看我们以前使用 JSON 文件的老方法和新方法使用数据库之间的区别。”

“每次我们想要保存一个新订单时,我们都会创建一个连接到数据库。然后我们发出一个特殊的命令来插入我们的新订单到数据库中。之后,我们调用一个特殊的commit()函数来确保我们的订单已保存在数据库中。”
“就像我们处理文件时做的那样,”埃里克说。
“是的,非常相似,”西蒙同意。“区别在于现在我们与数据库交流,而不是与文件交流。我们正在开发一个非常简单的程序,但如果我们咖啡店有不止一个咖啡师接受订单呢?如果几个人同时向同一个文件写入,会引发很多问题。当我们与数据库一起工作时,数据库会处理这些问题。更好的是,如果你学会了如何使用 SQL 语言与 SQLite 一起工作,你还可以使用该语言与许多其他数据库一起工作。它们会很好地理解 SQL。
“理论够多了,让我们写一些代码吧,”西蒙说。“首先,我们应该在我们的数据库中创建一个表。在我们的例子中,这个表非常简单——它包含顾客的姓名和我们常用的选项:饮料、口味和配料。”
他开始编辑app.py文件并解释。“我在开头添加了import sqlite3行。然后我移除了orders = load_orders()函数调用和整个load_orders()函数。在app = Flask()行之前,我添加了三条使数据库建立连接并创建名为orders的表的语句。”
. . .
import sqlite3 ❶
. . .
con = sqlite3.connect("orders.db") ❷
cur = con.cursor()
cur.execute(
"CREATE TABLE IF NOT EXISTS orders(name, drink, flavor, topping);") ❸
app = Flask(name)
. . .
❶ 导入 sqlite3 包
❷ 连接到数据库
❸ 创建订单表
轮到你了! 添加命令以连接到数据库并创建一个表
修改你的app.py文件并添加西蒙所做的三条语句。别忘了在你的app.py文件开头添加import sqlite3。
“cur是什么?”艾米丽问。
“在数据库领域,这被称为游标。就像你的鼠标游标一样,它可以指向数据库中的特定位置。我们不会深入细节——在这种情况下,我正在遵循官方 Python 文档中的示例。文档说我们需要这个游标来在数据库中执行 SQL 命令。
“看看下一行中的cur.execute()。这是我们第一个 SQL 指令。它创建了一个名为orders的表,其中包含名为name、drink、flavor和topping的列。在数据库中,表只是带有行和列的正常表格。每个新订单都是表中的一个新行。在每一行中,我们都有四个列,列名就是我刚才提到的。简单,对吧?”他画了一个简单的表格。

“现在让我们重写 save_orders() 函数。首先,我们将将其重命名为 save_order(),因为我们一次只保存一个订单。然后,我们将重复我之前写的两行来创建连接和创建游标。之后,我们可以执行 SQL 命令。在这种情况下,我们将使用 INSERT 命令并传递来自 order[] 字典的值。最后,我们将调用 commit() 函数以将新订单保存到数据库中。”
列表 12.3 app.py: 向表中添加订单
. . .
from flask import Flask, render_template, request
def save_order(order):
con = sqlite3.connect("orders.db")
cur = con.cursor()
cur.execute(
"INSERT INTO orders(name,drink,flavor,topping) VALUES(?,?,?,?);",
(order["name"], order["drink"], order["flavor"], order["topping"]),
)
con.commit()
return
. . .
你的回合! 创建一个 save_order() 函数
将 save_order() 函数复制到你的程序中。注意引号、方括号、逗号和括号。
“我知道,看起来很复杂,但如果你只是阅读它,理解起来相当简单。你告诉数据库将一些 VALUES 插入到 orders 数据库中。然后你在括号中列出这些值。你已经熟悉字典了,也知道如何为某个特定的 key 提取 value。”
“你为什么要把一些词写成大写?”埃里克问道。
“这是 SQL 中的一个约定。人们使用大写字母来表示 SQL 命令和关键字,如 INSERT 和 INTO。对于变量,如表名 orders,人们使用小写字母。我尝试使用小写字母来表示命令,也成功了。所以大写字母只是一个可读性约定,用于将 SQL 词汇与变量区分开来。”
“让我们试试!”埃米莉说。
“等等!”西蒙说。“我差点忘了——我们应该从 order() 函数中删除 load_orders() 调用。我们也应该重命名保存函数。”
他编辑了 order() 函数,使其看起来像这样:
列表 12.4 app.py: 删除 load_orders()
@app.route("/order", methods=("GET", "POST"))
def order():
if request.method == "POST":
new_order = {"name": request.form["name"],
"drink": request.form["drink"],
"flavor": request.form["flavor"],
"topping": request.form["topping"]
}
save_order(new_order)
return render_template(
"print.html", new_order=new_order
)
return render_template("order.html", drinks=drinks,
flavors=flavors, toppings=toppings)
你的回合! 编辑你的 order() 函数
按照西蒙的方式编辑你的 order() 函数。别忘了将 save _orders()(复数)改为 save_order()(单数)。
他保存了文件,说:“现在准备好了。埃米莉,运行它!但让我先填写订单,好吗?”
埃米莉点击了运行并重新加载订单页面。西蒙输入了菜单选项。
你的回合! 测试它
测试你的程序。现在它使用数据库而不是 JSON 文件,但界面应该相同。

“成功了!”西蒙如释重负地说。他自己也不确定是否做对了所有的事情,而且他不想表现出来。
“我们能检查订单是否已保存到数据库中,就像我们处理 JSON 文件那样吗?”埃里克问道。
“好问题!”西蒙说。“不,数据库文件不是一个我们可以读取的文本文件。但我们能做得更好。我们可以创建一个新的函数,它将显示网页上的所有订单。”
“那太好了!”埃里克说。“例如,如果一位商店经理想看看今天我们使用了多少份焦糖。”
“而且我们可以看到某个特定客户喜欢订购什么,”埃米莉补充道。
“好主意,”西蒙说。“你准备好工作得更深入一些了吗?我们需要三样东西。首先,记住为了打开订单页面,我们在网站地址中添加/order。在 Flask 中,这被称为路由。我们应该为一个新的路由创建一个函数,我们可以使用它来列出订单。
“让我们称这个路由为/list。这个路由的函数也将被称为list()。它与order()函数非常相似。在这个list()函数的末尾,我们也将调用render_template()函数来显示订单列表。
“其次,”他继续说,“我们将创建一个函数,它将与数据库通信并将订单列表返回给list()函数。让我们称它为get_orders()。
“第三,我们应该创建一个模板来显示订单。艾米丽,你学过如何在 HTML 中创建表格吗?”
“表格?是的,我能做到,”艾米丽说。“我们应该使用<table>标签来创建表格,然后为每一行使用<tr>标签,为每一行中的每个单元格使用<td>标签。”
“太好了!”西蒙说。“那么你将为我们创建list()函数的模板。埃里克,你写函数,好吗?”
“当然,”埃里克说。“我应该从哪里开始?”
“在save_order()函数之后立即创建一个名为get_orders()的函数。从save_order()函数中的相同第一行开始。在其后添加一行:con.row_factory = sqlite3.Row。这是一条特殊指令,允许我们在处理结果时使用列名。现在不用担心它——你稍后会看到。然后,使用之前的相同行创建一个游标:cur = con.cursor()。让我看看你的代码。”
埃里克展示了他所写的:
def get_orders():
con = sqlite3.connect("orders.db")
con.row_factory = sqlite3.Row
cur = con.cursor()
“很好,”西蒙说。“现在重要的部分:我们必须从名为orders的表中获取所有订单。我们将使用最常用的 SQL 指令之一,它告诉数据库给我们表中的所有行和所有列。它看起来像这样:SELECT * FROM orders;。星号*表示‘所有内容。’
“我们应该使用与在save_order()中使用的相同的cur.execute()函数。执行指令后,我们应该从数据库中检索结果。您需要使用cur .fetchall()函数将结果存储在名为rows的列表中。然后,您将从这个函数返回行列表。
“别担心,这是我从这里获取文档的页面。把它作为例子使用。”西蒙展示了他在浏览器中打开的标签:docs.python.org/3.8/library/sqlite3.html。
埃里克开始输入。几分钟后,他向西蒙展示了他的代码:
列表 12.5 app.py: get_orders()函数
def get_orders():
con = sqlite3.connect("orders.db")
con.row_factory = sqlite3.Row
cur = con.cursor()
cur.execute("SELECT * FROM orders;")
rows = cur.fetchall()
return rows
轮到你了! 创建一个get_orders()函数
创建一个像埃里克刚刚做的那样get_orders()函数。
“看起来不错,”西蒙说。“我们测试时就会看到。
“现在我们必须添加另一个处理/list路由的函数。我们应该在处理/order的order()函数之后添加它。让我来帮你。”他在文件的末尾开始编写函数。
“首先,我使用@app.route()指定我们将要处理的路由。然后,我以通常的方式定义函数。函数本身很简单:我只是调用get_orders()并将结果传递给render_template()。正如你将记得的,这个函数使用我们传递给它的模板在浏览器中显示数据。”
列表 12.6 app.py:创建 list() 函数
@app.route("/list", methods=["GET"])
def list():
orders = get_orders()
return render_template("list.html", orders=orders)
轮到你了! 创建一个 list() 函数
创建一个像埃里克那样做的 list() 函数。
“为什么在这个函数中我们没有从数据库中正确获取所有订单?”埃里克问道。“你为什么让我写一个单独的函数?”
“好问题,”西蒙说。“我正在尝试将处理用户的函数与处理数据的函数分开。有时人们称之为前端和后端。如果我们想改变我们处理数据的方式,我们必须改变get_orders()函数。例如,假设我们想要能够只获取带有焦糖风味的订单。我们会将这个请求传递给函数,它将以与之前相同的方式返回订单列表。我们的list()函数将显示从get_orders()获取的任何内容。它不在乎是完整的订单列表,还是只是部分订单——它只是显示它们。另一方面,get_orders()函数不关心订单将如何显示。它只是返回订单列表。
“这个想法在计算机科学中通常被称为关注点分离。我会给你一篇关于这个话题的维基百科文章,稍后阅读。
注意:关于关注点分离的维基百科文章在这里:en.wikipedia.org/wiki/Separation_of_concerns。
“唯一剩下的事情是列表模板,”西蒙说。“艾米丽,让我们在编辑器中创建一个新文件。删除穆为我们创建的示例网络应用程序。将你的新文件保存在templates目录下,文件名为list.html。别忘了将文件类型从‘Python (.py)’更改为‘Other (.*)’。

“从我们在print.html模板中使用的相同模板行开始。前两行和最后一行应该是相同的。”
艾米丽复制了这三行:
{% extends "base.html" %}
{% block content %}
{% endblock %}
“现在创建一个包含一个空行和空单元格的表格。每一行将有四个单元格和四个列标题。那将是第一行。你知道如何创建列标题吗?”西蒙问艾米丽。
“是的,使用<th>标签,”她回答。她开始在templates/list.html文件中创建一个 HTML 表格。
“现在你可以添加标题了,”西蒙说。“将有名称、饮料、风味和配料。”
列表 12.7 带有标题的 templates/list.html 模板
{% extends "base.html" %}
{% block content %}
<table>
<tr>
<th>Name</th>
<th>Drink</th>
<th>Flavor</th>
<th>Topping</th>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</table>
{% endblock %}
“太好了,”西蒙说,“现在让我添加循环。我们将在这里使用与显示菜单相同的for循环。记住,我们将名为orders的列表传递给这个模板。循环将遍历这个列表,就像这样:for o in orders。在每个单元格中,在<td>和</td>之间,我们将插入每个键的值:name、drink、flavor和topping。”西蒙修改了模板:
列表 12.8 带有for循环的templates/list.html模板
{% extends "base.html" %}
{% block content %}
<table>
<tr>
<th>Name</th>
<th>Drink</th>
<th>Flavor</th>
<th>Topping</th>
</tr>
{% for o in orders -%}
<tr>
<td>{{ o['name'] }}</td>
<td>{{ o['drink'] }}</td>
<td>{{ o['flavor'] }}</td>
<td>{{ o['topping'] }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}
你的机会! 创建一个列表模板
创建一个像艾米丽刚刚做的templates/list.html文件。注意引号、方括号和双大括号。
“是时候尝试列表函数了!”西蒙在保存文件后说,“启动应用程序并输入两三个订单。”
埃里克启动了应用程序,打开带有127.0.0.1:5000/order的标签页,并输入了三个订单:为西蒙、为艾米丽和为自己。
“现在,将地址中的/order路由改为/list,”西蒙说。
埃里克做了,他们看到了结果。

“看起来很棒,”艾米丽说,“对商店经理来说非常有用!”
“是的,我也喜欢,”埃里克说,“我觉得我们应该给行添加数字,看看我们服务了多少人。”
“好主意!”西蒙说,“而且你可以添加日期和时间,看看你每天服务了多少客户。所有这些都是可能的——我们只需要更改我们的数据库来保存这些信息。但我们稍后再做。明天,我还有一个想法。艾米丽,你碰巧知道 CSS 吗?”
“我们在课堂上学到了一点,”艾米丽回答。
“CSS 是什么?”埃里克问。
“这是一种改变浏览器中 HTML 页面外观的方法,”艾米丽解释说,“我认为它代表的是Cascading Style Sheets。”
“完全正确,”西蒙说,“明天,让我们用 CSS 让我们的网站看起来更好。但在你走之前,让我们回顾一下今天我们做了什么。”
艾米丽开始说,“我们创建了一个新的项目目录,并将所有内容都移动到里面。我们将主应用程序文件重命名为app.py。”
“我们像在之前的版本中一样使用文本文件作为菜单项,”埃里克补充说,“现在商店经理可以简单地编辑这些文件,而不是更改程序。”
“然后我们开始使用数据库,”艾米丽说,“这并不容易。”
“是的,我同意,”西蒙说,“但这值得。有了数据库,我们的程序更专业了。现在是我们让它更美观的时候了。我们明天会处理这件事。”
今天你学到的新东西
-
文件夹和目录——在不同的操作系统中,人们用不同的术语来表示相同的东西。文件夹和目录都是属于某个项目或特定类型文件的文件集合。
-
Jinja 模板——Jinja 模板被 Flask 用于创建 HTML 页面。你可以将变量、列表和字典传递到模板中,就像在 Python 中使用它们一样。你可以在模板中使用
for循环,就像在 Python 中一样。 -
数据库—数据库是一个可以存储数据并对数据进行各种操作的程序。数据库可以使用特定标准搜索记录或总结数据(例如,本月的香草口味订单数量)。
-
SQL—SQL 代表结构化查询语言,用于与数据库交互。
-
前端和后端—应用程序的前端部分与用户交互:它显示页面并从键盘和菜单获取输入。后端部分与数据交互:它存储和从文件和数据库中检索数据。前端和后端部分用于在处理大型应用程序时分离关注点。
本章代码
您可以在此处找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch12。
13 种样式:让它变得漂亮
在本章中
-
埃里克和艾米丽尝试不同的样式、颜色和字体
-
埃里克和艾米丽为商店创建了一个新的主页
-
埃里克和艾米丽给他们的页面添加图片
“让我们让它变得漂亮!”当朋友们下次见面时,西蒙说。
“你是说——在我们的 HTML 文件中使用样式?”艾米丽问道,“我们在 HTML 课程中使用了样式。你可以更改颜色、字体大小和背景。这很酷!”
西蒙说:“是的,让我们谈谈风格。艾米丽,你能解释一下吗?”
“它被称为 CSS。它代表……,”她停顿了一下,回忆起来,“它代表级联样式表,对吧?”然后她看着西蒙,“但说实话,我不记得为什么了。”
“你说得对!”西蒙说,“这就是级联样式表!它们被称为样式表,因为它们只是描述样式的纯文本文件。样式指定了你的 HTML 元素在页面上的外观。例如,你可以指定你的<h2>标题应该有 36 像素的大小,应该是红色。或者它们应该使用特定的字体。”
他继续说:“网页设计中的一个理念是分离内容和样式。当然,我们可以在 HTML 文件中设置样式。但这不是很方便。如果你想更改字体大小或颜色,你不得不浏览所有你的 HTML 文件并更改所有的<h2>标题。相反,我们在一个单独的文件中定义样式,我们说所有<h2>标题都应该使用这种颜色和大小。然后,如果我们想更改它,我们只需要在一个地方更改。非常方便,不是吗?”
“我明白了,”埃里克说,“就像 Python 中的函数一样。你写一次,然后在其他地方使用它。”
“是的,当然!”西蒙说,“我喜欢你的类比。所以,我们弄明白了为什么它们被称为样式表,但为什么是级联的呢?对于你的网站或应用程序,你可以有多个样式表,并且一个接一个地组合它们。通常,人们会使用别人创建的样式表,只添加或更改他们想要看起来不同的元素。这与函数的另一个相似之处。”
他继续说:“在我们的项目中,我们将使用 Mu 编辑器中包含的样式表,并且我们会稍作修改,使其更适合我们的咖啡店项目。看看你的订单页面,想想我们应该更改什么。”

埃里克看着屏幕说:“我们需要一个标题。就像我们在终端应用程序中一样:欢迎来到埃里克的咖啡店!”
“等等!”艾米丽说,“这已经不再只是你的咖啡店了。我觉得它应该被称作埃里克和艾米丽的咖啡店。或者,简称 E&E 咖啡店。同意吗?”
“我认为这现在是一个团队项目,E&E 是一个很好的名字,”西蒙说。
“是的,让我们把它做成 E&E 咖啡店,”埃里克同意了。
“让我添加一下,”艾米丽说。她从templates文件夹中打开了order.html文件。她添加了一个标题,所以现在文件的开始看起来是这样的:
{% extends "base.html" %}
{% block content %}
<h1>Welcome to E&E Coffee Shop</h1>
<form action="/order" method="post">
<input type="text" name="name" placeholder="ENTER NAME">
. . .
艾米丽保存了文件,切换到 Mu 编辑器的app.py标签页,并点击运行。她打开了浏览器标签页,再次输入了 URL:http://127.0.0.1:5000/order。

“看起来不错!”埃里克说。
“西蒙,你说我们可以更改标题中的字体,”艾米丽说。“这个看起来有点……无聊。我觉得我们需要一些更花哨的。”
“我同意,”西蒙说。“幸运的是,有很多免费的网页字体我们可以使用。有些字体需要付费,但,对于这个项目来说,免费的字体就足够了。例如,Google Fonts 是一个很好的资源。”
他在浏览器中打开了一个新标签页,并输入了地址:fonts.google.com。“正如你所见,有超过 1,400 种字体可供选择。你还可以找到适合不同语言的字体。看到这个写着‘输入一些内容’的字段吗?输入你的页面标题,你将看到它在不同字体中的样子。”
“关于‘类别’?那是什么意思?”艾米丽问道。
“如果你点击那个菜单,你会看到衬线体、无衬线体、显示体、手写体和等宽体。等宽体通常用于程序代码。如果你想为朋友打印一张贺卡,可以使用手写体。显示体意味着它只用于计算机显示器,不用于打印。衬线体和无衬线体听起来很奇怪,但它们实际上代表了两种你非常熟悉的字体类别。衬线体意味着字体在字母的末尾有小的笔画。当然,你知道 Times New Roman,这是大多数程序和文档的默认字体。尝试只从类别菜单中选择衬线体,你就会明白我的意思。”

“我喜欢这个 Playfair Display!”艾米丽说。
“好!”西蒙说,“记下来,我们回头再讨论。我只是想让你看看无衬线字体的样子。‘Sans’意味着‘没有’;在这种情况下,这意味着字体没有我之前提到的那些笔画。你很可能见过这个类别的 Arial。看这里,”然后他更改了类别为无衬线体。

“像这样的字体用于菜单和其他应用程序界面细节。只需四处看看,你很快就会开始注意到并识别出这些两种字体类别在网络上和应用程序中的无处不在。如果你想了解更多关于字体的信息,点击知识标签。相信我,这非常有趣!”
轮到你了! 了解更多关于 Google Fonts
打开fonts.google.com页面,四处看看。尝试找到有趣的字体,并搜索不同的类别。请注意,你可以以几种不同的方式对输出进行排序:趋势、最受欢迎、最新等。打开你最喜欢的网站,并尝试找出它们在哪里使用衬线体、无衬线体和等宽体字体。
“这些字体看起来不错,但我更喜欢 Playfair 字体。我认为它适合我们的页面标题。埃里克,你怎么看?”艾米丽问道。
“是的,我也喜欢。它不无聊,”埃里克说。“而且我喜欢他们制作的&符号。是的,它适合标题。”
西蒙切换回衬线类别并点击了 Playfair Display 字体。字体页面打开了。西蒙解释说:“在这里你可以找到有关如何在你的网页或应用程序中使用此字体信息的说明。”
他滚动页面,直到他们使用的标题出现在样式标题下方。“在这里你可以选择一种字体粗细。正如你所见,相同的字体,或者说字体样式,可以有不同‘粗细’。我只选择了一个,这将给我们所需的信息。注意页面右上角的小红点。这意味着我们已经为我们的收藏夹选择了某些内容,我们应该点击它来打开说明。”

他点击了带有红色点的图标,并显示了他们需要的说明。

“看看右边的块,在“在网页上使用”下。谷歌给了我们很好的说明,如何使用此字体。我们只需要将这些行复制到我们的文件中。在第一段代码中,它建议使用第一个,带有 <link>。第三段代码,称为 CSS 规则以指定家族,展示了我们如何在 CSS 文件中使用此字体。它可能看起来很复杂,但别担心,我会帮你的。
“在 Flask 中有一个好处是我们使用模板,”西蒙继续说。“看看你的 order.html 文件:你会看到它以 {% extends "base.html" %} 开头。你的 print.html 文件也是以这一行开始的。这意味着我们在模板中重用 base.html。我们不需要更改我们项目中的每个文件——我们只需要更改 base.html。是的,埃里克,又像函数一样,”西蒙微笑着。
“艾米丽,你能打开 templates 目录下的 base.html 文件吗?”
当艾米丽打开文件时,西蒙说:“看看带有说明的 Google Fonts 网页。从灰色框中复制以 <link> 开头的三行。然后将它们粘贴到 base.html 中,就在关闭的 </head> 标签之前。”
艾米丽将这一行复制到 base.html 文件中,现在看起来是这样的(我只展示其中一部分):
列表 13.1 templates/base.html: 添加字体
. . .
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/
css2?family=Playfair+Display&display=swap" rel="stylesheet">
</head>
<body>
. . .
轮到你了! 将字体链接添加到 base.html
在编辑器中打开 templates/base.html 文件并做出艾米丽刚刚所做的更改。
“很好。这是第一步,”西蒙说。“第二步是创建我们自己的样式表。在编辑器中点击新建。它将创建一个示例网络应用程序,但我们现在不需要它。删除其中的所有文本并点击保存。你应该将它保存在其他 CSS 文件所在的同一目录下,在 static/css 下。文件命名为 coffeeshop.css。别忘了将文件扩展名从‘Python (.py)’更改为‘其他 (.*)’。”

西蒙检查了 coffeeshop.css 文件是否出现在其他 CSS 文件旁边。“很好,”他说。“现在我们可以将字体规范添加到 h1 标题中。
“看看浏览器字体页面上的“指定字体家族的 CSS 规则”框。从那里选择并复制文本。现在输入h1 {并将你复制的文本粘贴在开括号之后。然后添加闭括号并保存文件。”
这很简单。艾米丽编辑了coffeeshop.css文件,使其看起来像这样:
列表 13.2 static/css/coffeeshop.css:向 CSS 添加字体
h1 {font-family: 'Playfair Display', serif;}
轮到你了! 创建 coffeeshop.css
在编辑器中创建static/css/coffeeshop.css文件,并添加h1样式的行。
“太好了!” 西蒙说。“现在我们有了自己的 CSS 文件。我们只需要告诉 Flask 我们想使用它。这是第 3 步。我想你还在编辑器中打开了base.html,艾米丽?找到带有stylesheet这个词的两行。你会认出这些名字的。”
“是的,我看到了,”艾米丽说。“它们被称为normalize.css和skeleton.css——和我保存样式表时看到的名字一样。”
“很好,现在复制带有skeleton.css的行,并将其粘贴在其后。你应该有两行完全相同的行。现在,在第二行中,将skeleton改为coffeeshop。”
艾米丽照做了,现在base.html文件看起来像这样:
列表 13.3 templates/base.html:添加coffeeshop CSS 文件
. . .
<!-- CSS
–––––––––––––––––––––––––––––––––––––––––––––––––– -->
<link rel="stylesheet" href="/static/css/normalize.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/coffeeshop.css">
. . .
轮到你了! 将coffeeshop.css样式表添加到base.html
在编辑器中打开templates/base.html文件,并做出艾米丽刚才所做的更改。
“现在你可以看到为什么这些样式表被称为级联的。我们有三个样式表。第一个,叫做normalize.css,是确保我们的网站能在不同设备上的各种浏览器上看起来好的必要条件。”
“甚至在我的 iPad 上?”艾瑞克问。
“是的,应该是这样,”西蒙回答。“我们稍后测试一下。”
他继续说:“下一个 CSS 文件叫做skeleton.css。它是与 Mu 编辑器一起提供的,为所有 HTML 元素提供了好看的样式。我们不需要为单个元素创建样式——它们看起来已经很好了。如果我们想改变某些东西,比如标题的字体家族,我们只需要在我们的 CSS 文件中添加样式。样式是按照在 HTML 文件中 CSS 文件的列表顺序依次应用的。这就是为什么它们被称为级联的。”
注意:你可以在这里了解更多关于normalize.css的信息:nicolasgallagher.com/about-normalize-css/。skeleton.css样式表在这里描述:getskeleton.com/。
“理论够多了,让我们看看我们的网站!” 西蒙说。
艾米丽和艾瑞克再次确认他们已经保存了所有文件。然后他们切换到app.py标签页并点击运行。他们打开了浏览器标签页,地址是熟悉的http://127.0.0.1:5000/order,他们看到了更新后的页面。

“哇!我喜欢!” 艾瑞克惊呼。
“现在你知道如何更改网页上的任何字体了,”西蒙说。
“但是为什么它说‘来自 Mu 的问候’?”艾瑞克注意到。“不应该说是‘来自 E&E 的问候’吗?”
“好主意!”西蒙说。“这很容易改变。在base.html文件中找到这个短语并更改它。它应该非常接近开头,被<title>标签包围。”
埃里克迅速找到了这个短语并更改了它。“我应该重启 Python 应用程序吗?”他问西蒙。
“不,你只需要在浏览器中重新加载页面,你就会看到变化。”
的确,页面现在显示了新的标题。

艾米丽说,“我们也可以更改背景色。我觉得我们需要一种看起来像咖啡或卡布奇诺的颜色。”
“好主意!”西蒙说。“首先,你们必须找到一个你们俩都喜欢的好颜色。最简单的方法是打开一个谷歌搜索页面,并输入color picker。”
西蒙就这样做了来演示,他们看到了颜色选择器。

“你可以使用彩虹线来选择基础色,然后使用白色圆圈来挑选你喜欢的色调。
“还有很多其他网站可以帮助你挑选好的颜色组合。这是一个全新的世界——就像字体一样。有很多东西可以学习。例如,”他选择了搜索页面上的第一个结果:htmlcolorcodes.com/color-picker/。“也试试它,你会发现很多关于颜色及其在网页上如何使用的有用信息。”
埃里克和艾米丽花了几分钟时间玩颜色选择器,最终找到了一个颜色。

轮到你了! 玩转颜色选择器
在谷歌中找到颜色选择器并玩玩。弄清楚如何更改基础色,然后如何更改它的明暗。找到你喜欢的背景色。
“好,”西蒙说。“现在复制这些在十六进制下的六个字母和数字,我们将编辑coffeeshop.css文件。这次谁在输入?”
“是的,”埃里克说。
“输入这个:body {background-color:。然后粘贴从颜色选择器复制的部分。确保你包括了井号(#)。然后输入一个分号并关闭花括号。”
埃里克添加了这一行,所以现在 CSS 文件看起来是这样的:
列表 13.4 static/css/coffeeshop.css:为 CSS 添加背景色
h1 {font-family: 'Playfair Display', serif;}
body {background-color: #e8d9b7;}
轮到你了! 更改你的页面背景
编辑你的static/css/coffeeshop.css文件,并添加带有你选择的背景色的body行。
埃里克重新加载了订单页面,现在看起来大不相同。

“我越来越喜欢它了,”艾米丽说。“我们可以把这些菜单也做成同样的咖啡色吗?”
“是的,当然,”西蒙回答。“但我建议使它们的色调稍微亮一些,不要完全相同。继续找找看。”
这甚至更容易。艾米丽和埃里克最终为菜单找到了一个新的色调。

“好的。现在让我帮你把它添加到样式表中,”西蒙说,“我们应该将这种颜色应用到我们表单的元素上。菜单元素被称为select,你输入客户名字的输入字段被称为input[type="text"]。让我们将它们添加到样式表中,并使用你选择的背景颜色:
列表 13.5 static/css/coffeeshop.css:更改菜单颜色
input[type="text"], select {background-color: #ede9df;}
“这些数字和字母是什么意思?它们代表什么?”埃里克问道。
“它们的意思是,你可以用这六个数字和字母描述任何你想要的颜色,”艾米莉解释说,“这六个数字由三个颜色(红色、绿色和蓝色)的三对数字组成。每一对数字说明我们选择的颜色中包含多少这种颜色。这就像颜料一样:你可以混合红色和蓝色得到紫色。每一对符号都是一个数字,但在十六进制中,它们可以是数字或从 a 到 f 的字母。我只是不记得为什么它被称为‘十六进制’。”
“十六进制是十六进制的缩写,”西蒙说,“这是一种表示数字的方法,类似于我们的常规方法,但更短,更容易被计算机理解。在我们的常规系统中,称为‘十进制’,我们有从 0 到 9 的数字,总共 10 个数字。用一个数字我们可以描述从 0 到 9 的数字。用两个数字我们可以描述从 0 到 99 的数字。十六进制系统类似,但不是 10 个数字,我们有 16 个数字。它们从 0 到 f:我们添加了从 a 到 f 的字母,并将它们用作数字。所以,数字 a 代表 10,b 代表 11,以此类推。数字 f 代表 15。用一个十六进制数字,我们可以描述从 0 到 15 的数字;用两个,从 0 到 255。而且它更短:255 在十进制系统中需要三个数字,但在十六进制中只需要 ff。
“让我们回到颜色的话题,”西蒙继续说,“正如艾米莉所说,每个六位十六进制数字都告诉我们特定颜色中红色、绿色和蓝色各占多少。例如,你菜单的颜色#ede9df意味着红色中包含ed,绿色中包含e9,蓝色中包含df。用十进制数字表示,那就是……,”西蒙看着调色板页面,并从那里读出数字,“这意味着红色中有 237,绿色中有 233,蓝色中有 223。这些数字可以增加到 255。如果这三个数字都变成 255,那就是纯白色。”
“而且如果三个都是 0,那就意味着黑色,”艾米莉补充说。
“完全正确,”西蒙确认说,“如果你想了解更多关于网络颜色,可以阅读维基百科上的相关文章。”他迅速找到了网络颜色页面,并在新标签页中打开:en.wikipedia.org/wiki/Web_colors。
“我想从我们的新设计中点些东西,”艾米莉说。她输入了她的订单。然后她点击提交,看到了新页面。

“为什么它没有打印出我们字体中的第二行?”她不太高兴。
“也许是因为我们刚刚设置了h1的字体?”埃里克建议。
“你说得对,埃里克,”西蒙说,“这很容易修复。打开skeleton.css文件,看看那里的样式是如何定义的。我们可以通过查看其他人的样式表是如何实现的来学到很多东西。去找到那个叫做Typography的部分。你会看到如何设置其他标题级别。”
埃里克打开了skeleton.css文件,并滚动到Typography部分。他看到了这个:
列表 13.6 static/css/skeleton.css:Typography部分
/* Typography
–––––––––––––––––––––––––––––––––––––––––––––––––– */
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 2rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
西蒙说,“这里我不详细说明,但你可以看到你可以列出不同的标题级别——h1、h2、h3 等等——用逗号分隔,并将样式应用到所有这些标题上。此外,对于每个标题级别,你可以设置它自己的字体大小和其他参数。你可以将这部分复制到你的coffeeshop.css文件中,并尝试更改各种参数。但重要的是,我们现在要添加h2、h3等等到我们的h1样式。”
“明白了,”埃里克说。他迅速更改了coffeeshop.css的第一行:
列表 13.7 static/css/coffeeshop.css:更改所有标题的字体
h1, h2, h3, h4, h5, h6 {font-family: 'Playfair Display', serif;}
他点击了重新加载按钮,浏览器显示了一个确认消息。

“点击继续,”西蒙说,“这只会再次提交埃米莉的订单。在我们测试时这没问题,但在实际应用中,它会在我们的数据库中创建另一个记录,并生成一个新的订单,所以当这个应用程序在实际咖啡馆运行时,你应该小心。”
现在,应用程序显示了正确的字体。

轮到你了! 更改其他标题的字体
编辑你的static/css/coffeeshop.css文件,将所有标题从h1更改为h6的字体。
“我们还可以添加一张图片,”埃米莉说,“我们在课堂上就是这样做的。我们只需要找到一张不错的图片并下载它。”
“是的,好主意,”西蒙说,“不过要确保你下载的不是任何一张图片。互联网上有很多很酷的图片,但大多数都有所有者,比如摄影师或艺术家,他们不喜欢有人未经提及所有者就把他们的图片上传到自己的网站上。关于这一点有很多规定,不同的所有者会有不同的要求。对我们来说,安全起见,我们应该搜索免费图片,确保我们不侵犯任何人的权利。搜索一下像coffee images free download这样的内容,看看它会给我们带来什么。”
埃米莉和埃里克搜索了几分钟,点击不同的图片,并就它们进行争论。最后,埃里克说,“这张不错。它展示了不同种类的咖啡饮品,装在漂亮的杯子中,上面有配料。”然后他向西蒙展示了页面:www.freepik.com/free-vector/colorful-coffee-cup-set-flat-style_1544839.htm。
“好吧,让我们看看,”西蒙说,“看看页面上的这个区块。”

他点击了“更多信息”,并查看了许可条款。

“看起来不错,”他说。“我们可以使用它,但我们必须使用归属——看到这个Attribution is required短语了吗?让我们看看我们应该做什么。”他点击了如何归属的链接。

“我们必须复制文本并将其添加到我们的页面上的图片旁边。没问题,”他说。他点击了蓝色的复制按钮。
“现在我们得把它加到我们的订单页面上,”他说。他打开了order.html文件,并将文本粘贴到页面底部,就在{% endblock %}行之前。“稍后我们会移动它,当我们放置我们的图片时,”他解释道。
“现在下载这张图片,”他对埃里克说。
埃里克点击了绿色的下载按钮,然后点击了免费下载下拉菜单。然后他选择了 JPG。

图片的文件名1544839_222805-P1KRC2-559.jpg出现在他的浏览器底部。
注意:在你的情况下,文件名可能不同。请在以下代码更改中使用你的文件名。
西蒙说:“现在将这个下载的图片文件从下载文件夹复制到你的项目文件夹。你需要在coffeeshop/static/img/下复制它。你能找到吗?”
“我想可以,”埃里克说。他打开了文件管理程序(他的 MacBook 上的 Finder)。经过几次鼠标移动和点击,他找到了正确的位置。他的项目目录现在包含了图片文件。

“太好了,”西蒙说。“现在我们必须把它放在页面上。再次打开order.html文件。让我帮你,”西蒙说。他在他之前复制的归属之前添加了一行。
order.html文件的底部现在看起来是这样的:
. . .
<img src="/static/img/1544839_222805-P1KRC2-559.jpg"
alt="Coffee drinks" width="100%">
Image by <a href="https://www.freepik.com/free-vector/
colorful-coffee-cup-set-flat-style_1544839.htm">Freepik</a>
{% endblock %}
他保存了文件,并对埃米莉和埃里克说:“现在重新加载页面,看看它是否工作。”
埃米莉点击了重新加载,他们看到了更新后的页面。
轮到你了! 将一张图片添加到你的页面
找一张你喜欢的图片并将其添加到你的订单页面。别忘了检查许可证并遵循关于归属的说明。

“干得好!”西蒙说。“现在看起来像是一个真正的在线商店了。重要的是它不仅看起来很棒,而且还能正常工作!”
埃米莉和埃里克很高兴看到他们辛勤工作的成果。把所有的小部件聚集在一起并不容易,但结果令人印象深刻——这是一个他们自己编写的真正的网络应用。
“来个总结怎么样?”西蒙问道。
“我喜欢使用字体和颜色。这比处理数据库容易,”埃米莉说。“还要感谢你解释了关于十六进制数字的内容——我不知道所有细节。”
“我喜欢谷歌字体页面,”埃里克说。“有这么多漂亮的字体!我可以在我的文档中使用它们吗?我打算制作一张海报。”
“当然!”西蒙回答。“你可以在电脑上安装它们并在 Word 中使用它们。在 Google Docs 中,你甚至不需要安装它们。只需在字体菜单中找到更多字体并使用它们。”
“此外,我们还学习了图片许可和归属权,”艾米丽补充道。“西蒙,你是对的,外面有一个全新的世界。”
“很好的总结,做得好,”西蒙说。“现在我又有了一个想法。你知道什么是 AI,或者人工智能,对吧?”
“是的,所有那些愚蠢的聊天机器人和制作搞笑图片的程序,”埃里克回答。“我们和朋友们尝试了一些。”
“是的,但我是在谈论可以帮助我们的开发者的 AI,”西蒙说。“一些工具可以帮助我们编写函数、改进我们的代码和解释错误信息。”
“是的,我看到了一些演示,”艾米丽说。“他们编写了完整的可工作程序。看起来就像魔法一样。西蒙,这意味着 AI 可以编写我们刚刚编写的应用程序吗?”
“可能吧,”西蒙说。“但是,它仍然需要人类的帮助。首先,你必须想出一个应用程序的想法,就像埃里克几周前做的那样。其次,你必须向 AI 解释你想要它做什么。你必须非常具体,否则 AI 会做一些你意想不到的事情。第三,你必须审查代码并确保它按你的意愿工作。当然,据我所知,没有 AI 可以像你刚才和埃里克做的那样选择你喜欢的网站颜色、字体和图片。这是一个品味问题,而且 AI 无法做到。”
“AI 可以是一个巨大的帮助,”他继续说,“但到目前为止,它还不能完全取代人类开发者和设计师。我们明天试试看 AI 能帮我们做什么。”
今天你学到的新知识
-
不同的字体类型——衬线字体、无衬线字体和等宽字体是你可以用在你的应用程序中的不同字体类型。衬线字体适合标题和书籍及文档文本。无衬线字体用于菜单,但也用于标题和文本。等宽字体用于演示程序代码、文件名和命令示例。
-
颜色代码——颜色可以用十六进制数字来描述,例如
#a1d4c5。它们由三种基本颜色组成:红色、绿色和蓝色。你可以使用这些颜色代码来指定网页的背景、菜单、标题和文本颜色。 -
图片有所有者和许可证——重要的是要知道,你在网上找到的并非所有图片都可以自由地用在你的网页上。图片和照片有所有者,他们决定你如何使用他们的作品。通常,你可以使用他们的图片而不必支付费用,但你应该在你的页面上放置一个归属权声明。这是对他们的工作的“谢谢”的一种方式。
本章代码
你可以在这里找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch13。
14 人工智能助力:提升我们的代码
在本章
-
埃里克和艾米丽向人工智能寻求帮助
-
人工智能提供代码审查
-
人工智能编写注释并解释错误信息
-
人工智能解释自己
-
人工智能提供其他关于如何使用人工智能的想法
“你准备好从人工智能那里获得帮助了吗?”西蒙问埃里克和艾米丽。
“当然,”埃里克回答道。“有没有为开发者专门的人工智能网站?”
“有几个为开发者提供的人工智能工具,”西蒙说。“其中一些,你登录到网站并请求帮助。你可以请求代码审查或代码解释,例如。其他工具可以安装在你的编辑器中,并帮助你编写代码。你稍后会尝试那些,所以今天我们先从使用网站开始。我在 ChatGPT 上有账号—让我们使用它。”
西蒙打开了一个网页 chat.openai.com,并输入了他的用户名和密码。他们看到一个底部有“发送消息……”提示的简单页面。

“记得我昨天是怎么告诉你的吗?”他开始说,“你必须精确地向人工智能说明你想要什么?它不是一个你可以先问‘你好吗?’的聊天机器人。我们想要对其中一个函数进行代码审查,所以我们必须这样解释。建议我们告诉人工智能它需要扮演某个人。在这种情况下,我们会要求它扮演代码审查员的角色。然后我们会告诉它我们期望它做什么。”
西蒙开始在提示窗口中输入:
Act as a code reviewer. Your task is to receive a function from me and
give me feedback on the code structure, syntax, any potential errors, and
improvements that can be made.
Here is my first function to review:
他停下来说道:“让我们从一个简单的函数开始。例如,让我们以 read_menu() 函数为例。”他从编辑器中复制了函数:
def read_menu(filename):
f = open(filename)
temp = f.readlines()
result = []
for item in temp:
new_item = item.strip()
result.append(new_item)
return result
人工智能几乎立即开始输入:
感谢你提供代码。以下是我的审查:
我已经审查了你的 read_menu 函数,以下是我的反馈:
-
代码结构—你的代码整体结构简单易懂。做得好!
-
语法—语法正确,遵循 Python 习惯。
-
潜在错误—代码缺少适当的错误处理,尤其是在文件 I/O 操作方面。如果给定的文件名不存在或存在权限问题,你的代码将引发异常。
-
改进:
-
你可以使用
with语句来打开文件,这会自动处理文件的关闭,即使在异常情况下也是如此。这确保了文件在使用后被正确关闭。 -
你可以使用列表推导式简化
for循环,这样可以使你的代码更短,更符合 Python 风格。
-
这是你函数的一个更新版本,包含建议的改进:
人工智能创建了一个包含 Python 代码的独立窗口:
def read_menu(filename):
try:
with open(filename) as f:
result = [item.strip() for item in f.readlines()]
except FileNotFoundError:
print(
f"Error: The file '{filename}' could not be found.")
result = []
except PermissionError:
print(f"Error: You do not have permission to read \
the file '{filename}'.")
result = []
except Exception as e:
print(f"An unexpected error occurred: {e}")
result = []
return result
之后,人工智能添加了它的解释:
这个函数版本包括了文件操作的错误处理,使用了一个 with 语句来处理文件,并包含了一个列表推导式来生成结果列表。
“哇!”艾米丽和埃里克同时说道。
西蒙评论道:“所有有效的观点。是的,使用with open是处理文件时的常见做法。我之前不想使用它,因为它需要额外的解释,而我们当时还没有准备好。try - except组合也非常常见。在这种情况下,如果找不到菜单文件,或者你没有权限读取它,程序不会因为错误而崩溃,而是礼貌地告诉你发生了什么。这是一个非常好的做法。”
“什么是列表推导式?”埃里克问道。
“这是一种特殊的 Python 方法,可以使for循环更短。看看人工智能写的代码:”
result = [item.strip() for item in f.readlines()]
“这是替换这个循环的方法:”
temp = f.readlines()
result = []
for item in temp:
new_item = item.strip()
result.append(new_item)
“是的,它更短,”埃里克同意,“但循环更容易理解。”
“没错!”西蒙说。“这就是为什么我们从一个循环开始。它们更容易理解。现在你知道它是如何工作的,你可以使用更紧凑的版本。我最初并不理解列表推导式。但我开始更频繁地使用它们,现在它们和普通循环一样容易阅读。而且,正如人工智能正确指出的,它更符合 Python 风格。哦,我们也可以让人工智能向我们解释这一点。”
西蒙输入:
Please explain, what is list comprehension in Python?
人工智能回答道:
列表推导式是 Python 中创建列表的一种简洁方式。它是一种语法结构,允许您通过指定要包含的元素来创建新列表,通常基于现有的可迭代对象(如列表、元组或范围)。列表推导式比使用传统的for循环和append()方法构建列表更简洁,通常也更快。
列表推导式的基本结构是:
[expression for item in iterable if condition]
-
expression: 一个表达式,用于确定新列表中每个项的值。它通常涉及item变量。 -
item: 一个表示可迭代对象中每个元素的临时变量。 -
iterable: 一个可迭代对象(例如列表、元组、范围),您想要遍历它。 -
if condition: 一个可选的条件,用于从可迭代对象中过滤元素。只有当条件为真时,元素才会包含在新的列表中。
以下是一个简单的示例,演示了如何使用列表推导式:
# Traditional way to create a list of squares using a for loop
squares = []
for i in range(1, 6):
squares.append(i**2)
print(squares) # Output: [1, 4, 9, 16, 25]
# Using list comprehension to create the same list of squares
squares = [i**2 for i in range(1, 6)]
print(squares) # Output: [1, 4, 9, 16, 25]
您还可以使用带有条件的列表推导式。以下是一个创建 1 到 10 之间偶数列表的示例:
even_numbers = [x for x in range(1, 11) if x % 2 == 0]
print(even_numbers) # Output: [2, 4, 6, 8, 10]
列表推导式可以使您的代码更简洁、更易读,尤其是在简单的操作中。然而,对于更复杂的操作或嵌套循环,它可能变得不那么易读,因此,在简洁性和可读性之间取得平衡是至关重要的。
“让我们让 AI 帮助我们做其他事情。当我们编写代码时,我们没有为它添加注释。这不是一个好的习惯。你应该在你的代码中添加注释来解释你在做什么。可能有人会阅读你的代码。或者,更常见的是,三个月后你会再次阅读它。我保证,如果你不注释你的代码,你会问自己‘这是什么?我为什么要这样写?我这是什么意思?’相信我,我是在从经验中说的,”西蒙说,然后微笑。
“所以注释你的代码是一个好习惯。通常,是你自己做的,但让我们看看 AI 是否可以帮助我们。”他在提示窗口中写下了以下内容:
Please add comments to the function you've just created.
人工智能立即回答:
当然!这是函数的更新版本,其中添加了注释来解释每个步骤:
def read_menu(filename):
"""
Reads the menu from a file and returns a list of items.
:param filename: The name of the file containing the menu.
:return: A list of menu items, with leading and trailing
whitespaces removed.
"""
try:
# Open the file using 'with' to ensure it's properly
# closed after use
with open(filename) as f:
# Read all lines from the file and remove whitespaces
# using list comprehension
result = [item.strip() for item in f.readlines()]
except FileNotFoundError:
# Handle the case when the file does not exist
print(f"Error: The file '{filename}' could not be found.")
result = []
except PermissionError:
# Handle the case when there is no permission to read
# the file
print(f"Error: You do not have permission to read \
the file '{filename}'.")
result = []
except Exception as e:
# Handle any other unexpected errors
print(f"An unexpected error occurred: {e}")
result = []
return result
“这真是太好了!”西蒙说。“看:在函数的开始处,它添加了一个文档字符串。这是一个可以包含在函数文档中的特殊字符串。然后,对于每个异常,它都添加了对它含义的解释。我自己写不出更好的注释了!看看这个例子,并且始终在你的代码中添加注释和文档字符串!”
“哦,我以为 AI 现在会帮我们做这件事呢,”埃里克说,然后笑了。
“是的,人工智能可以评论简单的代码行,比如异常处理,”西蒙回答,“但我确信很快你就会开始编写不那么简单的函数,即使是再聪明的 AI 也无法解释它们。如果你打算编写真正原创的代码,那就准备好自己动手吧。”
“人工智能可以帮助我们处理错误信息吗?”艾米丽问道。
“好主意!”西蒙说。“让我们试试——我相信它可以帮助解释一些错误信息。埃里克,记得吗?你一开始收到了你的第一个错误信息?”
“是的,我记得你对此非常高兴,”埃里克抱怨道。
“让我找找,我把它保存在我的笔记里。找到了,”西蒙说。他在提示窗口中输入了错误信息和他的请求:
Please explain the following Python error message:
Traceback (most recent call last):
File "/home/erik/mu_code/menu.py", line 30, in <module>
print("Main product: ", drinks[drink])
TypeError: list indices must be integers or slices, not str
人工智能回答道:
这个错误信息意味着你正在尝试使用字符串作为索引来访问列表中的元素,这在 Python 中是不允许的。在这个具体的错误信息中,看起来你正在尝试使用包含字符串的变量 drink 来访问列表 drinks 中的元素。以下是一个会产生这个错误的示例代码:
drinks = ["coffee", "tea", "soda"]
drink = "coffee"
print("Main product: ", drinks[drink])
在这段代码中, drink 是一个包含值 "coffee" 的字符串变量。当我们尝试通过* drinks[drink] 访问时,Python 会引发一个 TypeError,因为索引必须是整数,而不是字符串。为了修复这个错误,你需要使用整数索引来访问列表中的元素。例如,如果你想访问列表的第一个元素,你会使用 drinks[0]:
drinks = ["coffee", "tea", "soda"]
drink = 0
print("Main product: ", drinks[drink])
这将输出 "主产品:咖啡",这是 drinks 列表的第一个元素。
“它甚至知道我们的应用程序是关于什么的!”埃里克惊呼。
“是的,”西蒙说。“我们只给了它一个错误信息,它就猜出了我们的程序做什么。
“这是由于这个人工智能程序的工作方式,”他补充说。
“它是怎么工作的?你能解释一下吗?”艾米丽问道。
“嗯……”西蒙开始说,“我不是人工智能专家。我只是读了几篇文章。我会尽力解释我是怎么理解的。”
“人工智能公司的员工——比如创建了 ChatGPT 的 OpenAI——知道如何训练计算机理解人类语言和程序。为此,他们给计算机提供大量的文本和代码。然后一个特殊的程序,称为模型——顺便提一下,通常是用 Python 编写的——分析所有这些文本,找出哪些单词更经常与其他单词一起使用。就像在我们的例子中,人工智能模型发现人们经常使用“drinks”这个词时,也会使用“coffee”、“tea”和“soda”。这就是为什么它给出了一个与我们代码非常相似的例子。
“此外,”西蒙继续说,“这些模型可以计算所谓的文本相似度,并比较不同的单词和短语。然后他们可以使用这些结果来回答像‘与西班牙的关系与巴黎与法国的关系相同的是什么?’这样的问题。模型会计算“巴黎”和“法国”之间的‘距离’,然后执行如下数学运算:
? = Spain + (Paris - France)
“它将得到答案‘马德里’。”
“我能试试吗?”艾米丽问道。
“是的,让我们试试!”西蒙说,“我在几篇文章中看到了这个解释,但我自己从未尝试过。”
艾米丽拿起键盘,在提示窗口中输入:
What is related to Spain the same way Paris is related to France?
人工智能回答道:
马德里与西班牙的关系与巴黎与法国的关系相同。马德里和巴黎分别是西班牙和法国各自国家的首都。
“确实有效!”西蒙说,“让我们问问它是怎么得出这个答案的。”
艾米丽在文本框中输入:
Can you please explain how you came to this answer?
人工智能说:
当然!我是通过理解巴黎和法国之间的关系得出这个答案的。巴黎是法国的首都。所以,这个问题是在询问与西班牙有类似关系的东西。
知道马德里是西班牙的首都,我确定它与西班牙的关系与巴黎与法国的关系相同。因此,我提供了马德里作为这个问题的答案。这种推理称为类比推理,其中我根据不同项目之间的关系找到它们的相似之处。
西蒙又问了一个问题:
Did you use text similarity to find this relationship?
人工智能回答道:
作为一个 AI 语言模型,我没有明确使用文本相似度来找到巴黎和法国之间的关系,以及马德里和西班牙之间的关系。相反,我依靠我在大型文本数据集上训练期间学习到的关于国家及其首都之间关系的前期知识和理解。
然而,文本相似度技术,如词嵌入,可以帮助根据单词的上下文和使用情况找到单词之间的关系。在这种情况下,我利用我的知识和类比推理来建立关系并提供答案。
“太酷了!”西蒙说。“我们可以用 AI 来学习 AI!我知道人们使用 AI 来完成许多其他任务来帮助他们开发程序。我们已经尝试过编写注释、进行代码审查和解释错误信息了。我想知道 AI 还能帮助我们做什么。让我们去问它!”他输入了以下问题:
We asked ChatGPT to help us with our Python project.
We tried writing comments, doing code reviews, explaining error messages
already.
What else can ChatGPT help us with?
人工智能回答道:
除了编写注释、进行代码审查和解释错误信息之外,ChatGPT 还可以帮助您处理 Python 项目的其他方面,例如:
-
生成代码示例——ChatGPT 可以提供示例代码片段,展示如何使用某些函数、库或语言特性。
-
调试——您可以描述您遇到的问题,ChatGPT 可以建议潜在的解决方案或识别问题根本原因的方法。
-
推荐库和工具——基于其对 Python 生态系统中的流行和广泛使用的资源的了解,ChatGPT 可以推荐用于特定任务的适当库或工具。
-
最佳实践和编码规范——ChatGPT 可以提供关于最佳实践、编码规范和风格指南的指导,帮助您编写干净、可维护和高效的 Python 代码。
-
算法设计——ChatGPT 可以帮助您理解或设计算法,解释它们的逻辑,并提供如何在 Python 中实现它们的示例。
-
项目结构——ChatGPT 可以就组织 Python 项目提供建议,例如如何结构化您的目录、文件和模块,以及关于包管理和依赖处理的指导。
记住,虽然 ChatGPT 可能很有帮助,但它不能替代对概念进行彻底研究和理解。此外,审查生成的代码或建议以确保正确性和适合您特定项目需求是很重要的。
“所有的好主意!”西蒙说。“我认为我们应该一起尝试。明天,让我们讨论所有可能的改进应用程序的方法,并为未来制定计划,”西蒙说。“在人工智能的回答中,我认为最后一段非常重要。当人们第一次看到人工智能编写的代码时,他们开始认为人工智能可以为他们做任何事情。但我和它玩了几次,注意到并非所有生成的代码都有效。您总是需要审查和测试它。”
“好的,回到我们的咖啡店应用程序,”他继续说。“我们还需要对它做些什么?”
“您说我在 iPad 上可以打开这个应用程序,”埃里克说。
“是的,这是可能的,但我们需要做一些小的改动。您说得对,如果只能在自己的电脑上使用这个应用程序,那么这个应用程序就没有太多意义。明天,我会向您展示如何在 iPad 或手机上打开它。”
“我们会把它放在云端吗?”艾米丽问道。
“差不多,”西蒙说。“目前,它将放在我们自己的家庭网络内的‘云端’。但您需要将其放在真正的云端,以便可以从任何地方访问。我们明天再谈。”
今天你学到的新东西
-
人工智能可以帮助你编写优秀的程序—人工智能可以帮助你编写更好的代码,解释错误信息,修复错误,并注释你的代码。人工智能还可以为你提供关于程序结构和有用工具和库的想法。
-
人工智能不会取代程序员—人工智能可以做很多有趣的事情,但它仍然需要人类的帮助来制作出实用的工作应用。并非所有由人工智能生成的代码都能在没有错误的情况下工作。你应该始终审查和测试它。
-
人工智能使用称为模型的特殊程序—人工智能使用语言模型来生成其答案。这些模型是在大量文本上训练的,包括维基百科、在线文章、程序代码以及许多其他内容。模型可以计算单词和句子之间的关系,并基于这些计算构建答案。
15 下一步:未来的计划
在本章中
-
埃米丽和埃里克为他们的应用程序创建首页
-
西蒙演示了如何从其他设备访问应用程序
-
埃里克和艾米丽使用 iPad 和安卓手机测试应用程序
-
西蒙、艾米丽和埃里克制定计划
“在我们说这个应用程序完成之前,还有一件小事情要做,”西蒙说,“记得,当我们打开我们的应用程序时,我们看到的是‘来自穆的问候’网页。我认为是时候用我们自己的首页替换它了。”
“当然!”艾米丽说。“我们已经选择了颜色,并且找到了一张好图片!我认为我们只需将标题‘欢迎来到 E&E 咖啡店!’和我们在订单页面使用的图片放上去就可以了。”
“是的,好主意,”西蒙说。“但不要忘记首页的主要目的。”
“主要目的是什么?”埃里克问道。
“在任何网站或网络应用程序中,您都使用您的首页进行导航,”西蒙回答。“当人们打开您的网站时,他们应该能够快速了解它做什么以及如何使用它。看看您最喜欢的咖啡店网站,并告诉我您在那里发现了什么。寻找它们共有的特点。”
埃米丽和埃里克搜索熟悉的公司,并在几个标签页中打开它们的网站。经过一段时间探索后,埃里克说,“它们都有一个叫做菜单的页面。它们有一个位置页面或寻找商店,在那里它们展示了如何找到它们的商店。”
“当你打开几个咖啡店时,你也将需要创建这样的一个,”西蒙评论道。
“是的,”埃里克说。他继续说,“他们还有奖励卡和礼品卡。我认为我们也需要它们。在底部,他们通常有很多小链接。他们有‘关于我们’、‘联系我们’和其他的。”
“你做了很好的研究,”西蒙说。“现在你可以看到我所说的导航了。目前,除了首页外,我们只有订单页面。我认为我们可以在首页上放一个按钮,它会链接到订单页面。”
“就像我们在‘这里是您的订单’页面做的那样?”艾米丽问道。
“正是这样!”西蒙说。“您可以直接从print.html页面复制这三行。打开templates/index.html文件,删除第二行block content和最后一行endblock之间的所有内容。然后插入我们刚才讨论的内容:标题、按钮和图片。”
“是的,现在我知道该做什么了,”艾米丽说。他们开始着手制作首页。几分钟后,艾米丽和埃里克向西蒙展示了他们的首页:
列表 15.1 templates/index.html:创建您的首页
{% extends "base.html" %}
{% block content %}
<h1>Welcome to E&E Coffee Shop</h1>
<form action="/order">
<input type="submit" value="New order" />
</form>
<img src="/static/img/8507631_3925457.jpg"
alt="Coffee drinks" width="100%">
Image by <a href="https://www.freepik.com/free-vector/
take-away-coffee-coffee-mugs_8507631.htm">Freepik</a>
{% endblock %}
轮到你了! 编辑您的首页
打开您的templates/index.html文件,像埃里克和艾米丽刚才做的那样进行更改。您可以随意更改图片或标题。
“看起来不错,”他说。“让我们试试!”

“很好。现在尝试点击新订单。它应该会打开订单页面,”西蒙对埃里克说。
埃里克照做了,他看到了熟悉的订单页面和菜单。
“一切正常!”他说。“现在我们有一个完整的网络应用程序了!”
“西蒙,”艾米丽开始提问,“昨天你说我们也可以从我们的设备上使用这个应用程序。你能给我们演示一下吗?”
“是的,我想在我的 iPad 上试试,”艾瑞克说。
“是的,绝对可以,”西蒙回答。“艾瑞克,你还记得这个整个项目是从你用 iPad 收集订单开始的吗?我还告诉你,你可以创建一个网络应用程序,这样你就可以不仅把它当作笔记本使用?”
“是的,我记得,”艾瑞克说。“那是在几周前,”他微笑着。
“好的,让我们先让它在你的 iPad 上运行,”西蒙说。“让我解释我将要做什么。”他开始画图并解释。
“当我们从 Mu 编辑器运行我们的应用程序时,默认情况下,它只能在运行它的计算机上访问。这就是为什么我们使用 localhost 或地址 127.0.0.1 来使用我们的应用程序。Flask 和 Mu 编辑器这样做是为了确保在我们仍在开发应用程序时,不会将其展示给其他计算机和用户。”
“这很有道理,”艾瑞克说。“在我们添加字体和图片之前,它看起来并不漂亮。”
“对,”西蒙说。“但现在我们已经准备好在其他设备上使用它了,我们想改变这一点。幸运的是,这只是 Mu 编辑器配置中的一行。在我们做出这个更改后,我们家庭网络中的所有设备都将能够使用它——当然,如果它们有浏览器的话。”
“我能从我的手机试试吗?”艾米丽问。
“是的,但你应该连接到我们家的网络。让我输入我们的 Wi-Fi 密码,”西蒙在艾米丽的手机上输入了密码。
“这是左边的默认配置,”他继续解释。“我们只能从运行应用程序的计算机访问它。而在这里,在右边,我们可以从其他设备访问它。”

“每台计算机都有一个 网络地址。艾瑞克,你的笔记本电脑连接到我们的家庭网络。它的地址是 192.168.1.85。我们网络中的任何其他设备如果想要与之通信,都应该使用这个地址。如果您的计算机想要与 自己 通信,那么它使用 127.0.0.1。”
“我现在可以试试吗?”艾瑞克问。
“等等,我需要在配置中做一些更改。记住,我们现在正在向我们的家庭网络中的所有设备展示我们的应用程序。当然,连接的设备并不多,但我们仍然想确保我们的应用程序完全准备好。将来,我们将将其上传到云端,然后 全世界的所有计算机 都将看到它。我们最好确保它运行正确且看起来不错。”
“艾瑞克,你看到 Mu 编辑器右下角那个小齿轮吗?”西蒙继续说。“它通常意味着 设置 或 配置。请点击它。”

“现在选择 Python 3 环境 选项卡,并在那里的文本末尾添加以下行:”
FLASK_RUN_HOST=0.0.0.0
艾瑞克照做了,并看到了结果。

“现在点击确定并重新启动应用程序。”
埃里克通过点击“确定”保存了配置。他在编辑器中点击了“停止”,然后再次点击“运行”。
西蒙说,“现在看看下面的输出窗口。看到这条Running on all addresses吗?这正是我们需要的。在这两条下面,你会看到我们可以用来访问应用程序的地址。第一个是127.0.0.1,这是我们经常使用的。第二个是你的笔记本电脑在我们家庭网络中的地址。你应该在你的 iPad 浏览器中输入它。而你,埃米莉,在你的手机上输入它。在这种情况下,它是http://192.168.1.85:5000。”
“记住,这个地址可能会改变。例如,如果你带着笔记本电脑去埃米莉的家并连接到她的家庭网络,你的笔记本电脑将获得一个不同的地址。你会在输出窗口中看到它,在埃米莉的家中,你必须使用这个新地址。

“好的,现在在你的设备上打开这个地址,并给我看看你的订单页面上有什么。”
埃米莉和埃里克打开他们的设备并开始输入。
“明白了!”埃米莉展示了她的手机。

“看起来很棒!”西蒙说。“埃里克,你的 iPad 上有什么?”
“看!”埃里克说。

“它可以在你的两个设备上运行,看起来也很棒,”西蒙说。“记住,我告诉过你我们正在使用 Mu 编辑器附带的风格表,这些表单是为在不同设备上工作而设计的。在这里,我们在 MacBook、iPad 和 Android 手机上使用了你的应用程序,它们在所有设备上都看起来很棒。更重要的是,你注意到我们的菜单是如何重新排列以适应页面的吗?这就是使用normalize.css和skeleton.css风格表的优势。我们只是用我们的coffeeshop.css风格表添加了一些小小的改动。”
“很棒,我们的咖啡馆的顾客可以从他们的手机上订购饮料,”埃米莉说。“但为了这个,他们应该在我们店里并连接到我们的网络,对吧?西蒙,我们可以将我们的应用程序放置在真正的云中,这样我们就可以在任何地方使用它了?我很想在学校向我的朋友们展示它。”
“当然,这是可能的。当然,这比我们刚才只是添加一个字符串要复杂一些。通常,云服务需要一些费用,但对我们这样的简单项目来说,找到免费服务是可能的。其中之一叫做 PythonAnywhere。我知道他们可以托管你的 Python 应用程序,但我自己还没有尝试过。给我一些时间,我会测试它并给你提供说明。
注意:你可以在本书的配套网站上找到使用 PythonAnywhere 的说明,网址为pythonicadventure.com。
“但在我们将应用程序向全世界开放之前,让我们先讨论一下可能的改进。埃里克,你还记得我们与数据库一起工作时讨论过什么吗?”
“你说在数据库表中包含订单时间是个好主意,”埃里克说。
“是的,没错!让我们开始一个待办事项列表来规划我们的未来改进。”他开始在文本编辑器中写:
* Timestamps in orders
“我们可以记住每位顾客,”艾米丽说,“并显示他们上次订购了什么。”
“好主意,”西蒙说。“但是,在这种情况下,我们应该为每位顾客创建一个独特的名字。我们不希望把两个名字叫亚历克斯的不同顾客搞混。”
“我们的菜单怎么办?”埃里克问道。“当我们在我笔记本电脑上运行它时,我们可以编辑文本文件。但是当我们把它运行在云上时,我们无法编辑文件,对吧?”
“是的,你说得对,埃里克,”西蒙说。“如果我们使用云服务,所有操作都应该通过网页浏览器完成。我们需要另一个页面来编辑菜单。别忘了这个页面应该仅供商店经理使用。”
“我明白你的意思,”艾米丽说。“我们需要密码!”
“对,”西蒙确认说。“在我们的应用程序中我们需要不同的账户。我们将有顾客账户,将有咖啡师账户,他们将在那里接收订单。我们还将有商店经理账户,他们将在那里列出订单,编辑菜单,并做其他管理工作。”
“我们需要更多的图片!”艾米丽说。“我的意思是,我们产品的图片:所有口味和配料。”
“那促销怎么办?”埃里克问道。“比如每周的口味,或者类似的东西。西蒙,你说过我们应该知道一天或一个月我们使用了多少份口味和配料。我们能在网页上显示这些信息吗?”
“哎呀,哎呀,请慢一点。我记不住你们所有的想法,”西蒙说,然后微笑。“说真的,我喜欢我们这样头脑风暴。头脑风暴的一个主要原则——我们在机器人俱乐部经常使用它——是不允许批评想法。每个人都欢迎提出最疯狂的想法。我们不会说,‘哦,这不可能实现。’我们只是收集所有想法,然后稍后再讨论。”
“然后我想在我们的咖啡馆里添加语音命令,”埃里克说。“还需要一个机械臂来倒咖啡和添加口味和配料。”
“记住了,”西蒙说。
“我认为我们不应该使用密码,而应该使用面部识别,”艾米丽说。“密码太无聊了。想象一下,一位顾客走进商店,我们问候他们说,‘嘿,马克斯,你今天还要你常点的香草味吗?’”
“太好了,我也记住了,”西蒙说。“这是我到目前为止收集到的。”他向艾米丽和埃里克展示了他的列表:
* Timestamps in orders
* Remember each customer and what they ordered (create user accounts)
* Edit menus from the browser
* Accounts for shop managers, baristas, cashiers, customers
* List orders for a day, week, month; list all orders from a customer
* More pictures of our products
* Product promotions
* Reports: how many orders of each flavor and topping per day, week, month
* Voice commands
* Robotic arms for coffee, flavors, and toppings
* Face recognition for customers
“此外,别忘了我们昨天从 AI 那里收到的建议。让我再创建一个列表,”西蒙说。他从 ChatGPT 复制了答案:
* Code examples: ask the AI to review all our functions and give suggestions for improvement
* Debugging: ask the AI to analyze our functions for potential bugs
* Recommend libraries and tools: ask the AI to give us suggestions
* Algorithm design: ask the AI to analyze our algorithms (we didn’t use many yet)
* Project structure: ask the AI for suggestions on project structure
* Create tests for our functions (the AI didn’t tell us yesterday, but I know people use it for tests)
“这足够我们忙一整年了!”西蒙说。“我担心这本书里没有足够的空间来涵盖所有这些内容。”
“哪本书?”埃里克问道。
“埃里克,你不知道吗?”西蒙问道,“我们的爸爸正在写一本关于我们的书——关于你、艾米丽和我。这本书全部关于这个咖啡店项目以及我们是如何开发它的。他说他不能再给这本书添加更多章节了,所以我担心我们不得不在网上继续我们的项目。我建议我们创建一个网站,并更新我们的进度和开发的新功能。你怎么看?”
“我喜欢!”艾米丽说,“它将像博客一样,对吧?”
“是的,一个博客!也许我们还应该添加一个 GitHub 仓库,我们可以把所有的程序和示例都保存在那里。”
“太好了!”埃里克说,“我听说过 GitHub——大多数开发者都把他们的代码放在那里。”
“而且当我们这样做的时候,”西蒙继续说,“其他人将能够使用我们的代码,从中学习,也许还能为我们应用程序添加他们自己的功能。也许他们会分享他们的应用程序,我们也会帮助他们。这就是这些天大多数项目是如何开发的。”
“太棒了!我也加入!”艾米丽说。
“我也是,”埃里克补充道。
“太好了,那我们就继续在我们的网站上吧。我会和爸爸谈谈,我们一起把它设置好。明天见!”西蒙说。他喜欢和艾米丽、埃里克一起做这个项目。他自己也因为咖啡店项目学到了一些新东西。“是时候学习更多关于账户、机械臂和面部识别的知识了,这样我才能向孩子们解释,”他想着。
注意:他们创建的网站在这里:pythonicadventure.com/。来寻找你项目的新想法,故障排除提示,等等!
今天你学到的新知识
-
主页——当你访问一个网站时看到的第一个页面。通常,它被用来告诉访客可以在网站上找到什么。
-
网络地址——每个连接到网络的计算机都有一个地址,允许网络中的其他计算机找到它。如果我们想访问埃里克笔记本电脑上的网络应用程序,我们需要知道它的网络地址。
本章代码
你可以在这里找到本章的代码:github.com/pavelanni/pythonicadventure-code/tree/main/ch15。
附录 A. 第一款应用程序的想法
创建咖啡店应用程序对你来说听起来并不吸引人?创建其他东西!我在这本书中讨论的所有编程想法和方法都适用于许多其他项目。只需四处看看,你就会为其他应用程序获得灵感。
这里有一些例子:
披萨店
这应该与咖啡店应用程序非常相似。你可以向顾客询问以下问题:
-
他们主要的饮料是什么?
-
他们想要什么口味?
-
他们想要什么配料?
你可以为每个问题给顾客一个选项列表,顾客从菜单中选择。
你也可以向披萨店的顾客询问以下问题:
-
你想要什么类型的饼底——薄还是厚?
-
你想要什么大小——小,中,还是大?
-
你想要哪种酱汁——红色还是白色?
-
你想要什么类型的披萨——玛格丽塔,辣味肠,还是蔬菜?(为了获得灵感,去你最喜欢的披萨店看看他们有什么。)
-
你还想添加哪些额外的配料?
冰淇淋店
去你最喜欢的冰淇淋店,看看他们是如何准备你的订单的。他们会问你什么?他们会给你哪些选项?这些选项将会出现在你的应用程序菜单中。
很可能,他们会问你以下问题:
-
你想要什么类型的圆锥形冰淇淋——糖,华夫饼,还是蛋糕?
-
要几勺?
-
你想要什么口味?
-
有任何配料吗?
在这里,它与咖啡店略有不同。在你问“要几勺?”之后,你将不得不为每一勺的口味问这么多次。想想看:你如何在 Python 中做到这一点?
这里有一个提示:Python 中有一个range()函数可以在for循环中使用。我们在我们的菜单中使用了它。试着用它来询问冰淇淋口味的确切次数。
乐高迷你人偶
也许你有一套很好的乐高迷你人偶及其部件,你想帮助你的朋友建造一些新的东西。你会问他们什么问题,给他们什么选项?以下是一些例子:
-
选择头部——笑脸,太阳镜脸,带胡子的脸
-
选择头饰——深色头发,金色头发,安全帽,警察帽
-
选择身体——机械师,警察,领带衬衫,T 恤
-
选择腿——蓝色牛仔裤,绿色短裤,棕色工装裤
-
选择配件——一把剑,一台收音机,一把锤子,一个放大镜
你可以在你的应用程序中添加特殊条件。例如,如果你的朋友选择了一顶警察帽,他们就不能选择棒球棒作为配件。想想看,你如何在菜单中添加这个条件。
随机选择部件怎么样?这可能会创造出一些有趣的迷你人偶。你如何在菜单中添加一个随机选项?你将如何实现它?
这里有一个提示:Python 中有一个名为 random 的模块。你应该在程序开头使用 import 语句导入它,并使用 choice() 函数。你可以给这个函数提供一个选择列表,它会随机选择其中的一个。下次你调用它时,函数会随机选择另一个(或者可能是同一个项目——它是随机的!)。例如,创建以下简短的程序并运行它。在这个程序中,我们让 Python 从三种不同类型的头发列表中随机选择五个项目。
列表 A.1 choice.py
import random
for _ in range(5):
print(random.choice(["dark hair", "blond hair", "red hair"]))
以 python choice.py 的方式运行程序,你会看到类似以下内容:
blond hair
red hair
blond hair
blond hair
dark hair
当然,你的列表将不同,并且可能包含 5 个 其他 随机选择,顺序不同。
其他项目想法
你有其他项目想法吗?请在 liveBook 论坛上分享:livebook.manning.com/book/a-pythonic-adventure/discussion.
附录 B. 如何安装 Mu 编辑器和 Python 环境
在这个附录中,我将解释如何在您的计算机上安装 Python。最简单的方法是安装包含 Python 的编程编辑器。我建议您安装 Mu 编辑器。我在这本书中使用它,所以如果您使用相同的编辑器,将很容易跟随书中的对话和说明。
我还会提供其他安装 Python 的链接——您也可以尝试它们。
Mu
按照以下步骤安装 Mu:
-
在您的网络浏览器中打开 Mu 编辑器的网页:
codewith.mu/。![B1-mu-editor-langing-page]()
-
点击下载(绿色按钮)。您将看到 Mu 的下载页面。
![B2-mu-editor-download]()
-
点击您操作系统的下载按钮。您的浏览器将按照以下方式下载适用于您操作系统的安装文件:
-
对于 Windows,它将是一个
.msi文件。 -
对于 macOS,它将是一个
.dmg文件。 -
对于 Linux,它将是一个
.AppImage文件。
-
-
点击您操作系统的说明并按照说明操作。
-
正常打开您操作系统中的应用程序一样打开 Mu。您就可以开始工作在您的项目上了!
您还可以使用 Mu 编辑器来编程微控制器和构建机器人,但这将是另一本书的主题。
Thonny
Thonny 是另一个为初学者设计的优秀的 Python 编辑器。您可以在以下位置找到它:thonny.org/。
在第一页,您将找到 Windows、macOS 和 Linux 的安装程序。对于 Windows,下载 .exe 文件并运行它。对于 macOS,下载 .pkg 文件并安装它。对于 Linux,运行说明中提供的命令。

安装完应用程序后,启动它并探索其设置。您可以从十几种选项中选择颜色主题、编辑器和终端字体以及许多其他选项。
Thonny 有一个非常有用的功能叫做助手。在选项菜单中,您可以配置它,在您的代码中每次出现警告时启动。当您的程序显示错误时,它也会启动。助手会为您提供关于您的代码可能出错的几个建议。尝试在您的代码中犯一个小的错误(例如变量名中的拼写错误),然后运行程序。您将看到助手的实际操作。
Python
这两个先前的编辑器都包括 Python 在它们的安装包中。但,出于某种原因,您可能想单独安装 Python。
如果您使用的是 macOS 或 Linux,您的操作系统已经安装了 Python。很可能是不是最新版本的 Python,但这不是问题:本书中开发的全部程序都将从 3.5 版本的 Python 开始工作。在这些操作系统上不需要安装任何东西——至少,不是为这本书。
如果您使用的是 Windows,您必须前往官方 Python 网站,并从那里下载安装程序:www.python.org/downloads/windows/。
请仔细阅读说明,并根据你的 Windows 版本选择正确的 Python 版本。

在 Windows 上安装 Python 的另一种方法是打开 PowerShell 窗口并输入 python。Windows 将建议安装正确的 Python 版本。你只需接受即可。




浙公网安备 33010602011771号