原文:zh.annas-archive.org/md5/8f93267a61af3c28aa765d9c5f3f38e8

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:使用 GitHub Copilot 介绍 AI 辅助编程

本章涵盖

  • AI 助手如何改变新程序员的学习方式

  • 为什么编程永远不会一成不变

  • 如何使用 GitHub Copilot 等 AI 助手工作

  • AI 辅助编程的可能风险

计算机编程长期以来一直是受过特殊培训并拥有高级技能的专业人士的领域。毕竟,你希望运行银行、电话、汽车等应用的程序每次都能完全正确地运行!正如曾经占据房间大小的计算机,堆满纸卡片和数英里长的磁带被现代设备所取代一样,编程语言和工具也变得更容易使用。现在,像 ChatGPT 这样的 AI 工具让计算机编程几乎对每个人来说都触手可及。我们希望帮助你打开这扇门!

学习如何编程,你将能够承担工作中的新任务,创建自己的电脑游戏,并在工作中让电脑为你工作。在这本书中,我们将向你展示如何使用 ChatGPT 和 GitHub Copilot 编写自己的计算机程序。在这个过程中,你将学习一些 Python 编程语言中的技能,Python 是最受欢迎的编程语言之一。

1.1 改善我们与计算机的交流方式

让我们先请计算机从 0 数到 9。几十年前,一本关于编程的书会要求你学习如何阅读和理解以下代码(基于mng.bz/EOdO):

section .text 
global _start 
_start: 
    mov ecx, 10 
    mov eax, '0' 
    l1: 
    mov [num], eax 
    mov eax, 4 
    mov ebx, 1 
    push ecx 
    mov ecx, num 
    mov edx, 1 
    int 0x80 
    mov eax, [num] 
    inc eax 
    pop ecx 
    loop l1 
    mov eax, 1 
    int 0x80 
section .bss 
    num resb 1

我们很高兴我们不再是这样编程的。那个怪物是用汇编语言编写的,汇编语言是一种低级编程语言。正如你所见,低级编程语言并不是人类可以轻松阅读和编写的语言。它们是为计算机设计的,而不是为人类设计的。

没有人想编写那样的程序,但,特别是在过去,有时这是必要的。程序员可以使用它来精确定义他们希望计算机执行的操作,甚至到单个指令。这种程度的控制是必要的,以便从性能不足的计算机中榨取每一丝性能。例如,90 年代最速度关键的电脑游戏,如 Doom 和 Quake,就是用汇编语言编写的,就像之前的代码示例一样。否则,制作那些游戏是不可能的。

1.1.1 让它变得稍微容易一些

好的,让我们继续前进。这是一个更现代的计算机程序,它也会打印数字。

for num in range(0, 9): 
     print(num)

这段代码是用 Python 语言编写的,这是许多程序员现在使用的语言。与低级语言汇编语言不同,Python 被认为是一种高级语言,因为它与自然语言更加接近。即使你还不了解 Python 代码,你也可能猜出这个程序试图做什么。第一行看起来像是在处理从 0 到 9 的数字范围。第二行是在打印某些内容。相信这个程序,就像汇编语言的怪物一样,旨在打印从 0 到 9 的数字并不困难。不幸的是,出了点问题,它只打印了 0 到 8 的数字。

虽然这段代码更接近英语,但它并不是英语。它是一种像汇编语言一样的编程语言,具有特定的规则。就像之前的代码一样,误解这些规则的细节可能会导致程序出错。如果你好奇,误解的规则是range函数在第二个数字之前停止,所以它不包括数字 9。如果你想从 0 到 9,你需要说range(0,10)

与计算机进行交流的圣杯是使用像英语这样的自然语言。在过去 80 年里,我们使用各种编程语言与计算机交谈,并不是因为我们想这么做,而是因为我们不得不这么做。计算机的运算能力不足以处理像英语这样的语言的复杂性和特殊性。我们的编程语言从符号汤汇编语言发展到例如 Python,但它们仍然是计算机语言,而不是自然语言。这种状况正在改变。

1.1.2 使其变得更加容易

使用人工智能助手,我们现在可以用英语提出我们想要的内容,并让计算机为我们编写相应的代码。为了得到一个正确打印从 0 到 9 的 Python 程序,我们可以用正常的英语语言向我们的 AI 助手(Copilot)提出如下请求:

# Output the numbers from 0 to 9

Copilot 可能会对此提示做出如下回应:

for i in range(10):
    print(i)

与我们之前展示的例子不同,这段 Python 代码实际上是可以运行的!

人工智能编码助手可以帮助人们编写代码。在这本书中,我们将学习如何使用 Copilot 为我们编写代码。我们可以用英语提出我们想要的内容,然后得到 Python 代码的回应。

不仅如此,我们还将能够将 Copilot 作为工作流程中无缝的一部分来使用。如果没有像 Copilot 这样的工具,程序员通常会打开两个窗口:一个用于编写代码,另一个用于询问 Google 如何编写代码。第二个窗口包含 Google 搜索结果、Python 文档或程序员论坛,他们讨论如何编写代码来解决特定问题。他们经常将这些结果中的代码粘贴到他们的代码中,然后根据他们的上下文稍作修改,尝试其他替代方案等等。这已经成为程序员的一种生活方式,但你可以想象这里的低效。据估计,高达 35%的程序员时间用于搜索代码[1],而且找到的代码往往不易使用。Copilot 通过帮助我们编写代码,极大地改善了这种体验。

1.2 关于技术

本书我们将使用两种主要技术:Python 和 GitHub Copilot。Python 是我们将使用的编程语言,GitHub Copilot 是我们的人工智能助手,它将帮助我们与 Python 代码一起工作。

1.2.1 Python,你的编程语言

如前所述,Python 是一种编程语言,它是与计算机进行交流的方式。人们用它编写各种程序,执行有用的任务,如数据分析、游戏、交互式网站、可视化、文件组织应用、自动化常规任务等等。

还有其他编程语言,例如 Java、C++、Rust 以及许多其他语言。Copilot 也可以与这些语言配合使用,但截至本书编写时,它与 Python 配合得非常好。与许多其他语言(尤其是汇编语言)相比,Python 代码编写起来要容易得多。更重要的是,Python 易于阅读。毕竟,我们不会是编写 Python 代码的人——我们的 AI 助手会!

计算机不知道如何读取和运行 Python 代码。计算机唯一能理解的是一种称为机器码的东西,它看起来甚至比汇编码还要荒谬,因为它实际上是汇编码的二进制表示(是的,只是一堆 0 和 1!)。在幕后,你的计算机将你提供的任何 Python 代码转换为机器码,然后再运行,如图 1.1 所示。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/1-1.png

图 1.1 你的 Python 程序在你在屏幕上看到输出之前要经过几个步骤。

所以,现在没有人再从零开始用计算机的机器码语言编写代码了。程序员们都会选择当时对他们特定任务最方便的语言,并使用软件来帮助他们编写、运行和调试(即修复)代码,这种软件被称为集成开发环境(IDE)。在本书中,我们将使用 Visual Studio Code(VS Code)作为我们的 IDE,因为它与 GitHub Copilot 配合得非常好。

1.2.2 GitHub Copilot,你的 AI 助手

什么是 AI 助手?AI 助手是一种 AI 代理,它帮助你完成工作。也许你家里有 Amazon Alexa 设备或者一部带有 Siri 的 iPhone——这些是 AI 助手。它们帮助你订购杂货,了解天气,或者确定,是的,在 哈利·波特 电影中扮演贝拉特里克斯的女演员真的在 斗士俱乐部 中出现过。AI 助手只是一个能够对典型的人类输入,如语音和文本,以类似人类的回答进行响应的计算机程序。

Copilot 是一个具有特定任务的 AI 助手:它将英语转换为计算机程序(以及更多,我们很快就会看到)。还有其他像 Copilot 一样的 AI 助手,包括 Amazon Q Developer、Tabnine 和 Ghostwriter。我们选择 Copilot 来编写这本书,是基于我们能够产生的代码质量、稳定性(它从未崩溃过!)以及我们自己的个人偏好。我们鼓励你在感到舒适的时候也去检查其他工具。

1.2.3 Copilot 在幕后是如何工作的——30 秒内了解

你可以把 Copilot 看作是你和你要编写的计算机程序之间的一个层。你不需要直接编写 Python,你只需用文字描述你想要的程序——这被称为 提示——然后 Copilot 就会为你生成程序。

Copilot 背后的“大脑”是一个名为 大型语言模型(LLM)的复杂计算机程序。LLM 存储关于词语之间关系的信息,包括哪些词语在特定上下文中是有意义的,并使用这些信息来预测对提示的最佳词语序列。

想象一下,我们问你这句话的下一个词应该是什么:“这个人打开了 ________。”这里可以填入很多词,比如“门”、“盒子”或“对话”,但也有许多词不适合这里,比如“the”、“it”或“open”。LLM 会考虑到词语的当前上下文来生成下一个词,并且它会一直这样做,直到完成任务。它以 非确定性 的方式做这件事,这意味着它的决策是有些随机的,也就是说,如果你要求它填入那个词,有时它会给你“门”这个词,有时它会给你“盒子”这个词。这意味着如果你要求 Copilot 给你代码,它可能会每次给出不同的答案。

此外,我们并没有说 Copilot 理解它在做什么。它只是使用当前上下文来继续编写代码。在整个过程中请记住这一点:只有我们知道生成的代码是否是我们想要它做的。通常它确实做到了,但无论如何,你都应该保持健康的怀疑态度。图 1.2 展示了 Copilot 从提示到程序的过程。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/1-2.png

图 1.2 使用 Copilot 从提示到程序的过程

你可能会想知道为什么 Copilot 为我们编写 Python 代码而不是直接编写机器代码。Python 现在不是可替代的中间步骤吗?嗯,不是的,原因在于 Copilot 会犯错误。而且如果它要犯我们需要修复的错误,用 Python 来做比用机器代码要容易得多。

事实上,几乎没有人检查从 Python 生成的机器代码是否正确。这部分原因是因为 Python 语言规范的确定性。我们可以想象一个未来,Copilot 的对话如此精确,以至于检查 Python 代码变得没有必要,但我们离那个未来还有很长的路要走。

1.3 Copilot 如何改变我们学习编程的方式

在过去学习如何编程时,学习者通常花费大部分时间与程序的语法和基本结构打交道。当我们提到 语法 时,我们指的是在给定语言中有效的符号和单词。程序员需要从头开始(一个字符一个字符,一行一行)编写程序的所有语法。学习编程的人过去常常需要花费数周或数月才能达到能够编写甚至基本程序的水平。现在,Copilot 可以立即编写这些相同的基本程序,并提供的代码几乎总是语法和结构上正确的。正如你将在本书的其余部分看到的那样,我们仍然需要验证这些代码是否正确,因为 Copilot 可能会出错。然而,我们不再需要从头编写它。我们相信 Copilot 和类似工具标志着人们学习编程的旧方式的终结。

你作为一个对学习编程感兴趣的人,根本不需要与语法作斗争,理解如何准确调用给定的 Python 函数,以及编写代码时所需的其他 Python 概念。当然,我们将在本书中学习这些概念,但不是为了让你通过从头编写 Copilot 可以轻松生成的代码来展示你的理解。不,我们学习这些概念仅仅是因为它们帮助我们解决有意义的问题,并与 Copilot 有效地互动。相反,你将能够更快地学习如何编写更大、更有意义的软件,因为 AI 助手从根本上改变了学习编程所需掌握的技能。

1.4 Copilot 还能为我们做些什么?

正如我们所看到的,我们可以使用 Copilot 根据我们想要的英文描述来为我们编写 Python 代码。因此,我们可以这样说,Copilot 接收英文语法的描述,并以 Python 语法的代码返回。这是一个巨大的进步,因为学习编程语法在历史上一直是新程序员的一个主要障碍。我应该在这里使用哪种括号——[、( 或 {]?我需要在这里缩进吗?我们应该按照什么顺序编写这些内容:x 然后是 y,还是 y 然后是 x?

这样的问题层出不穷,而且——让我们说实话——这并不有趣。当我们只想编写一个程序让某事发生时,谁会在乎这些呢?Copilot 可以帮助我们摆脱语法上的枯燥。我们认为这是帮助更多人成功编写程序的重要一步,我们期待着有一天这个人工障碍被完全消除。目前,我们仍然需要 Python 语法,但至少 Copilot 在这方面帮助我们。

但 Copilot 能做的不仅仅是这些。以下是一些相关且同样重要的任务,Copilot 可以帮助我们完成:

  • 解释代码 — 当 Copilot 为我们生成 Python 代码时,我们需要确定该代码是否满足我们的需求。同样,正如我们之前所说的,Copilot 可能会犯错误。尽管我们并不想教你 Python 工作的每一个细节(那是旧的编程模式),但我们会教你如何阅读 Python 代码,以获得对其功能的整体理解。我们还会使用 Copilot 的代码解释功能,用英语向你解释代码。当你完成这本书和我们的解释后,你仍然可以使用 Copilot 来帮助你理解它给出的下一部分复杂的代码。

  • 使代码更容易理解 — 有不同的方式来编写代码以完成同一任务。有些可能比其他更容易理解。你可以要求 Copilot 重新组织你的代码,使其更容易使用。易于阅读的代码在需要时通常更容易增强或修复。

  • 修复错误 — 一个 错误 是在编写程序时犯下的错误,可能会导致程序执行错误。有时,你的 Python 代码几乎可以工作,或者几乎总是可以工作,但在一种特定情况下却不行。如果你听过程序员的谈话,你可能听说过这样一个常见的例子:程序员可能花费数小时,最终只移除了一个导致程序失败的等号符号。那可不是几个小时的乐趣!在这些情况下,你可以尝试 Copilot 的功能,该功能可以帮助自动找到并修复程序中的错误。

  • 解释错误 — 如果你的代码没有正确工作,你通常会从 Python 运行时环境得到一个错误报告。有时,这些错误可能相当晦涩难懂,但 Copilot 可以帮助你解释错误,并指导你如何修复它。

  • 寻找 Python 库 — Python 是一种成熟的编程语言,拥有许多模块(库)可以帮助完成特定任务,例如数据分析、编写游戏、处理不同的图像文件格式等等。与 Copilot 进行一次简短的对话通常可以帮助你找到使你的工作变得更轻松的模块,并为你提供一些入门示例。

1.5 使用 Copilot 的风险和挑战

现在我们都对让 Copilot 为我们编写代码感到兴奋,我们需要谈谈使用 AI 助手固有的危险(参见参考文献[2]和[3],以详细说明这些观点的一些内容):

  • 版权—Copilot 通过使用人类编写的代码来学习编程。(当人们谈论像 Copilot 这样的 AI 工具时,你会听到他们使用“训练”这个词。在这个上下文中,训练是学习的另一个词。)更具体地说,它是通过使用包含开源代码的数百万个 GitHub 仓库进行训练的。一个担忧是 Copilot 会“窃取”那段代码并给我们。根据我们的经验,Copilot 很少建议大量他人的代码,但这种可能性是存在的。即使 Copilot 给出的代码是各种他人代码片段的融合和转换,也可能存在许可问题。例如,Copilot 产生的代码归谁所有?目前还没有关于这个问题的共识。Copilot 团队正在添加功能来帮助;例如,Copilot 可以告诉你它产生的代码是否与现有的代码相似以及该代码的许可情况[4]。自学和实验是很好的,我们鼓励这样做——但如果你打算将此代码用于家庭以外的目的,请务必小心。我们故意说得比较含糊,因为法律可能需要一段时间才能赶上这种新技术。在这些社会辩论进行时,最安全的做法是谨慎行事。

  • 教育—作为入门级编程课程的讲师,我们亲眼见证了 Copilot 在我们过去给学生布置的作业类型上的出色表现。在一项研究中[5],Copilot 被要求解决 166 个常见的入门级编程任务。它做得怎么样?在第一次尝试中,它解决了近 50%的问题。给 Copilot 更多的信息,这个数字会上升到 80%。鉴于像 Copilot 这样的工具,教育需要改变,目前讲师们正在讨论这些变化可能的样子。在一些学校,学生被允许使用 Copilot 来辅助他们的学习和作业。在其他学校,Copilot 在某些情况下(例如,考试)或对某些学生(计算机科学专业学生)是不被允许的。在许多学校,LLMs 被允许作为学生的辅导老师。在某些情况下,LLM 辅导老师只是像 Copilot 或 ChatGPT 这样的常规 LLM,但在其他情况下,LLM 界面已被改变以限制学生收到的答案类型。目前还太早知道 LLMs 将如何影响计算机教育,但这样的趋势已经初露端倪。

  • 代码质量—我们需要小心不要信任 Copilot,特别是对于敏感代码或需要安全的代码。例如,为医疗设备编写的代码或处理敏感用户数据的代码必须始终彻底理解。向 Copilot 请求代码,对它产生的代码感到惊奇,并未经审查就接受这些代码是很诱人的。但在本书中,我们将处理不会大规模部署的代码,因此,虽然我们将关注获取正确的代码,但我们不会担心使用此代码的更广泛用途的影响。我们还将建立你需要的独立确定代码是否正确的基础。

  • 代码安全—与代码质量一样,当我们从 Copilot 获取代码时,代码的安全性绝对不能得到保证。例如,如果我们正在处理用户数据,仅仅从 Copilot 获取代码是不够的。我们需要进行安全审计,并拥有专业知识来确定代码的安全性。然而,尽管如此,我们不会在实际场景中使用 Copilot 的代码,因此我们不会关注安全问题。

  • 非专家—成为专家的一个标志是意识到自己知道什么,同样重要的是,知道自己不知道什么。专家通常也能表达他们对回答的信心程度,如果他们不够自信,他们会进一步学习,直到他们确信自己知道。Copilot 和更广泛的 LLMs 则不具备这种能力。你问他们一个问题,他们就会直接回答,就是这样。如果需要,他们会编造:他们会将真实信息与垃圾信息混合,形成一个听起来合理但实际上毫无意义的回答。例如,我们见过 LLMs 为活着的人编造讣告,这显然是没有意义的,但这些“讣告”确实包含了一些关于人们生活的真实信息。当被问及算盘为什么能比电脑更快地完成数学运算时,我们见过 LLMs 给出听起来自信的回答——包括关于算盘是机械的,因此必然是最快的说法。LLMs 在这个领域正在进行工作,以便能够说,“抱歉,不,我不知道这个。”但我们还没有达到那个阶段。他们不知道自己不知道什么,这意味着他们需要监督。

  • 偏见——LLMs 会复制它们在训练数据中存在的相同偏见。如果你要求 Copilot 生成一个名字列表,它将主要生成英语名字。如果你要求一个图表,它可能产生的图表没有考虑人类之间的感知差异。而且,如果你要求代码,它可能产生一种特定群体编写代码风格的代码。(毕竟,在计算机科学中,人口群体得到了很好的代表,Copilot 就是在这些代码上训练的。)计算机科学和软件工程长期以来一直遭受着缺乏多样性的困扰。我们无法进一步压制多样性,我们需要扭转这一趋势。我们需要让更多的人加入,并允许他们以自己的方式表达自己。如何使用像 Copilot 这样的工具来处理这个问题目前正在被解决,这对编程的未来至关重要。然而,我们相信 Copilot 有潜力通过降低进入该领域的门槛来提高多样性。

1.6 我们需要的技能

如果 Copilot 可以编写我们的代码、解释它并修复其中的错误,我们是不是就完成了?我们是不是只需要告诉 Copilot 要做什么,然后庆祝我们的纯粹厉害?

不。首先,Copilot 可能会犯错误。它给出的代码可能在语法上是正确的,但有时它并不做我们想要它做的事情。我们需要保持警惕,以便在 Copilot 犯这些错误时能够捕捉到。其次,尽管程序员依赖的一些技能(例如,编写正确的语法)的重要性可能会降低,但其他技能仍然至关重要。例如,你不能把一个巨大的任务扔给 Copilot,比如,“制作一个视频游戏。哦,还要让它有趣。”Copilot 会失败的。相反,我们需要将这样一个大问题分解成 Copilot 可以帮助我们的小任务。我们如何那样分解问题?实际上并不容易。当与像 Copilot 这样的工具进行对话时,人类需要发展这种关键技能,我们在整本书中都在教授这种技能。

无论如何,其他技能在 Copilot 的帮助下可能会变得更加重要。测试代码在创建高质量代码中一直是一项关键任务。我们了解人类编写的代码的测试,因为我们知道在哪里寻找典型问题。我们知道人类经常在值的边界处犯编程错误。例如,如果我们编写了一个乘以两个数的程序,我们可能会得到大多数值正确,但可能不会在其中一个值为 0 时正确。那么,AI 编写的代码呢?20 行无瑕疵的代码可能隐藏着一行如此荒谬的代码,我们可能不会期望它在那里。我们没有这方面的经验。我们需要比以前更加仔细地测试。

我们还需要知道如何在代码出错时修复错误。这个过程被称为调试,并且仍然非常重要,尤其是当 Copilot 给你接近正确但还不够准确的代码时。

最后,一些必需的技能完全是新的。其中最主要的是被称为提示工程的技能,它涉及如何告诉 Copilot 要做什么。如前所述,当我们要求 Copilot 编写一些代码时,我们使用提示来发出请求。虽然我们可以用英语来编写这个提示并要求我们想要的东西,但这还不够。如果我们想让 Copilot 有做对事情的机会,我们需要非常精确。即使我们很精确,Copilot 仍然可能做错事情。在这种情况下,我们首先需要确定 Copilot 确实犯了一个错误。然后,我们可以尝试调整我们的描述,希望将其引导到正确的方向。根据我们的经验,对提示的看似微小的改变可能会对 Copilot 产生的结果产生不成比例的影响。在这本书中,我们将教你所有这些技能。

1.7 对像 Copilot 这样的 AI 代码助手的社会担忧

目前社会对像 Copilot 这样的 AI 代码助手存在不确定性。我们认为应该以几个问题和我们的当前答案结束本章。也许你自己也在思考这些问题!我们的答案可能最终会显得荒谬错误,但它们确实捕捉了我们作为两位致力于编程教学的教授和研究者的当前想法:

Q: 现在我们有了 Copilot,技术编程工作会减少吗?

A: 很可能不是。我们预期会改变的是这些工作的性质。例如,我们认为 Copilot 可以帮助完成许多通常与初级编程工作相关的任务。这并不意味着初级编程工作会消失,只是随着程序员能够利用越来越复杂的工具完成更多工作,这些工作会发生变化。

Q: Copilot 会扼杀人类的创造力吗?它会不会只是不断循环和重复人类已经编写过的代码,限制新想法的引入?

A: 我们怀疑不是。Copilot 帮助我们以更高的层次工作,远离底层机器代码、汇编代码或 Python 代码。计算机科学家使用术语抽象来指代我们与计算机底层细节断开连接的程度。抽象自计算机科学诞生以来一直在发生,我们似乎并未因此遭受损失。相反,它使我们能够忽略已经解决的问题,并专注于解决越来越广泛的问题。事实上,更好的编程语言的诞生促进了更好的软件的发展——当只有汇编语言时,Google 搜索、Amazon 购物车和 macOS 等软件都没有被编写(很可能也无法编写)!

Q: 我经常听到关于 ChatGPT 的消息。那是什么?它和 Copilot 一样吗?

A: 它与 Copilot 不同,但建立在相同的技术之上。然而,ChatGPT 并不是专注于代码,而是关注一般性的知识。因此,它已经渗透到比 Copilot 更广泛的各种任务中。例如,它可以回答问题,撰写文章,甚至在沃顿商学院的 MBA 考试中表现出色[6]。因此,教育将需要改变:我们不能让人们通过 ChatGPT 的方式获得 MBA!我们花费时间的有价值的方式可能会改变。人类是否会继续写书,如果是的话,会以什么方式?人们是否愿意阅读那些部分或全部由 AI 撰写的书籍?这将在包括金融、医疗保健和出版在内的各个行业产生影响[7]。同时,目前存在无节制的炒作,因此很难区分真相与虚构。这个问题由于一个简单的事实而加剧,即没有人知道长期会发生什么。罗伊·阿玛拉(被称为阿玛拉定律)提出了一句古老的谚语说:“我们往往高估了技术在短期内的影响,而低估了在长期的影响。”因此,我们需要尽我们所能关注讨论,以便我们可以相应地适应。

在下一章中,我们将指导您在电脑上开始使用 Copilot,以便您可以开始编写软件。

摘要

  • Copilot 是一个 AI 助手,它是一种 AI 代理,可以帮助您完成工作。

  • Copilot 改变了人类与计算机的互动方式,以及我们编写程序的方式。

  • Copilot 改变了我们需要磨练的技能的焦点(减少对语法的关注,更多关注问题分解和测试)。

  • Copilot 是非确定性的;有时它会产生正确的代码,有时则不会。我们需要保持警惕。

  • 关于代码版权、教育和职业培训以及 Copilot 结果中的偏见等问题仍需要解决。

第二章:使用 Copilot 开始

本章涵盖了

  • 在你的系统上设置 Python、Visual Studio Code 和 Copilot

  • 介绍 Copilot 的设计过程

  • 理解 Copilot 在数据处理任务中的价值

我们希望你能从一开始就能自己创建软件。为此,我们将引导你设置你的机器上的 Visual Studio Code(VS Code)、Python 和 Copilot,并让你熟悉如何与这些工具交互。在你设置好工具后,你将能够跟随我们的示例,并开始自己创建软件。实践是检验真理的唯一标准,我们相信你可以在我们这本书的剩余部分边学边实践。

一旦你设置了 Copilot,我们将通过一个展示 Copilot 在解决标准任务中强大功能的有趣示例来引导你。你会看到如何与 Copilot 交互,并学习你如何在不实际编写任何代码的情况下编写软件。记住,Copilot 并不完美,你需要能够阅读并理解一点 Python 才能得到你想要的结果,但它给了你一个很大的起点。让我们开始创建你的第一个计算机程序。

2.1 设置你的电脑开始学习

学习如何编写软件需要你超越仅仅阅读它,并实际执行编写软件的任务。如果这是一本关于如何弹吉他的书,你会不尝试弹吉他就继续阅读吗?我们认为你不会。不跟随并亲自尝试,就阅读这本书,就像看着马拉松运动员完成比赛,然后认为你准备好自己跑一场一样。我们将停止使用类比,但说真的,在我们继续前进之前,你需要安装并运行你的软件。

目前最让我们感到害怕的是,我们刚刚到达了一个新手们,甚至那些渴望学习编程的新手们往往会失败的最常见点,我们真的希望看到你成功。现在,你可能正在想,“等等,真的吗?我们才刚刚开始。”是的,这正是关键所在。在 Leo 在 Coursera 上关于学习 Java 编程的流行课程[1]中,你能猜到大多数新学习者离开的点是什么吗?是课程末尾的挑战性作业,涉及在实时地球上绘制地震标记吗?不是的。实际上,是热身作业,学习者必须设置他们的编程环境。因此,我们理解这可能对你来说是一个障碍。我们希望,通过这个不太明显的推动,我们可以帮助你实现你购买这本书时心中所设定的所有目标。一切从安装软件开始。

2.2 我们将使用的软件

为了轻松设置和使用 Copilot,我们将安装新手和软件工程师都会使用的软件编辑工具。你将使用的工具是 Python、VS Code、GitHub 和 Copilot。当然,如果你已经安装了所有这些工具,请跳转到 2.6.1 节。

2.2.1 Python

任何编程语言都可以用于这本书,但我们选择了 Python,因为它是世界上最受欢迎的编程语言之一,也是我们在大学入门课程中教授的语言。正如我们在第一章中提到的,与其它语言相比,Python 更易于阅读、理解和编写。对于这本书,Copilot 将主要生成代码,而不是你。然而,你将想要阅读并理解 Copilot 生成的代码,Python 在这方面做得很好。

2.2.2 Visual Studio Code

你可以使用任何文本编辑器来编程。然而,如果你想有一个良好的编程环境,可以编写代码,轻松地从 Copilot 获得建议,并运行你的代码,VS Code 是我们的首选工具。VS Code 被学习软件的初学者使用,并且受到学生的喜爱[2]。它也被全球的专业软件工程师广泛使用,这意味着在完成这本书后,你可以在使用这个环境的同时工作和学习。为了让 VS Code 适用于这本书,你需要安装一些扩展,以便使用 Python 和 Copilot,但 VS Code 的一个优点是安装这些扩展很容易。

2.2.3 GitHub 账户

GitHub 是开发、维护和存储软件的行业标准工具。然而,在这本书中,我们不会使用 GitHub。我们注册 GitHub 仅仅是因为你需要一个账户来访问 Copilot。注册 GitHub 账户是免费的,但截至写作时,他们为 Copilot 收费。如果你是学生,他们将免除这项费用。如果你不是学生,截至写作时,你可以获得 30 天的免费试用。

你可能会问为什么他们要收费,有一个很好的答案。构建 GPT 模型(想象有成千上万的计算机运行一年来构建模型)的成本很高,GitHub 通过提供模型预测来承担成本(许多机器接收你的输入,将其通过模型运行,并生成你的输出)。如果你还没有准备好使用 Copilot,你可以在注册后的大约 25 天内做一个日历提醒,如果你在那个时间没有使用 Copilot,只需取消即可。另一方面,如果你已经成功地学会了如何使用 Copilot 编写软件,并且正在用它来提高工作效率或作为爱好,那么保留它可能是有意义的。

在本章的整个过程中,我们将安装所有这些工具,但我们将其分为两部分进行。第一部分,将在下一节中介绍,将帮助你设置自己的环境以编写和运行代码,让你熟悉这个过程。第二部分将帮助你设置使用 Copilot 来辅助编写代码的环境。

2.3 设置你的系统:第一部分

在我们安装指南的第一部分,我们将安装 Python 和 VS Code。为了简化这一部分,我们只是概述了你应该遵循的主要步骤。然而,更详细的说明可以在以下位置找到:

  • VS Code 在 mng.bz/znjQ 提供了入门编写 Python 代码的教程。

  • 本书网站 (mng.bz/0M46) 提供了设置 PC 和 macOS 系统的详细说明。由于这些工具的网站可能会在我们写这本书之后发生变化,我们鼓励您结合使用 GitHub 链接和本书网站。

  • 在在线书籍论坛 (mng.bz/NBK1) 中,您可以寻求帮助并查看常见问题的答案列表。

您需要完成的步骤主要包括以下几步:

  1. 安装 Python:

  2. 安装 VS Code:

    • 访问 code.visualstudio.com/download,并选择适合您操作系统的主下载版本(例如,Windows 或 Mac)。

    • 下载并安装 VS Code 的最新版本。

  3. 安装 VS Code 扩展程序:(详情请见 mng.bz/9o01)。

    • Python (by Microsoft)—按照 mng.bz/j0gP 中的说明正确设置 Python 扩展程序(特别是选择正确的解释器)。

虽然这里的说明很简短,但我们知道在现实中它们可能需要一些时间。如果您遇到任何问题,请参考前面提到的资源以获取更详细的设置说明。

2.4 在 Visual Studio Code 中使用 Python

现在您的系统已经设置好了,让我们熟悉图 2.1 所示的 VS Code 界面。(您可能需要点击左上中的资源管理器图标以获得相同的视图。)图 2.1 中标识了以下区域:

  • 活动栏—在左侧远端是活动栏,我们可以在此打开文件文件夹(也称为目录)或安装扩展程序(就像您在上一节中安装 Python 扩展程序那样)。

  • 侧边栏—侧边栏显示活动栏中当前打开的内容。在图 2.1 中,活动栏显示了选中的资源管理器,因此侧边栏显示了当前文件夹中的文件。

  • 编辑器面板—这是我们创建软件的主要区域。编辑器面板中的编辑器类似于任何其他文本编辑器,您可以使用剪贴板编写、编辑、复制和粘贴文本。然而,编辑器是专门为与代码良好协作而设计的。在此阶段,我们将在此窗口中编写代码,但在此章的后面,您将主要通过请求 Copilot 生成代码并在该窗口中测试代码来工作。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/2-1.png

图 2.1 VS Code 界面 [3]
  • 输出和终端面板在这个界面区域,你将看到你的代码输出或任何在以下标签中发生的错误:问题、输出、调试控制台和终端。我们将主要使用问题标签,我们可以看到我们代码中的潜在错误,以及终端标签,它允许我们与 Python 交互并看到代码的输出。

VS Code 有许多不同的配色方案,所以你可以使用你喜欢的任何配色方案,因为功能是相同的。

2.4.1 设置你的工作文件夹

在 VS Code 左侧的活动栏顶部,你会找到作为顶部图标的资源管理器。点击资源管理器后,你应该会看到一个“未打开文件夹”的消息。点击按钮打开一个文件夹,并在你的电脑上选择一个文件夹(或者创建一个新的——我们喜欢命名为 fun_with_Copilot 的文件夹)。一旦你打开了这个文件夹,你的工作区将是你打开的文件夹,这意味着你应该在这个文件夹中拥有你的代码和任何数据文件,比如我们将在本章后面使用的那个文件。

文件未找到或文件缺失错误

如果你收到一个错误消息说缺少文件,不要灰心:这些是每个人都会犯的错误。在编写软件时,这些错误可能会非常令人烦恼。也许你只是没有把文件放在你的工作文件夹里——这种情况可能发生——但是通过复制或移动文件到正确的文件夹就可以轻松修复。然而,有时你会在文件夹中看到文件,但当你用 VS Code 运行你的代码时,Python 似乎找不到它。如果你遇到这种情况(在我们编写这本书的时候也遇到了这种情况!),请确保使用 VS Code 中的资源管理器(如图 2.1 中的侧边栏所示)打开包含代码和所需文件的文件夹。

2.4.2 检查你的设置是否正常工作

让我们检查我们是否正确设置了所有内容。为此,我们首先创建一个新的文件来存放我们的程序。你可以通过转到文件 > 新建文件(如图 2.2 所示),然后选择 Python 文件(如图 2.3 所示)来完成此操作。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/2-2.png

图 2.2 如何在 VS Code 中创建新文件

创建文件后,我们喜欢确保我们已经保存了文件。转到文件 > 另存为,并将此文件命名为 first_program.py。接下来,在文本编辑器中,准确地输入以下内容,就像这里显示的那样:

print("Hello World")

你可能已经猜到了当我们运行这个程序时会发生什么。它应该在屏幕上打印“Hello World”。让我们运行它看看!首先,你需要保存你的文件,方法是转到文件 > 保存。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/2-3.png

图 2.3 选择 Python 文件以创建新的 Python 文件。

警告:在运行文件之前务必保存!我们很不好意思地承认,我们曾经花费了大量时间试图修复那些正确但未保存的代码。

要运行你的程序,转到文本编辑器的右上角,并点击之前在图 2.1 中显示的运行代码图标。点击图标后,在底部的终端部分,你应该会看到类似以下的内容:

> & C:/Users/<YOUR_NAME>/AppData/Local/Programs/Python/Python312/python.exe 
↪ c:/Users/<YOUR_NAME>/Copilot/first_program.py
Hello World

以>开头的第一行是计算机运行你代码的命令,它只是告诉使用 Python 运行 first_program.py。第二行是运行命令的输出,显示“Hello World”,这是我们希望看到的。

恭喜!你已经编写了你的第一个程序!现在我们知道你的编程环境已经设置正确。让我们开始编写几段更小的代码,以便熟悉这个工作流程,然后我们将转向与 Copilot 一起的工作流程。

2.5 编写和运行一些小程序

你刚刚完成了安装 Python 和 VS Code 以及使其工作的大步骤。在我们开始使用我们的 AI 助手 Copilot 之前,让我们编写几个小程序,这样你就可以对输入和运行代码有所体会。

让我们从编写一个涉及加法的小程序开始。请删除你写的打印 Hello World 的行,并用以下代码替换:

a = 10
b = 20
print(a + b)

你认为这段代码会打印什么?它会打印“a + b”,还是会打印 30?请运行它看看结果。

计算机计算了 10 和 20 的和,并打印了 30。这很酷,对吧?我们使用计算机进行了一些小计算。如果你改变ab的值,你认为会发生什么?你可以自己尝试一下。

我们将在本书的后面花更多的时间讨论如何阅读代码的细节,但如果你好奇,以下是阅读该代码的方法:

a = 10     #1
b = 20           #2
print(a + b)   #3

#1 创建一个名为“a”的变量并将其值设为 10

#2 创建一个名为“b”的变量并将其值设为 20

#3 将“a”和“b”相加,然后将结果打印到屏幕上

当前关键的是让你习惯在编辑器窗格中输入代码并使用运行按钮来运行它。

让我们再举一个简单的例子。假设你想计算一个正方形的面积。回忆一下我们关于形状的公式,边长为s的正方形的面积如下:

面积 = s²

让我们从定义一个边长并打印面积开始:

side = 5
area = side * side
print("Side length:", side, " Square area is:", area)

我们鼓励你输入并运行这段代码。当你运行它时,你应该得到以下结果:

Side length: 5 Square area is: 25

你在输入这段代码时遇到任何问题吗?我们怀疑很多人会遇到。如果你漏掉了引号、逗号或括号,当你运行代码时,你可能会得到一些不愉快的错误,比如(这里,如果你忘了逗号):

File "c:\Users\Leo\Copilot\first_program.py", line 7
    print("Side length:" side, " Square area is:", area)
          ^^^^^^^^^^^^^^^^^^
SyntaxError: invalid syntax. Perhaps you forgot a comma?

在传统的编程学习方法中,你需要花费大量时间确保你理解了编写编程语言 Python 所需的精确字符。我们将再次强调好消息:有了像 Copilot 这样的 AI 助手,语法变得不再那么重要。让我们安装 Copilot,这样你就可以学习如何使用这个工具了。

2.6 设置系统:第二部分

现在,我们已经准备好安装指南的第二部分。这里有一些新的步骤,超出了你之前所做的工作。再次提醒,你可以自由地参考以下资源以获取更多关于安装过程的详细信息:

  • 访问 GitHub 的文档 mng.bz/WVP1

  • 该书的网站 (mng.bz/0M46) 提供了设置 PC 和 macOS 系统的详细说明。由于这些工具的网站可能会在我们写这本书之后发生变化,我们鼓励您结合使用 GitHub 链接和本书网站。

  • 在在线书籍论坛 (mng.bz/NBK1) 中,你可以寻求帮助并查看常见问题的答案。

这次你需要完成的主要步骤如下:

  1. 设置您的 GitHub 账户,并注册 Copilot:

    • 访问 github.com/signup,并注册一个 GitHub 账户。

    • 进入 GitHub 的设置并启用 Copilot。这是你需要验证你是学生或注册 30 天免费试用(在撰写本文时可用)的地方。

  2. 安装以下 VS Code 扩展程序(详细信息请参阅 mng.bz/9o01):

    • GitHub Copilot(由 GitHub 提供)——在撰写本文时,安装 GitHub Copilot 会自动安装 GitHub Copilot Chat。请检查 GitHub Copilot Chat(由 GitHub 提供)是否也已安装,你可以通过检查已安装的扩展来确认。如果没有安装,你需要添加此扩展。

我们知道这里的步骤很简短。如果你遇到任何问题,我们鼓励你参考前面提到的资源以获取更详细的设置说明。

2.6.1 检查 Copilot 是否正常工作

如果你的安装工作正常,你应该在 VS Code 界面的右下角看到 Copilot 图标(如图 2.4 所示)。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/2-4.png

图 2.4 运行 Copilot 的 VS Code 界面

让我们检查 Copilot 是否正常工作,通过编辑 first_program.py 文件。你可以自由地删除当前文件中的任何代码并从头开始,或者创建一个新的 .py 文件——由你决定。在文本编辑器中,输入以下内容:

# output "Hello Copilot" to the screen

我们编写的提示和代码将以粗体显示,以帮助我们区分我们编写的内容,以及 Copilot 可能给出的代码和注释。起始处的 # 符号很重要(你应该将其包含在你输入的内容中)。这意味着你编写的内容是一个注释(根据你的 VS Code 颜色方案,它可能与我们即将生成的代码颜色不同)。注释不是代码:计算机执行代码,不执行注释。程序员使用注释来提供代码对其他软件工程师可读的摘要,以帮助阅读代码。今天,它的用途已经扩展到也可以提示 Copilot。在编写注释(有时甚至在编写注释时),Copilot 将尝试给我们提供建议。你可以将其视为一种更复杂的自动完成功能,就像你在搜索引擎中键入“New York T”时,它会自动完成为“New York Times”。

要触发 Copilot 开始提供代码(或更多注释),请在行尾按 Enter 键,然后你会处于新行的开始处。暂停片刻,你应该会看到一些东西出现。直到被接受,Copilot 的建议将以浅灰色斜体显示。如果你还没有收到建议,你可能需要再次按 Enter 键来触发 Copilot 提供代码。以下是我们遇到的情况:

# output "Hello Copilot" to the screen
*`*print("Hello Copilot")*`*

如果你仍然没有看到 Copilot 的建议,尝试按 Ctrl-Enter(按住 Ctrl 键的同时按 Enter 键)。当你按 Ctrl-Enter 时,一个新窗口将在你的编辑器窗口右侧出现,该程序称为 GitHub Copilot 建议。如果该窗口没有出现,可能你的设置有问题,我们鼓励你访问书籍网站,检查你是否正确地遵循了所有步骤,或者寻找(或请求)帮助。

如果你看到了 Copilot 的建议,按 Tab 键接受 Copilot 的建议。一旦这样做,之前以浅灰色斜体显示的建议现在应该以标准字体显示:

# output "Hello Copilot" to the screen       #1
print("Hello Copilot")     #2

#1 我们编写的提示

#2 Copilot 生成的代码

如果你看到与此不同的代码,那是因为我们之前提到的原因:Copilot 是非确定性的,所以你可能看到的代码与我们不同。我们提到这一点是因为有时 Copilot 在这里的代码中会犯一些小错误,可能会给你提供类似这样的代码:

print "Hello Copilot"

你可能会认为这个细微的差异("Hello""Copilot"之间没有括号)不会影响什么,但实际上它会影响。在 Python 3 之前,这是打印语句的正确语法,而当 Python 3 被引入时,它切换到了带括号的代码。因为我们正在运行 Python 3,所以你需要有括号才能使代码工作。你可能会问为什么 Copilot 会出错。问题在于 Copilot 在训练过程中使用了部分旧的 Python 代码。如果这让你感到烦恼,我们表示同意。但这又是新手程序员在 Copilot 出现之前所经历的挫折的另一个提示。Copilot 建议的大部分内容在语法上是正确的。但如果你是一个从头开始编写代码的新手,遗漏括号或某处缺少冒号可能会让你浪费很多时间。现在我们有了正确的代码,

# output "Hello Copilot" to the screen
print("Hello Copilot")

这与我们在之前手动编写的“Hello World”代码类似。我们希望看到 Copilot 能生成这样的代码能让你感到鼓舞!

现在,让我们看看它在确定上节中正方形面积的例子中的表现。请输入以下代码(再次,以粗体显示):

side = 5
area = side * side
# Print the side length is *** and the area is ***

我们从 Copilot 那里收到了以下回应:

print("The side length is", side, "and the area is", area)

注意 Copilot 如何处理这段代码。对于人类来说,尤其是编程新手,犯错很容易,但 Copilot 很少出错。我们希望 Copilot 给了你类似的回应。如果你想要更熟悉界面,不妨随意用 Copilot 试一试。

现在我们知道你的编程环境和 Copilot 都设置正确了,所以我们可以进入一个更大的编程任务。但在我们这样做之前,我们想回顾一下我们在与 Copilot 一起工作时遇到的常见问题的处理技巧,这样你就可以在处理下一个例子时使用这些技巧。

2.7 解决常见的 Copilot 挑战

现在讨论 Copilot 的常见挑战可能似乎还为时尚早,但你可能在编写第一个程序时就已经遇到了挑战。当你处理我们接下来的例子和下一章的内容时,你肯定会遇到一些这些挑战,所以我们现在就想把这些信息提供给你。

在我们与 Copilot 一起工作的这段时间里,我们遇到了一些常见的挑战。随着 Copilot 的改进,这些挑战可能会减少,但它们在撰写本文时仍然是问题。尽管表 2.1 中的挑战不是你可能会遇到的唯一挑战,但我们希望我们关于如何解决这些常见挑战的建议能帮助你快速上手。我们将在我们书籍的网站上保持一个持续更新的列表,所以如果你觉得我们遗漏了什么,请随时在论坛上联系我们。

表 2.1 与 Copilot 一起工作的常见挑战
挑战 描述 解决方案

| 只有注释 | 如果你使用注释符号(#)给 Copilot 一个提示,并在新的一行开始时,它只想给你更多的注释而不是代码。例如:# output "Hello Copilot" to the screen

# print "Hello world" to the screen

我们看到 Copilot 生成了一行又一行的注释,有时甚至重复自己!当这种情况发生时,右侧列中的 3 号补救措施(使用 docstrings)有时是最有效的。

| 1. 在你的注释和 Copilot 的建议之间添加一个换行符(按 Enter 键),以帮助它从注释切换到代码。2. 如果换行符不起作用,你可以输入一两个字母的代码(没有注释符号)。通常,你的提示中的关键词的一两个字母会起作用。例如:

# output "Hello Copilot" to the screen

pr

关键词的一两个字母通常会导致 Copilot 提供代码建议。

  1. 将使用 # 注释切换到类似这样的 docstring 注释:

""

output "Hello Copilot" to the screen

""

  1. 使用 Ctrl-Enter 查看是否 Copilot 会给你代码而不是注释的建议。

|

| 错误的代码 | 有时 Copilot 从一开始就给你显然错误的代码。(你将在本书中学习如何识别错误的代码!)此外,有时 Copilot 似乎陷入了错误的道路。例如,它可能看起来在试图解决与你要求它解决的问题不同的另一个问题。(特别是补救措施 3 可以帮助 Copilot 走上一条新的道路。)

| 本书的大部分内容是关于如何解决这个问题,但这里有一些快速提示来帮助 Copilot:1. 改变你的提示,看看你是否能更好地描述你需要的内容。

  1. 尝试使用 Ctrl-Enter 来找到 Copilot 提供的正确代码建议。

  2. 关闭 VS Code 程序,稍等片刻,然后重新启动。这有助于清除 Copilot 缓存以获取新的建议。

  3. 尝试将问题分解成更小的步骤(更多细节见第七章)。

  4. 调试代码(见第八章)。

  5. 尝试向 ChatGPT 请求代码,并将它的建议粘贴到 VS Code 中。不同的大型语言模型(LLM)有时会给出有助于其他 LLM 解锁的建议。

|

| Copilot 给你 # YOUR CODE HERE | 我们看到 Copilot 似乎在提示我们编写自己的代码,通过在提示后生成此文本(或类似文本)来实现:# YOUR CODE HERE

| 我们认为这是当我们要求 Copilot 解决一个过去由教师分配给学生解决的问题时发生的。为什么?好吧,当我们为学生编写作业时,我们(作为教师)通常会编写一些代码,然后告诉学生通过编写 # YOUR CODE HERE 来编写其余部分

我们希望学生在那里编写他们的代码。学生往往会将此注释留在他们的解决方案代码中,这意味着 Copilot 被训练成认为这个注释是解决方案的一个重要部分(其实不是)。通常,我们能够通过使用 Ctrl-Enter 在 Copilot 的建议中找到合理的解决方案来解决这个问题,但如果这不起作用,请参阅关于错误代码的补救措施。

|

缺少模块 Copilot 为你提供了代码,但由于缺少模块,它将无法工作。(模块是可添加到 Python 中以提供预构建功能的其他库。) 在 2.8.2 节中,查看“Python 模块”侧边栏以了解模块的描述,并在第五章中查看如何使用模块的说明。

2.8 我们的前进道路

您已经看到 Copilot 可以为您生成代码。那么,我们能否让 Copilot 为我们解决任务呢?我们担心不能。我们仍然需要学习一些基本技能,以便能够有效地与 Copilot 合作并设计对我们有意义的软件。

为了帮助您欣赏您需要学习的技能,并让您对您将要去往的方向感到兴奋,我们将提供一个更大的示例,代表我们希望您在阅读和完成这本书的过程中大约一半的时候能够独立完成的事情。

2.8.1 我们将在整本书中如何与 Copilot 合作

我们将在后面的章节中扩展这个工作流程,但核心元素将是相同的:

  1. 使用注释(#)或文档字符串(""")向 Copilot 编写提示。

  2. 让 Copilot 为您生成代码。

  3. 通过阅读和测试来检查代码是否正确:

    • 如果它起作用,请转到步骤 1 以进行您想要它执行的下一步。

    • 如果不起作用,请从 Copilot 中删除代码,回到步骤 1,并修改提示(并查看之前在表 2.1 中显示的补救措施)。

2.8.2 展示 Copilot 在数据处理任务中的价值

对于这个更大的示例,我们希望您关注我们与工具的交互以及我们需要的技能。我们不期望您在本书的较晚部分理解代码。我们提供代码仅是为了让您看到 Copilot 为我们提供了什么,但您不需要在本章中尝试理解代码。

您可以跟随我们完成这个示例,或者只是阅读它。我们还没有给您提供在遇到困难时继续前进的技能,所以您可以等到稍后的章节再尝试完成这样的示例。我们假设在本节的其余部分,您只是会阅读。

我们希望从一些数据处理开始,因为这是你们很多人在个人或职业生活中可能做过的事情。为了找到一个数据集,我们访问了一个叫做 Kaggle [4] 的优秀网站,它提供了大量免费的数据集供使用。其中许多包含重要的数据,如不同国家的健康统计数据、帮助追踪疾病传播的信息等等。我们不会使用这些数据,因为我们希望我们的第一个程序更轻量。由于我们两个都是美式足球迷,我们将使用国家橄榄球联盟(NFL)的进攻统计数据数据库。如果你想自己查看数据集,这里有一个链接:mng.bz/86pw。这个数据集包含了 2019 年至 2022 年的 NFL 信息(见图 2.5)。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/2-5.png

图 2.5 nfl_offensive_stats.csv 数据集的前几列和行
第 1 步:了解我们的数据集

nfl_offensive_stats.csv 文件是一个逗号分隔值(CSV)文本文件(参见图 2.5 中的文件部分)。这是存储数据的标准格式。它顶部有一行标题,解释了每一列的内容。我们(或计算机)知道列之间的边界是通过在单元格之间使用逗号来确定的。注意,每一行都放在自己的行上。好消息是:Python 有很多读取 CSV 文件的工具。

让我们先探索一下这个文件中存储了什么内容。为了预览文件的内容,你可以在 Kaggle 网页上的“详情”部分查看它,或者下载并使用 VS Code 或 Microsoft Excel 等电子表格软件打开它。不用担心现在就下载和使用这个文件,那将在本书的后面部分进行。然而,如果你下载并打开它,这里就是标题(顶部)行的开始(如图 2.5 所示):

game_id,player_id,position ,player,team,pass_cmp,pass_att,pass_yds,

有更多的列,但对我们第一个任务来说,这些已经足够了。我们想知道在这段时间内哪个四分卫的表现最好。

第 2 步:在这段时间内,所有四分卫的表现如何?

如果你不懂美式足球,那没关系。你不需要了解这项运动就能跟上。四分卫负责传球。因此,我们将从查看他们在那个时间段内传了多少码开始。我们只想与其他四分卫进行比较,因为他们是负责传球球的球员。当然,一名跑卫可能一季只会传球一次,当他们这样做时通常很有趣,但这并不是他们的本职工作。为了只包括四分卫,我们需要告诉 Copilot 将我们的结果限制在那些位置(第三列)是QB(代表四分卫)的球员。为了解决这个问题,我们创建了一个名为 nfl_stats.py 的新 Python 文件,并与 Copilot 进行了对话,你可以看到在列表 2.1 中。

列表 2.1 Copilot 分析顶级四分卫的代码
"""                                                        #1
**open the csv file called "nfl_offensive_stats.csv" and** 
**read in the csv data from the file** 
**"""** 

# import the csv module      #2
import csv

# open the csv file
with open('nfl_offensive_stats.csv', 'r') as f:
    # read the csv data
    data = list(csv.reader(f))

**"""                                                        ** #3
**the 3rd column in data is player position, the fourth      ** #3
**column is the player, and the 8th column is the            ** #3
**passing yards. For each player whose position in           ** #3
**column 3 is "QB", determine the sum of yards from          ** #3
**column 8                                                   ** #3
**"""                                                        ** #3

# create a dictionary to hold the player name and passing yards
passing_yards = {}
# loop through the data
for row in data:
    # check if the player is a quarterback
    if row[2] == 'QB':
        # check if the player is already in the dictionary
        if row[3] in passing_yards:
            # add the passing yards to the existing value
            passing_yards[row[3]] += int(row[7])
        else:
            # add the player to the dictionary
            passing_yards[row[3]] = int(row[7])

**"""                                                 ** #4
**print the sum of the passing yards sorted by sum    ** #4
**of passing yards in descending order                ** #4
**"""                                                 ** #4
for player in sorted(passing_yards, key=passing_yards.get, reverse=True):
    print(player, passing_yards[player])

#1 首次读取数据的提示

#2 Copilot 的响应有时会包含注释。我们从 Copilot 那里得到的注释不会加粗,与加粗的提示不同。

#3 我们的第二个提示是分析数据

#4 我们的第三个提示是打印结果

注意到我们给了 Copilot 三个提示。第一个是处理输入数据,第二个是处理数据,第三个是输出响应。这种输入数据、处理数据和输出数据的循环在编程任务中非常常见。

如果你选择跟随我们,并将这些提示给 Copilot,你可能会得到不同的代码。这没关系,因为 Copilot 是非确定性的,所以它不会总是给出相同的结果。然而,如果你得到了不同的代码,我们还没有提供工具让你知道它是否正确。因此,如果你愿意跟随,请随意使用我们的代码。

提醒:Copilot 是非确定性的

正如第一章和本章所述,Copilot 是非确定性的,所以 Copilot 给你提供的内容可能与我们的不同。这将是本书剩余部分的一个挑战:当我们得到正确结果时,如果你得到错误结果,你会怎么做?我们相当有信心 Copilot 会在这里给你一个正确的答案,但如果你从 Copilot 那里得到一个错误答案,请继续阅读本节的剩余部分,而不是在 VS Code 中与 Copilot 一起工作。当 Copilot 给你一个错误答案时,我们绝对会给你所有需要的工具来修复代码,但这项技能将在本书的剩余部分教授,所以我们不希望你现在就卡在这里。

看着 Copilot 的结果,我们不得不说这相当令人印象深刻。你目前不需要担心代码,但请注意,从头开始编写这段代码对许多新学习者来说可能很困难。好消息是,任何人都可以使用这个强大的工具来做很多没有它不可能做到的事情。

第 3 步:使用 Copilot 修改代码以排除一个玩家

我们不仅可以用 Copilot 生成新代码,还可以修改它。如果我们从 2.1 列表中的数据集运行该代码,以下是输出中的前五行:

Patrick Mahomes 16132
Tom Brady 15876
Aaron Rodgers 13852
Josh Allen 13758
Derek Carr 13271

如果你关注足球,这些结果对你来说可能不会感到惊讶。为了看看 Copilot 能多好地适应我们的愿望,让我们尝试进行一个小的修改。假设因为汤姆·布雷迪已经被公认为有史以来最优秀的四分卫之一,你更愿意从这次比较中省略他。

为了进行这个更改,我们只需修改底部的提示。我们到了代码中写着以下内容的地方:

"""
print the sum of the passing yards sorted by sum
of passing yards in descending order 
"""
for player in sorted(passing_yards, key=passing_yards.get, reverse=True):
    print(player, passing_yards[player])

我们删除了代码,只留下注释,并在文档字符串中添加了另一行:

"""
print the sum of the passing yards sorted by sum
of passing yards in descending order 
Do not include Tom Brady because he wins too much
"""

Copilot 随后向我们建议了以下内容:

for player in sorted(passing_yards, key=passing_yards.get, reverse=True):
    if player != "Tom Brady":                  #1
        print(player, passing_yards[player])

#1 从数据中排除汤姆·布雷迪的代码

这正是我们希望在代码中看到的变化。(感谢汤姆·布雷迪在这个例子中表现得如此好。)代码在打印结果时排除了所有关于汤姆·布雷迪的数据。当我们保存文件并再次运行时,前五行现在看起来像这样:

Patrick Mahomes 16132
Aaron Rodgers 13852
Josh Allen 13758
Derek Carr 13271
Matt Ryan 13015
反思这个例子和我们需要用 Copilot 编写代码的技能

尽管我们没有自己编写任何代码,但我们能够解决一个大型现实世界任务。然而,你仍然需要学习一些基本的编程技能。我们将在接下来的章节中通过解决各种任务来教你这些技能。到第七章结束时,你将能够解决这样的大型任务。我们希望你能从这个例子中吸取以下内容:

  • Copilot 是一个强大的工具。 我们没有自己编写任何代码,但能够让 Copilot 生成执行一些初步数据分析所需的代码。如果你使用过电子表格,你可能会想到使用像 Microsoft Excel 这样的电子表格应用程序来完成这项任务,但这可能不像编写这样的代码那样容易。即使你以前没有使用过电子表格,你也必须承认,编写基本、可读的提示能够生成这样的正确代码和输出是令人惊讶的。

  • 将问题分解成小任务是重要的。 对于这个例子,我们尝试通过一个单一的较大提示(未显示)或将其分解成更小的任务来编写这段代码。我们发现,当给 Copilot 一个较大的任务而不是多个较小的任务时,Copilot 更容易出错。一般来说,将问题分解成更小的任务会显著增加 Copilot 生成正确代码的可能性。你将在本书的剩余部分看到如何将较大的问题分解成较小的任务,因为这是你需要掌握的最重要技能之一。实际上,即将到来的第三章将帮助你开始理解哪些任务被认为是合理的 Copilot 任务,而第七章将完全致力于学习如何将较大的程序分解成较小的部分。

  • 我们仍然需要在某种程度上理解代码。 这有几个原因。其中一个原因是编写好的提示需要基本了解计算机知道什么以及不知道什么。我们不能只是给 Copilot 一个提示说,“给我汤姆·布雷迪的传球码数。”Copilot 可能无法弄清楚数据存储在哪里,数据的格式是什么,哪些列对应球员和传球码数,或者汤姆·布雷迪是一名球员。我们必须将这些细节详细地告诉 Copilot,以便它能够成功。另一个原因与确定 Copilot 生成的代码是否合理有关。当我们两个人阅读 Copilot 的响应时,我们能确定代码在做什么,因为我们知道如何阅读代码。你需要在某种程度上能够做到这一点,这就是为什么第四章和第五章专门用于阅读代码。

  • 测试很重要。 我们需要知道 Copilot 给出的代码是否正确!当程序员谈论测试时,他们指的是确保他们的代码在可能的情况下正确工作的实践。我们在这个方面没有花太多时间,除了检查 Copilot 的答案是否合理,仅使用一个数据集的估计,但总的来说,我们需要在测试上花费更多的时间,因为这是代码编写过程中的一个关键部分。可能不用说,但代码中的错误从尴尬(如果你告诉你的 NFL 铁杆粉丝朋友一个球员的错误传球码数)到危险(如果汽车中的软件表现不当)再到昂贵(如果企业基于错误的分析做出决策)。在你学会阅读代码之后,我们亲身体验到,即使代码看起来是正确的,它可能也不是!为了解决这个问题,我们必须测试 Copilot 创建的每一块代码,以确保它按预期工作。你将在后面的章节中学习如何严格测试 Copilot 的代码,特别是第六章,该章节专门介绍这项技能。

  • Python 非常强大。 如果你错过了这一点,也没关系,但在列表 2.1 中,代码从 Python 库中导入了一个模块(import csv)。Python 有许多内置工具(称为库)来帮助人们完成任务。在这种情况下,它是导入一个库来使读取和修改 CSV 文件变得更加容易。我们将在后面的章节中利用更多的这些库。

Python 模块

Python 模块扩展了编程语言的功能。Python 中有许多模块,它们可以帮助你从数据分析到创建网站再到编写视频游戏做任何事情。你可以通过代码中的import语句来识别代码想要使用 Python 模块。一些模块是 Python 内置的,但还有数千个不是,如果你想要使用它们,你需要手动安装。第五章将向你展示如何使用模块。

在本章中,我们取得了巨大的成就!如果你已经完成了编程环境的设置,并开始使用 Copilot 编写代码,你应该感到自豪。你朝着编写软件迈出了巨大的一步!除了设置环境的细节之外,通过一起工作,你已经看到了我们的方向。

在本章的示例中,Copilot 为我们提供了我们想要的代码,而无需我们更改提示或调试代码来找出为什么它不能正常工作。这是一种展示使用 AI 助手编程力量的好方法,但你会发现,你经常需要测试代码,更改提示,有时甚至试图理解代码为什么是错误的。这是你将在接下来的章节中了解更多内容的 AI 助手编程过程。

摘要

  • 你已经安装了 Python 和 VS Code,并设置了 Copilot,这样你就可以与本书一起工作,并开始自己编写代码。

  • VS Code 界面包含文件管理、代码编辑和运行代码的区域,这些功能将在整本书中使用。

  • 提示是我们告诉 Copilot 生成代码的方式,如果编写得仔细,可以是一种创建软件的高效方法。

  • 数据分析是常见的编程任务,使用 CSV 文件是存储计算机要处理的数据的常见方式。

  • Copilot 是一个强大的工具,在适当的人类监督下可以生成复杂的代码。

第三章:设计函数

本章涵盖

  • Python 中的函数及其在软件设计中的作用

  • 与 Copilot 交互的标准工作流程

  • 使用 Copilot 编写良好函数的示例

  • Copilot 可以解决的合理任务

对于编程新手来说,最难的一个挑战是知道给 Copilot 分配什么合理的任务,以便它能找到好的解决方案。如果你给 Copilot 分配的任务太大,它通常会以非常壮观的方式失败,这可能非常难以修复。那么,什么是一个合理的任务呢?

这个问题对于我们的 Copilot 使用很重要,但它远远超出了这个范围。人类程序员也会遇到复杂性问题。如果经验丰富的软件工程师试图在不将其分解为更小的、更易解决的子部分的情况下编写代码来解决过于复杂的问题,他们通常也会遇到麻烦。对于人类来说,解决方案是使用一种称为“函数”的东西,其任务是执行一个任务。函数帮助我们组织代码,并使其更容易思考并修复任何错误。关于如何编写合理函数有一些经验法则,但本质上,这些规则试图捕捉到编写(1)执行单个任务且(2)不会过于复杂以至于难以正确执行的东西。

对于那些以传统方式学习编程的人来说,没有 Copilot 的帮助,他们可能在接触到函数之前,就已经在 5-10 行长的代码中挣扎于语法问题。在那个阶段,他们通过经验自然地理解到,他们不应该在一个函数中编写超出自己能够测试和调试能力的代码。因为你是学习与 Copilot 合作而不是直接与语法打交道,所以本章的任务是教你关于函数以及什么被认为是合理和不合理的任务来请求 Copilot 在单个函数中解决。

为了帮助你了解函数,我们将在本章提供一些示例。这些示例采用了与 Copilot 交互的核心工作流程——具体来说,是编写提示、从 Copilot 接收代码以及测试该代码是否正确的过程。在 Copilot 生成的函数中,你将开始看到核心编程工具,如循环、条件和列表,我们将在接下来的两章中对其进行扩展。

3.1 函数

在我们学习编写函数的细节之前,我们需要了解它们在软件中的目的。函数是帮助完成更大任务的较小任务,反过来,这些任务又帮助解决更大的任务,如此类推。你可能已经对将大任务分解为小任务有很多直觉。让我们用一个例子来具体化这种直觉。

假设你在报纸上找到了一个你想要解决的单词搜索谜题(见图 3.1 中的示例谜题)。在这些类型的谜题中,你需要找到单词列表中的每个单词。单词可以从左到右、从右到左、从上到下或从下到上找到。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/3-1.png

图 3.1 示例单词搜索谜题

在高层次上,你的任务是“找到单词搜索中的所有单词。”不幸的是,这个任务的描述本身并没有帮助。它没有告诉我们需要采取哪些步骤来解决问题。

现在尝试花几分钟时间解决这个问题。你是如何开始的?你是如何将整体任务分解,使其更容易实现的?

你可能会这样做,比如说,“好吧,找到每个单词是一个大任务,但一个更小的任务是找到第一个单词(CAT)。让我先从那开始!”这是一个将大任务分解成小任务的例子。因此,为了解决整个谜题,你可以为需要找到的每个单词重复执行这个较小的任务。

现在,我们如何找到一个单独的单词,比如 CAT?甚至这个任务也可以进一步分解,使其更容易完成。例如,我们可以将其分解为四个任务:从左到右搜索 CAT,从右到左搜索 CAT,从上到下搜索 CAT,以及从下到上搜索 CAT。我们不仅使任务越来越简单,而且还将我们的工作组织成逻辑部分。最重要的是,正如我们将在本章中看到的那样,我们将要求 Copilot 编写这些简单任务的代码,并将其最终组装成我们的完整程序。

将一个大问题分解成更小的任务称为问题分解,这是软件设计中的一项重要技能,我们在第七章中专门对其进行了讨论。目前,了解何时一个任务太大,以至于不能让 Copilot 完成它是很重要的。让 Copilot 制作一个结合 Wordscapes 和 Wordle 的新视频游戏是完全行不通的。然而,你可以让 Copilot 编写一个对解决更大问题很重要的函数;例如,你可能有一个函数,用于检查玩家提供的单词是否在有效单词列表中。Copilot 可以很好地解决这个问题,而这个函数将帮助 Copilot 更接近解决更大的问题。

3.1.1 函数的组成部分

“函数”这个名字的起源可以追溯到数学,在数学中,函数根据输入定义某物的输出。例如,在高中数学中,你可能学习了以下常见的函数写法:

f(x) = x²

这是一个给定变量x的值的函数,并计算该值的平方。因此,当x为 6 时,f(x)是 36。由于编程函数也有特定输入的预期输出,所以这个名字对编程来说也很合适。

作为程序员,我们也喜欢将函数视为承诺或合同。如果一个名为 larger 的函数被告诉我们它接受两个数字并给出这两个数字中较大的一个,我们就相信当我们给这个函数数字 2 和 5 时,它会返回答案 5。我们不需要看到这个函数是如何工作的就能使用它,就像我们不需要知道汽车的工作原理就能使用刹车踏板一样。按下刹车踏板,汽车就会减速。给 larger 函数两个数字,它会返回这两个数字中较大的一个。

Python 中的每个函数都有一个 函数头(也称为 函数签名),它是函数的第一行代码。鉴于它们的普遍性,我们希望阅读和编写函数头。函数头描述了函数的名称和它的输入。在其他一些语言中,函数头有时会包含有关输出外观的信息,但在 Python 中,你必须在代码的其他地方找到它。

在第二章中,我们写了 # 注释来告诉 Copilot 要做什么。如果我们想让 Copilot 生成一个函数,我们可以继续使用那种方法。例如,我们可以使用注释来要求 Copilot 编写一个函数,告诉我们两个数字中哪个更大:

# write a function that returns the larger of two numbers
# input is two numbers
# output is the larger of the two numbers
def larger(num1, num2):          #1
    if num1 > num2:        #2
        return num1        #2
    else:                  #2
        return num2        #2

#1 这个函数头定义了一个名为“larger”的函数,它接受两个输入,分别称为 num1 和 num2。

#2 函数体

与上一章的代码一样,我们只是写了注释来提示 Copilot 给我们代码。函数头有三个主要组成部分:def 关键字,它告诉 Python 这是一个函数;函数的名称;以及函数的输入。行尾还有一个冒号——一定要包含它,否则代码将不是有效的 Python 代码。单词 def 表示它正在创建(定义)一个函数。def 之后是函数的名称;这个名字应该尽可能地描述函数的行为。这个函数的名称是 larger。如果一个函数很难命名因为它做了很多不同的事情,这通常是一个线索,表明这个任务对于单个函数来说太大(关于这一点稍后还会讨论)。

在函数声明的括号中,你会找到参数。参数 是你向函数提供它运行所需信息的方式。一个函数可以有任意数量的参数,有些函数没有参数。这个函数有两个参数,分别命名为 num1num2;有两个参数是因为它需要知道它正在比较的两个数字。

函数只能有一个输出;在确定函数输出内容时需要查找的关键字是 returnreturn 后面跟随的内容即为函数的输出。在这个代码中,num1num2 将会被返回。函数不一定要返回任何内容(例如,一个将列表打印到屏幕上的函数没有必要返回任何内容),所以如果你没有看到返回语句,这并不一定是一个问题,因为函数可能正在做其他事情(例如与用户交互)而不是返回内容。函数必须要么返回内容,要么不返回内容:它们不能在某些情况下返回内容,在其他情况下不返回内容。

尽管我们使用 # 注释让 Copilot 生成这个函数,但实际上这对 Copilot 来说是一项大量工作。它首先必须正确获取头部,包括确定你需要多少个参数。然后,它必须正确获取函数的实际代码。在这个例子中,Copilot 还提供了函数的代码(也称为函数的主体)。

有一种方法可以提示 Copilot 为函数编写代码,这可能有助于它更准确地生成代码,并可能帮助我们更好地理解我们想要函数做什么。这涉及到编写文档字符串,我们将使用文档字符串来编写本书的大多数函数。

文档字符串解释函数行为

文档字符串 是程序员描述 Python 函数的方式。它们跟在函数头部之后,并以三个双引号开始和结束,如即将展示的 larger 函数的代码所示。

通过编写头部和文档字符串,你会让 Copilot 更容易地生成正确的代码。在头部,你将决定函数的名称,并提供函数要使用的每个参数的名称。在函数头部之后,你将提供一个文档字符串,告诉 Copilot 函数做什么。然后,就像之前一样,Copilot 将为函数生成代码。因为我们给了 Copilot 函数头部,它将能够从头部学习,并减少出错的可能性。以下是在编写相同的 larger 函数时,这种替代方法的样子:

def larger(num1, num2): 
 """ **#1
 **num1 and num2 are two numbers.** #1
 #1
 **Return the larger of the two numbers.** #1
 **"""** #1
    if num1 > num2: #1
        return num1
    else:
        return num2**

#1 函数的文档字符串描述 **注意,我们不仅写了函数头部,还写了文档字符串,Copilot 提供了函数的主体。

3.1.2 使用函数

一旦我们有了函数,我们如何使用它?回想一下我们的 f(x) = x² 类比,我们如何给 x 赋予 6 的值,以便它返回 36?让我们看看如何通过使用我们刚刚编写的 larger 函数来用代码实现这一点。

使用函数的方法是调用它。调用函数意味着在特定参数值上调用函数。这些参数值被称为参数。Python 中的每个值都有一个类型,我们需要注意给出正确类型的值。例如,larger函数期望两个数字;如果我们提供非数字的输入,它可能不会按预期工作。当我们调用函数时,它会运行其代码并返回结果。我们需要捕获这个结果,以便稍后使用;否则,它将会丢失。要捕获结果,我们使用一个变量,它只是一个指向值的名称。

在这里,我们要求 Copilot 调用函数,将结果存储在变量中,然后打印结果:

# call the larger function with the values 3 and 5
# store the result in a variable called result
# then print result
result = larger(3, 5)      #1
print(result)

#1 使用值 3 和 5 作为输入调用较大的函数并存储结果

代码正确地调用了larger。注意,它将我们要比较的两个值放在了开括号之后。当函数执行完毕后,它返回一个值,我们将该值赋给结果。然后我们打印结果。如果你运行这个程序,你会看到输出 5,因为 5 是我们询问的两个值中较大的一个。

如果你在这里对所有的细节都不舒服,那没关系,但我们希望你能认识到当函数被调用时,如下所示:

larger(3, 5)

函数调用的通用格式是

function_name(argument1, argument2, argument3,... )

因此,当你看到名称后面紧跟着的括号时,这意味着有一个函数调用。像我们这里这样做函数调用对于与 Copilot 的工作流程非常重要,尤其是在测试函数是否正常工作方面。我们还需要调用函数来完成工作,因为函数在调用之前不会做任何事情。

3.2 使用 Copilot 的函数设计周期

让我们向你介绍接下来几章我们将使用的流程。使用 Copilot 设计函数涉及以下步骤周期(见图 3.2):

  1. 确定函数的预期行为。

  2. 编写一个尽可能清晰地描述函数的提示。

  3. 允许 Copilot 生成代码。

  4. 仔细阅读代码,看看它是否合理。

  5. 测试代码以查看其是否正确:

    • 如果代码经过多次测试后仍然正确,则继续。

    • 如果代码不正确,则回到步骤 2,并编辑提示。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/3-2.png

图 3.2 使用 Copilot 的一般编辑周期。这假设你定义了一个合理的函数。

我们将在下一章学习如何进行第 4 步,但我们确信你已经能够识别出代码明显错误的情况。例如,Copilot 可能只给出注释来填充函数的主体。注释并不做任何事情——它们不是代码——所以一大堆没有其他代码的注释显然不是正确的事情。或者,Copilot 可能只写一行,比如return -1,或者,我们个人的最爱,Your code here。Copilot 是从我们教授那里学到的,当我们提供学生部分代码并要求他们用“Your code here”来编写其余部分时。这些都是明显错误的,但在下一章中,我们将讨论如何阅读代码,这样你可以更快地发现更复杂的代码错误,也许更重要的是,看到在哪里以及如何修复它。在后面的章节中,我们将继续扩展这个周期,包括有效的调试实践,并继续练习如何改进提示。

3.3 使用 Copilot 创建良好函数的示例

在本节中,我们将与 Copilot 一起编写一系列函数。我们认为通过查看具体示例,你会对函数有更深的理解,我们将完全在 Copilot 中编写这些函数,以便帮助你看到我们刚刚描述的功能设计周期。尽管本章的目标还不是帮助你阅读代码,但我们会看到解决方案中的编程特性(有时称为构造),这些特性在代码中非常常见(例如,if语句,循环),因此当我们看到它们时,我们会指出这些特性。然后,在第四章中,我们将更详细地说明如何阅读这段代码。

我们将要处理的许多函数之间没有关联。例如,我们将从一个关于股票股价的函数开始,然后转向关于强密码的函数。你通常不会将这种不相关的内容存储在同一个 Python 文件中。因为我们只是在探索良好函数的不同示例,所以你可以自由地将所有函数存储在同一个 Python 文件中,也许命名为 function_ch3.py 或 function_practice.py。

3.3.1 Dan 的股票选择

Dan 是 AAAPL 这支股票的投资者。他以每股 15 美元的价格购买了 10 股。现在,这些股票每股的价值是 17 美元。Dan 想知道他在股票上赚了多少钱。

记住,我们希望使我们的函数尽可能通用。如果我们的函数唯一能做的就是计算这个特定的 AAAPL 情况,那么它在一般情况下就不会很有用。当然,它现在会帮助 Dan,但当他再次对 AAAPL 的股价感兴趣或他对另一支股票感兴趣时呢?

这里的一个有用的通用函数将接受三个参数,所有这些参数都是数字。第一个参数是购买的股票数量,第二个参数是购买股票时的股价,第三个参数是当前的股价。让我们称这个函数为money_made,因为它将确定我们在股票上赚了多少钱或损失了多少钱。一般来说,你希望给你的函数命名为一个动作词或描述函数所做事情的词组。有了这个,我们就有了编写函数头部的足够信息:

def money_made(num_shares, purchase_share_price, current_share_price):

现在,我们需要一个文档字符串。在文档字符串中,我们需要通过在句子中使用其名称来解释每个参数的作用。我们还需要包括函数应该做什么。添加我们的文档字符串,以下是提供给 Copilot 的完整提示:

def money_made(num_shares, purchase_share_price, current_share_price):
 """
 num_shares is the number of shares of a stock that we purchased.
 purchase_share_price is the price of each of those shares.
 current_share_price is the current share price.

 Return the amount of money we have earned on the stock.
 """

在输入提示后,转到下一行,并按 Tab 键。Copilot 将填充函数的代码。不用担心代码会被缩进:函数的代码应该是缩进的,实际上,如果没有缩进,那将是一个错误!以下是 Copilot 给出的代码:

    return num_shares * (current_share_price - purchase_share_price)

这段代码看起来很有道理。在括号内,它计算出当前价格与购买价格之间的差异(这里使用-表示减法),然后将其乘以我们所持有的股票数量(这里使用*表示乘法)。检查这样的代码是一种有用的技能,我们将在下一章中认真对待它。另一个有用的技能是测试函数。

要测试money_made函数,我们可以使用各种输入调用它,并观察每种情况下的输出。我们可以通过让 Copilot 调用函数,然后运行我们的程序来实现这一点,就像我们之前对larger函数所做的那样。然后我们可以要求 Copilot 通过使用不同的输入来更改函数调用,然后我们再次运行程序,重复所需的次数。然而,我们发现直接从交互窗口调用函数更容易、更方便。这样,我们可以多次调用函数,而无需通过 Copilot,也不会在我们的程序中添加我们打算删除的东西。

要尝试这种交互式方法,选择/高亮显示函数的所有代码,然后按 Shift-Enter(你可以在 Python 窗口中选择文本,右键单击,然后在 Python 窗口中选择运行选择/行来访问类似的交互会话,但这里的指导是如果你使用 Shift-Enter)。图 3.3 显示了如果你选择函数的文本并按 Shift-Enter 会出现什么情况。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/3-3.png

图 3.3 在 VS Code 中运行 Python 的交互会话。注意终端标签页底部的>>>。

在结果窗口的底部,你会看到三个大于号>>>。这被称为提示符,你可以在那里输入 Python 代码。(这个提示符与我们与 Copilot 交互时使用的提示符没有关系。)它将立即显示我们输入的代码的结果,这既方便又快捷。

要调用我们的money_made函数,我们需要提供三个参数,它们将从左到右分配给参数。我们放第一位的将分配给num_shares,放第二位的将分配给purchase_share_price,放第三位的将分配给current_share_price

让我们试试!在提示符下,输入以下内容,然后按 Enter(或 Shift-Enter)。不要输入>>>,因为那里已经有了;我们在整本书中都包括它,以便清楚地表明我们在哪里输入。图 3.4 显示了在 Python 提示符下终端运行函数的示例:

>>> money_made(10, 15, 17)

你将看到以下输出:

20

20是正确的吗?嗯,我们买了 10 股,每股上涨了 2 美元(从 15 美元到 17 美元),所以我们确实赚了 20 美元。看起来不错!

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/3-4.png

图 3.4 在 VS Code 终端的 Python 提示符下调用money_made函数

尽管如此,我们还没有完成测试。当你测试一个函数时,你希望以各种方式测试它,而不仅仅是测试一次。一个测试用例只能告诉你,它碰巧在你提供的特定输入值上工作。我们尝试的测试用例越多,每个测试用例都以不同的方式测试函数,我们就越有信心我们的函数是正确的。

我们如何以不同的方式测试这个函数?我们正在寻找某种不同类别的输入。一个不太好的测试方法现在可能是说,“如果我们的股票从 15 美元涨到 18 美元,而不是从 15 美元涨到 17 美元怎么办?”这几乎和之前的测试一样,而且很可能它会正常工作。

一个好主意是测试股票实际亏损时会发生什么。我们预计在这种情况下会得到一个负的返回值。而且看起来我们的函数在这个测试类别中工作得很好。这是我们的函数调用和返回给我们的输出:

>>> money_made(10, 17, 15)
-20

我们还能进行哪些其他测试?嗯,有时候股票价格根本不会变化。我们预计在这种情况下会是 0。让我们验证一下:

>>> money_made(10, 15, 15)  
0

看起来不错!测试是科学和艺术的结合。有多少种事物类别需要测试?这两个调用是否真的属于两个不同的类别?我们是否遗漏了任何类别?通过实践,你会提高你的测试能力,我们将在第六章全部讨论测试。目前来看,我们的money_made函数似乎正在正常工作。

函数可以在其代码中使用变量(而不仅仅是它的参数),我们现在想给你展示一个例子,这样当你看到 Copilot 这样做时,你就能准备好了。这是编写我们的money_made函数代码的等效方式:

    price_difference = current_share_price - purchase_share_price
    return num_shares * price_difference

这段代码可能对你来说更容易阅读:它首先计算出股价的差异,然后将其乘以股票数量。我们鼓励你测试这个版本,以帮助你自己确信它仍然是正确的。

3.3.2 Leo 的密码

Leo 正在注册一个新的社交网络网站,名为 ProgrammerBook。他想确保自己的密码是强的。

Leo 对密码强弱的定义很朴素:如果它不是单词password也不是单词qwerty,那么它就是强的。当然,这些密码很糟糕,但在现实中,我们必须比这个定义做得更好,以确保我们的密码是强的!一个有用的函数将是一个可以接受一个建议的密码并告诉我们它是否强大的函数。

与本章中我们之前的函数不同,我们这里处理的是文本,而不是数字。参数,即要检查的密码,是文本。返回值应该表示某种是/否结果。我们需要新的类型!

Python 中用于文本的类型称为字符串。由于我们可以使用字符串来存储我们想要的任何文本,因此可能的字符串数量是无数的。Python 中用于是/否结果的类型称为布尔型bool。布尔型只有两个值:TrueFalse

好的!我们已经准备好提示 Copilot 了。对于返回布尔值(True/False)的函数,我们通常将函数命名为has_xis_x或类似名称,使用一些表示真/假结果的动词:

def is_strong_password(password):
 """
 A strong password is not the word 'password'
 and is not the word 'qwerty'.
 Return True if the password is a strong password, False if not.
 """

作为回应,Copilot 为我们写了以下内容:

    return password != 'password' and password != 'qwerty'

仔细看看那段代码。那个!=运算符是“不等”运算符,每个不被视为强密码的字符串都用单引号括起来。每次我们需要指定特定的字符串值时,我们都会使用引号。再次提醒,我们将在下一章中更多地讨论如何阅读代码,但到目前为止,我们希望这段代码看起来有点模糊的合理性:它看起来像是在检查密码不是password也不是qwerty,这符合我们强密码的定义。

现在,让我们通过进行一些测试调用来确保这个函数确实能工作。我们将首先测试那些不是强密码的密码。我们最好为这些密码都返回False——我们确实做到了:

>>> is_strong_password('password')        
False      

>>> is_strong_password('qwerty')          
False

好的!现在,让我们尝试一个强密码。我们需要在这里看到True

>>> is_strong_password('N3w Y0rk J375')    
True

太好了!我们现在看到这个函数有时返回False,有时返回True,所以至少它不是总是返回False。让我们再试一次:

>>> is_strong_password('Leo') 
True

好吧,从技术上讲,True是正确的,但哇,Leo 这个密码真是太糟糕了。我们真的应该对我们的强密码定义做得更好。让我们改变提示,给出一个更合理的定义,说明一个密码为什么是强的。这是我们的下一个提示和 Copilot 给出的内容:

def is_strong_password(password):
 """
 A strong password has at least one uppercase character,
 at least one number, and at least one special symbol.

 Return True if the password is a strong password, False if not.
 """
    return any(char.isupper() for char in password) and \
             any(char.isdigit() for char in password) and \
             any(not char.isalnum() for char in password)

这里出现几次的for char in password是 Python 考虑密码中每个字符的一种方式。它启用了一种称为循环的功能,这是我们将在第五章学习的关键主题。

当然,有可能你没有从 Copilot 那里得到与我们相同的代码。但根据我们得到的代码,看起来第一部分是在寻找任何大写字母,第二部分是在寻找任何数字,而第三部分是在寻找不是isalnum的东西。

也许isalnum意味着“是字母数字的”。这包括特殊符号吗?如果是的话,是哪些符号?让我们测试这个函数看看我们做得怎么样:

>>> is_strong_password('Leo')
False

>>> is_strong_password('N3w Y0rk J37$')
True

>>> is_strong_password('N3w Y0rk J375')
True

哎呀!我们为那个第三个密码得到了错误的答案。它看起来相当强大,但实际上并不行,因为它缺少一个特殊符号。我们的函数返回了True,但这是错误的返回值:它本应该返回False

Copilot 给出的代码是错误的。也许你得到的代码运行得很好,但我们没有这么幸运,而且了解如何识别(就像我们刚才做的那样)代码是错误的并且修复它是很重要的。现在知道为什么它是错误的并不重要,但如果你对我们的提示可能导致的错误行为感到好奇,那是因为我们没有具体说明我们所说的特殊字符是什么,而它给出的代码将空格视为特殊字符。

在这个阶段,当我们开始调试我们的错误代码时,我们有两个选择。一个选择是按 Ctrl-Enter 并查看 Copilot 给出的其他建议。也许其中的一条建议与那个没有工作的特殊符号有所不同?

的确,在我们的电脑上,我们看到这样的选项,包括这个:

    num_upper = 0
    num_num = 0
    num_special = 0
    for char in password:
        if char.isupper():
            num_upper += 1
        elif char.isnumeric():
            num_num += 1
        elif char in '!@#$%^&*':     #1
            num_special += 1
    if num_upper >= 1 and num_num >= 1 and num_special >= 1:
        return True
    else:
        return False

#1 特殊符号的具体选择

LeoN3w Y0rk J375N3w Y0rk J375$这样的字符串测试这个函数,你应该看到函数运行良好。

我们的第二个选择是进行一些提示工程,这意味着调整我们的提示以影响 Copilot 给出的内容。在我们的原始提示中,我们提到了特殊符号。回想起来,这是模糊的。我们可能指的是更具体的东西,比如标点符号。如果我们具体地在提示中提到标点符号,我们会得到与 Copilot 的这种互动:

def is_strong_password(password):
 """
 A strong password has at least one uppercase character,
 at least one number, and at least one punctuation.

 Return True if the password is a strong password, False if not.
 """
    return any(char.isupper() for char in password) and \
           any(char.isdigit() for char in password) and \
           any(char in string.punctuation for char in password)

看起来不错!最后一行是在谈论标点符号,这让人感到希望。让我们在这里测试它:

>>> is_strong_password('Leo')
False

>>> is_strong_password('N3w Y0rk J375')
 Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "ch2.py", line 44, in is_strong_password
    any(char in string.punctuation for char in password)
  File "ch2.py", line 44, in <genexpr>
    any(char in string.punctuation for char in password)
                ^^^^^^
NameError: name 'string' is not defined

看看错误信息的底部,你会看到'string'没有定义。Copilot 想要使用一个名为string的模块,但这是一个在使用之前需要导入的模块。这种“未定义”的结果可能发生在各种与找不到名称相关的错误中,而且很多时候罪魁祸首是一个尚未导入的模块。Python 中有许多模块,但string模块是众所周知的。随着你更多地使用 Copilot,你会了解到哪些模块是常用的,这样你就知道要导入它们。你也可以快速进行网络搜索来询问,“string 是 Python 模块吗?”搜索结果会确认它是。我们需要做的是导入这个模块。

注意,这与第二章中 Copilot 为我们生成的代码发生的情况相似。当时,Copilot 正在使用 csv 模块,这是一个与 Python 一起安装的模块。字符串模块也类似地包含在 Python 中;Copilot 只是忘记导入它。正如我们在第二章中讨论的,一些模块确实需要手动安装,但字符串模块不是其中之一。因此,我们不需要安装字符串;我们只需要导入它即可。

导入模块

Python 中有许多有用的模块。在第二章中,Copilot 使用了 csv 模块来简化处理逗号分隔值(CSV)文件。但为了 Python 代码能够利用一个模块,我们必须导入该模块。你可能会问为什么我们没有导入模块就可用,但那将大大增加代码的复杂性和 Python 在幕后运行代码所需的工作。相反,模型是,如果你想使用模块,就包含它们,它们不是默认包含的。

让我们在代码顶部添加import string

import string

def is_strong_password(password):
 """
 A strong password has at least one uppercase character,
 at least one number, and at least one punctuation.

 Return True if the password is a strong password, False if not.
 """
    return any(char.isupper() for char in password) and \
           any(char.isdigit() for char in password) and \
           any(char in string.punctuation for char in password)

现在我们处于良好的状态:

>>> is_strong_password('Leo')
False

>>> is_strong_password('N3w Y0rk J375')
False

>>> is_strong_password('N3w Y0rk J375$')
True

最后一个是True。这是一个强密码,因为它添加了$标点符号。

我们希望你现在已经相信了测试的价值。有时候,新程序员不会测试他们的代码。他们假设他们写的代码是正确的,因为它们对他们来说是有意义的。新手程序员和经验丰富的程序员之间一个有趣的不同之处在于,新手程序员经常假设他们的代码是正确的,而经验丰富的程序员则假设他们的代码是错误的,直到经过彻底测试并证明否则。除此之外,我们发现新程序员有时无法很好地进行测试,因为得知代码有错误会令人沮丧。但现在知道总比在别人在严重应用中使用你的代码时才知道要好。通过测试发现错误实际上是一件好事。

3.3.3 获取强密码

现在我们有一个函数可以告诉我们密码是否强大,让我们编写一个函数从用户那里获取强密码。它将不断要求用户输入密码,直到用户输入一个强大的密码。这是网站在告诉你“抱歉,你的密码太弱了,请再试一次”时使用的代码。

这样的函数的标题应该是什么样的呢?嗯,它将要求用户输入密码,但我们不会有一个密码参数。实际上,我们根本不会有任何参数,因为该函数不需要任何信息来完成其工作——它只需要提示用户,用户将在键盘上输入密码。当函数完成其工作后,它将返回强密码,因此它将继续以字符串作为输出返回。让我们尝试这个提示:

def get_strong_password():
 """
 Keep asking the user for a password until it’s a strong 
 password, and return that strong password.
 """

我们从 Copilot 得到了以下代码作为结果:

    password = input("Enter a strong password: ")
    while not is_strong_password(password):            #1
        password = input("Enter a strong password: ")
    return password

#1 它正在调用我们之前定义的 is_strong_password 函数!

这个 while 关键字创建了一种另一种循环,只要输入的密码不强,这个循环就会继续。Copilot 也足够聪明,可以调用我们之前的 is_strong_password 函数来确定什么算作强密码。正如你将在未来的章节中看到的,以这种方式使用函数作为构建块正是构建大型程序的方法。你经常会注意到 Copilot 调用你之前的函数来解决后续的问题,就像我们在这里观察到的那样。

让我们来测试一下!高亮显示我们两个密码函数的所有代码,然后按 Shift-Enter。我们将调用我们想要测试的函数。然后,尝试输入密码,每次输入后按 Enter。你会注意到它会一直询问,直到你最终提供一个强密码:

>>> get_strong_password()
Enter a strong password: Leo
Enter a strong password: N3w Y0rk J375
Enter a strong password: N3w Y0rk J375$
'N3w Y0rk J375$'

注意,当我们最终提供一个强密码时,它就不再询问我们密码了。然后,我们看到它返回的字符串,这确实是我们的强密码。

3.3.4 Scrabble 计分

丹最喜欢的桌面游戏之一是 Scrabble。你玩过吗?如果没有,你只需要知道你手里有一些带有字母的方块,你的目标是使用这些字母的任意组合来形成一个单词。你不需要只用你的方块来形成单词——你可以将这些字母附加到板上的现有字母上以形成更长的单词——但我们在这里不会担心这一点。对我们来说,重要的是不同的字母有不同的分数。例如,a 只值 1 分,因为 a 是如此常见的字母。但 qz 呢?这些宝贝每个都值 10 分,因为它们很难使用——或者说 令人困惑 的使用。是的,这样更好。

要计算一个单词的分数,我们将它的每个字母的分数加起来。例如,zap 的分数将是 14。这是因为 z 值 10,a 值 1,而 p 值 3。

丹想要一个函数,给定一个单词,告诉他这个单词值多少分。好吧,所以我们需要一个函数,它接受一个单词(这只是一个参数)。让我们尝试这个提示,其中我们包括了每个字母根据官方 Scrabble 规则的价值:

def num_points(word): 
 """ 
 Each letter is worth the following points: 
 a, e, i, o, u, l, n, s, t, r: 1 point 
 d, g: 2 points 
 b, c, m, p: 3 points 
 f, h, v, w, y: 4 points 
 k: 5 points 
 j, x: 8 points 
 q, z: 10 points 

 word is a word consisting of lowercase characters. 
 Return the sum of points for each letter in word. 
 """

对于这个函数,Copilot 不会通过按一次 Tab 键来为我们填写整个函数体。Tab 也可以用来缩进代码,我们在这里也需要这样做,这样 Copilot 才会展示代码。所以,我们不得不按 Tab(有时一次用于缩进,然后再次用于 Copilot),等待代码出现,然后按 Tab 接受该代码,然后再次按 Enter 以展示更多代码,并重复此过程。

要让 Copilot 建议代码,可能需要按 Tab 或 Enter

我们发现,Copilot 有时会在给出建议时暂停,直到你按下键来帮助提示它。如表格 2.1 中提到的,如果你给它评论,Copilot 有时只想给你更多的评论作为建议。再次按下 Enter 键通常可以触发它开始生成代码。有时,按下 Tab 键会导致它继续提供建议。如果 Copilot 没有为你生成代码,请参考表格 2.1。

最后,经过几次这样的操作后,整个代码展开,我们得到以下结果:

    points = 0
    for char in word:
        if char in "aeioulnstr":
            points += 1
        elif char in "dg":
            points += 2
        elif char in "bcmp":
            points += 3
        elif char in "fhvwy":
            points += 4
        elif char == "k":
            points += 5
        elif char in "jx":
            points += 8
        elif char in "qz":
            points += 10
    return points

注意,这里列出了所有字母,每个类别应得的分数在下一行。我们将在下一章讨论这种整体决策结构,但简单来说,就是逐个字符遍历单词,对于每个字符,确定它是哪个字母,并为该字母添加相应的分数。

让我们来测试一下,确保我们的代码正在按照我们期望的方式运行。我们知道单词 zap 应该值 14 分,确实如此:

>>> num_points('zap')
14

让我们再试几个。在每种情况下,手动计算你预期的答案——这样你就可以知道代码是否正确地执行了:

>>> num_points('pack')
12

>>> num_points('quack') 
20

编写函数的正确代码有多种方式。如果你按下 Ctrl-Enter 并查看 Copilot 的建议,你可能会看到不同类型的代码。这并不意味着其中一种类型是正确的,而其他类型是错误的。例如,这里是从其他 Copilot 建议中得到的 num_points 的另一个解决方案:

    points = {'a': 1, 'e': 1, 'i': 1, 'o': 1, 'u': 1, 'l': 1, 
              'n': 1, 's': 1, 't': 1, 'r': 1,
              'd': 2, 'g': 2,
              'b': 3, 'c': 3, 'm': 3, 'p': 3,
              'f': 4, 'h': 4, 'v': 4, 'w': 4, 'y': 4,
              'k': 5,
              'j': 8, 'x': 8,
              'q': 10, 'z': 10}
    return sum(points[char] for char in word)

注意这里,每个字母都单独列出,并附上其对应的分数。这种包含字母和分数的结构称为 字典,再次强调,你将在后面的章节中详细了解字典。现在重要的是,Copilot 编写函数代码通常有多种等效的好方法。在继续之前,先尝试用一些测试案例来验证这个方法!

3.3.5 最佳单词

让我们继续 Scrabble 主题。假设 Dan 现在有一堆单词可以组成,但他不知道哪个单词能给他带来最多的分数。他应该组成单词 zappack 还是 quack?如果有一个函数能接受一堆单词并告诉我们哪个是最好的,那就太好了。

在这样的函数中,我们应该有多少个参数?你的第一反应可能是三个参数,每个参数对应我们选项中的三个单词。但这不会很灵活。毕竟,如果我们想知道 5 个单词、10 个单词或 50 个单词中的最佳单词,怎么办?我们希望我们的函数能够处理我们考虑的任何数量的单词。

技巧是使用一个只有一个(是的,只有一个!)参数的函数,这个参数是一个单词列表。就像数字、字符串和字典一样,列表是 Python 支持的一种类型。它非常有用,因为它允许我们将尽可能多的值收集到一个地方。我们可以像这样提示 Copilot 来获取我们想要的函数:

def best_word(word_list):
 """
 word_list is a list of words.

 Return the word worth the most points.
 """

Copilot 将如何知道每个单词值是多少?嗯,它可以调用我们在上一节中编写的那个num_points函数!这是 Copilot 给出的代码:

    best_word = ""
    best_points = 0
    for word in word_list:
        points = num_points(word)
        if points > best_points:
            best_word = word
            best_points = points
    return best_word

我们在测试时需要小心地正确调用这个函数。使用

>>> best_word('zap', 'pack', 'quack')

是不会工作的,因为那样传递了三个参数,而不是一个。正确调用它的方法是把单词放在方括号里,就像这样(我们将在下一章中了解更多关于列表的内容):

>>> best_word(['zap', 'pack', 'quack'])  
'quack'

但,像往常一样,你不应该只满足于一个测试用例。这个函数也应该在其他有效但可能奇怪的用例中工作,例如,当我们列表中只有一个单词时:

>>> best_word(['zap'])
'zap'

然而,我们不会在一个没有任何单词的列表上测试这个函数。在那个情况下,这样做又有什么意义呢?不管这个函数做什么,在实际上没有正确行为的情况下,很难说它是否做了正确的事情!

3.4 函数的好处

现在你已经看到了许多函数,我们希望你在开始了解函数在软件中的应用。我们已经提到,函数在执行问题分解中是至关重要的。除了问题分解之外,函数在软件中还有许多其他价值,包括以下方面:

  • **认知负荷—你可能之前听说过认知负荷[1]。这是你的大脑在任何给定时间内可以处理的信息量,同时仍然保持有效。如果你被给出四个随机单词并要求重复它们,你可能能够做到。如果你被给出同样的任务,但单词有 20 个,我们大多数人会失败,因为一次处理的信息太多。同样,如果你曾经和家人一起开车旅行并试图优化旅行时间,包括为孩子们停车、午餐休息、洗手间停车、加油站停车、好的酒店位置等等,你可能会有感觉你的头昏脑胀,难以同时管理所有这些限制。当你无法一次处理所有事情的时候,就是你超过了你自己的大脑处理能力的时候。程序员也有同样的问题。如果他们试图一次做太多事情或者在一个代码块中解决一个过于复杂的问题,他们会发现很难正确地做到。函数的设计是为了帮助程序员避免一次做太多工作。

避免重复程序员(我们可以说,人类普遍来说)并不热衷于反复解决相同的问题。如果我写了一个函数,可以正确计算圆的面积,那么我就不需要再写那段代码了。这意味着如果我的代码中有两个部分需要计算圆的面积,我会写一个计算圆面积的函数,然后在这两个地方都调用这个函数。** 改进 测试与只做一件事情的代码相比,测试做多件事情的代码要困难得多。程序员使用各种测试技术,但一个关键的技术被称为单元测试*。每个函数都接受一些输入并产生一些输出。例如,对于一个计算圆面积的函数,输入将是圆的半径,输出将是其面积。单元测试会给函数一个输入,然后比较这个输入与期望的结果。对于圆面积函数,我们可能会通过给它不同的输入(例如,一些小的正数,一些大的正数,以及 0)来测试它,并将函数的结果与我们已知是正确的值进行比较。如果函数的答案与我们的预期相符,我们就对代码的正确性有了更高的信心。但如果代码产生了错误,我们检查以找到和修复问题的代码就不会很多。但是,如果一个函数执行多个任务,它会极大地复杂化测试过程,因为你需要测试每个任务以及这些任务之间的交互。*** 提高可靠性当我们作为经验丰富的软件工程师编写代码时,我们知道我们会犯错误。我们也知道 Copilot 会犯错误。如果你想象自己是一位出色的程序员,你写的每一行代码有 95%的可能性是正确的,你认为你能在至少有一行可能不正确之前写多少行代码?答案是只有 14 行。我们认为每行 95%的正确性可能即使是经验丰富的程序员也是一个很高的标准,可能比 Copilot 产生的标准还要高。通过保持任务小,任务可以在 12-20 行代码内解决,我们降低了代码中存在错误的概率。如果与前面提到的良好测试相结合,我们可以更有信心地认为代码是正确的。最后,没有比代码中存在多个相互作用的错误更糟糕的了,随着你写的代码越多,多个错误的概率就越大。我们俩都经历过因为代码中存在多个错误而进行的数小时调试之旅,并且我们都因此变得更加擅长频繁测试短小的代码片段!** 提高代码可读性在这本书中,我们主要使用 Copilot 从头开始编写代码,但这不是使用 Copilot 的唯一方式。如果你有一个更大的软件项目,你或你的同事都在编辑和使用,Copilot 也可以跳进来帮助编写那个项目的代码。无论是大部分代码是由人类还是由 Copilot 编写,理解代码对每个人来说都是有益的。这样,我们可以更容易地找到错误,确定当我们想要添加新功能时,从哪里开始修改代码,以及从高层次上理解使用我们的整体程序设计实现什么会容易或困难。将任务分解成函数有助于我们理解代码的每一部分在做什么,这样我们可以更好地了解它们是如何一起工作的。它还有助于分配工作责任,确保代码的正确性。****

**这些好处对程序员来说非常巨大。编程语言并不总是有函数。但在它们有函数之前,程序员们尽力使用其他特性来模仿函数。这些方法很丑陋(如果你感兴趣,可以谷歌搜索“goto 语句”),所有程序员都为现在有了真正的函数而感到高兴。

你可能会问,“我明白这些优势对人类的重要性,但它们是如何影响 Copilot 的?”总的来说,我们认为适用于人类的所有原则也适用于 Copilot,尽管有时原因不同。Copilot 可能没有认知负荷,但当我们要求它解决与人类之前所做类似的问题时,它会表现得更好。因为人类编写函数来解决任务,Copilot 会模仿这一点并编写函数。一旦我们手动或通过 Copilot 编写并测试了一个函数,我们就不想再写一次。知道如何测试你的程序是否正常工作对于人类编写的代码和 Copilot 编写的代码来说同样重要。Copilot 在生成代码时可能会犯错误,所以我们想快速捕捉这些错误,就像我们处理人类编写的代码一样。即使你只处理自己的代码,并且从未让其他人阅读它,你也经常需要多年后回过头来编辑你之前写的代码。我们自己做这件事,可以证明,对于你未来的自己来说,让你的代码可读是非常重要的!

3.5 函数的角色

函数在编程中扮演着多种不同的角色。在较高层次上,程序本身就是函数,它们(通常)会调用其他函数。关键的是,所有程序,包括 Python 程序,都起源于一个单一函数(在 Java、C 和 C++等语言中命名为main)。Python 的main函数本质上是非函数中的第一行代码。但如果我们认为每个程序都从一个单一函数开始,并且试图用一个函数解决一个大问题是一个错误,那么这是如何工作的呢?嗯,main会调用其他函数,而这些函数又会调用其他函数,如此类推。代码仍然会在每个函数中(主要)顺序执行,所以它可能从main开始,然后移动到另一个函数,依此类推。

例如,让我们使用以下列表中的代码。我们编写这段代码,而不是 Copilot,因为没有人会想编写这段代码来用于任何有用的教学之外的事情。这只是为了演示函数调用是如何工作的。

列表 3.1 展示 Python 如何处理函数调用的代码
def funct1():
    print("there")
    funct2()
    print("friend")
    funct3()
    print("")

def funct2():
    print("my")

def funct3():
    print(".")

def funct4():
    print("well")
print("Hi")      #1
funct1()
print("I'm")
funct4()
funct3()
print("")
print("Bye.")

#1 这是程序的开始。在其他语言中,我们将这个称为“main”。

如果我们运行这个程序,输出将会是(我们将在下文中解释原因):

Hi
there
my
friend
.

I'm
well
.

Bye.

在图 3.5 中,我们提供了一个示例 3.1 中代码在计算机中如何被执行的示意图。我们故意提供了一个包含许多函数调用的例子,以便将我们刚刚学到的知识联系起来。再次强调,这不是实际可用的代码;这只是用于学习目的。让我们一起追踪代码执行。在跟随时,你可能更容易参考图 3.5 而不是列表 3.1,但两者都可以。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/3-5.png

图 3.5:示例 3.1 中函数执行流程

程序将从 Python 代码中第一个不是函数的第一行开始执行(print("Hi"))。尽管 Python 本身没有 main 函数,但为了便于解释,我们将函数之后的代码块称为 main。代码按顺序执行,除非它遇到告诉它执行其他地方代码的命令。因此,执行 print("Hi") 后,它将转到下一行,即调用 funct1funct1()。对 funct1 的调用改变了代码执行的地点,到该函数的开始,即以下语句:print("there")funct1 的下一行调用 funct2,因此程序将执行 funct2 的第一行:print("my")。有趣的是,当 funct2 完成时会发生什么。没有更多的代码行可以执行,所以它自动将执行权返回到 funct1 中调用 funct2 的下一行。(如果函数调用在另一个语句的中间,那个语句将恢复执行,但在这个例子中,函数调用都在单独的一行上。)你可能好奇为什么它会在调用 funct2 后转到下一行而不是返回到 funct2 的调用处。问题是如果它返回到 funct2 的调用处,它将永远陷入调用 funct2 的循环中。因此,函数在被调用后总是返回到下一行代码以执行(在这个例子中,是下一行)。

继续这个例子,接下来执行的代码行将打印 friend 的那一行。下一行调用 funct3,它打印一个点(.)然后返回到调用者。

因此,我们又回到了 funct1,在 print("") 这一行。打印一段空文本会导致新的一行。现在 funct1 已经完成,所以它将执行权传回给 main 中调用它的下一行。现在你可能已经明白了这个概念,所以我们稍微快一点进行:

  • main 下一步打印 I'm 然后调用 funct4.

  • funct4 打印 well 然后返回到 main,在那里下一行代码调用 funct3

  • funct3 打印一个点(.)然后返回到 main。注意,funct3 既有 funct1 又有 main 调用,但这没关系,因为函数记得如何返回到调用它们的函数。实际上,多个函数调用同一个函数是函数被多次调用且具有重用价值的标志。

  • funct3 返回到主函数后,它将打印 "",这会导致开始新的一行,然后它打印单词 Bye

这是一个很长的例子,但我们提供它是为了让你了解函数是如何执行的,以及程序是如何由定义和调用函数组成的。在任何你使用的软件中,想想它执行的具体任务:程序员可能为每个任务编写了一个或多个函数。在文本编辑器中改变文本为粗体的按钮可能调用一个函数来改变文本为粗体。这个函数可能会改变编辑器对文本的内部理解(编辑器可能以不同于你查看的方式存储你的文本),然后它可能调用另一个函数来更新用户(你)对文本的视图。

我们还希望用这个例子来讨论函数扮演的不同角色。一个辅助函数是一个其工作是为了使另一个函数的工作更容易的函数。从某种意义上说,每个不是 main 的函数都是一个辅助函数。最好的辅助函数是那些执行其他任务的一些小而定义明确的部分的函数,比如我们在 3.3.2 节中的 is_strong_password 函数作为 3.3.3 节中的 get_strong_password 函数的辅助函数。

有些函数只是调用了一堆其他函数而没有做任何自己的工作。在我们的例子中没有这样的函数。然而,如果你从 funct1 中移除了三个 print 语句,它就变成了这种协调函数。其他函数可能调用辅助函数,然后自己做一些工作。funct1 是一个很好的例子,它调用其他函数同时也自己做工作。

另一种类型的函数——我们称之为叶函数——独立存在,不需要调用其他函数来帮助(除了可能已经包含在 Python 中的函数)。为什么叫叶函数?如果你想象所有的函数调用就像一棵大树,这些函数就是树的叶子,因为它们没有东西输出。在我们的例子中,funct2funct3funct4 都是叶函数。我们主要关注本章中的叶函数,但你在这里和后面的章节中也会看到其他类型的函数的例子。

3.6 函数的合理任务是什么?

没有明确的规则来决定什么是一个好的函数,但有一些直觉和建议我们可以分享。不过,不要误解——识别好的函数是一种需要时间和实践技巧的技能。为了帮助你,我们在 3.3 节中给你提供了一些好的函数的例子。在本节中,我们将概述我们的建议,并提供一些额外的好的和坏的例子来帮助你建立这种直觉。

3.6.1 好函数的特性

这里有一些我们认为会帮助你看到什么使一个函数好的指南:

  • 一个明确的任务要执行 — 一个叶函数可能像“计算球体的体积”、“在列表中找到最大的数字”或“检查列表是否包含特定值”这样的功能。非叶函数可以实现更广泛的目标,比如“更新游戏图形”或“收集并净化用户输入”。非叶函数仍然应该有一个特定的目标,但它们的设计是基于它们可能会调用其他函数来实现其目标。

** 行为定义明确 —“在列表中找到最大的数字”这个任务是明确定义的。如果你给我一个数字列表并问我最大的数字是什么,你知道你应该做什么。相比之下,“在列表中找到最好的单词”这个任务定义得并不好,如上所述。你需要更多信息:什么是“最好的”单词?是长度最长的,使用最少元音的,还是不与“Leo”或“Dan”共享任何相同字母的?你明白我的意思了;主观任务对计算机来说并不好。相反,我们可以编写一个函数“在列表中找到具有最多字符的单词”,因为预期的结果是明确定义的。通常,程序员无法只通过函数名来包含所有细节,所以他们会在文档字符串中填写细节以阐明其用法。如果你发现自己需要写超过几句话来描述函数的行为,那么这个任务可能超出了一个函数的范围。 代码行数少 —多年来,我们听到了关于函数长度的不同规则,这些规则是由不同的公司风格指南所决定的。我们听到的长度从 12 到 20 行 Python 代码不等,作为最大行数。在这些规则中,行数被用作代码复杂性的代理,这不是一个坏的经验法则。作为程序员,我们两人都应用类似的规则来确保复杂性不会失控。有了 Copilot,我们也可以将其作为指南。如果你要求 Copilot 提供一个函数,并且它给你返回了 50 行代码,那么这个函数名或任务可能不是好的。正如我们之前讨论的,那么多的代码行很可能会有错误。 通用价值高于特定用途 —一个返回列表中大于 1 的值的数量的函数可能只是你程序的一部分的特定需求,但有一种方法可以使它更好。这个函数应该重写为返回列表中大于另一个参数的值的数量。新的函数将适用于你的用例(给函数第二个参数为 1)以及任何除了 1 以外的值。我们努力使函数尽可能简单但功能强大。这就是为什么我们在 3.3.1 节中编写函数是为了适用于任何股票,而不仅仅是 AAAPL。 清晰的输入和输出 —你通常不希望有很多参数。但这并不意味着你不能有很多输入。一个参数可以是一个项目列表,就像我们在 3.3.5 节中的best_word函数一样。但这确实意味着你想要找到方法将输入的数量保持在最低。你只能返回一个东西,但再次强调,你可以返回一个列表,这样你就不像看起来那么受限了。但如果你发现自己正在编写一个函数,有时返回一个列表,有时返回一个单一值,有时返回空值,那么这个函数可能不是好的。**

### 3.6.2 优秀(及不良)的叶子函数示例

这里有一些良好的叶子函数的例子(函数以斜体形式位于左侧,该函数的目标在右侧描述):

  • 计算球体的体积—给定球体的半径,返回其体积。

  • 找出列表中的最大数字—给定一个列表,返回最大的值。

  • 检查列表中是否包含特定值—给定一个列表和一个值,如果列表包含该值则返回 True,如果不包含则返回 False。

  • 打印国际象棋游戏的状态—给定一个表示游戏棋盘的二维列表,将游戏棋盘输出到屏幕上。

  • 在列表中插入一个值—给定一个列表、一个新值和列表中的位置,返回一个新列表,该列表是旧列表,新值被插入到所需的位置。

这里有一些不良的叶子函数的例子以及我们为什么认为它们不好:

  • 请求用户的税务信息并返回他们今年应缴的金额—也许在某些国家这不会太糟糕,但鉴于税务规则的复杂性,我们无法想象在美国或加拿大将其作为一个单一函数!

  • 在列表中识别最大的值并将其从列表中删除—这看起来可能并不糟糕,但实际上它做了两件事。第一是找到列表中的最大值。第二是从列表中删除一个值。我们建议两个叶子函数,一个用于找到最大值,另一个用于从列表中删除值。然而,如果程序需要频繁执行此任务,这可能会成为一个好的非叶子函数。

  • (思考第二章中的数据集。)返回数据集中传球超过 4,000 码的四分卫的名字—这太具体了。毫无疑问,数字 4,000 应该是一个参数。但可能更好的是创建一个函数,该函数接受位置(四分卫、跑卫)、统计数据(传球码数、比赛场次)以及我们关心的截止值(4,000、8,000)作为参数。这个新函数提供了比原始函数多得多的功能,允许用户调用函数来确定不仅传球超过 4,000 码的特定四分卫的名字,例如,跑卫在比赛中得分超过 12 次。

  • 确定史上最佳电影—这个函数太模糊了。最佳电影是根据什么定义的?应该考虑哪些电影?更好的版本可能是确定用户评分至少达到最低数量的最高评分电影的功能。这个函数可能是一个更大程序的一部分,其中该函数会从电影数据库(例如 IMDB)和最低用户评分作为输入。该函数的输出将是至少有指定评分数量的最高评分电影。

  • 玩使命召唤—这可能是使命召唤游戏大型代码库中的main函数,但它绝对不是一个叶子函数。

总体来说,在本章中,你已经了解了 Python 中的函数以及如何使用 Copilot 来帮助编写它们。你还学习了良好函数的特征以及确保你的函数能够被 Copilot 良好管理的重要性。

本书接下来的步骤都围绕着理解 Copilot 生成的代码是否正确,以及当它不正确时如何修复。在下一章中,我们将首先解释如何阅读 Copilot 生成的代码的基础知识,因为这为我们提供了 Copilot 是否按照我们的预期工作的第一个合理性检查。然后,在后面的章节中,我们将深入探讨如何仔细测试代码,以及当它出错时应该做什么。

3.7 练习

  1. 审查以下函数描述,并确定每个是否是叶子函数。根据函数执行的任务的复杂性和它们是否可能需要调用额外的自定义函数,提供你的答案的理由。

    1. 抓取数据。 给定一个 URL 列表,从每个页面抓取数据,解析内容以提取特定信息,并将提取的数据保存到数据库中。

    2. 处理客户订单。 给定客户订单列表,通过检查库存,计算包含折扣和税的总价,更新库存,并生成订单摘要来处理每个订单。

    3. 在列表中找到最大的数字。 给定一个列表,返回最大的值。

    4. 检查列表中是否包含特定值。 给定一个列表和一个值,如果列表包含该值则返回 True,如果不包含则返回 False

    5. 生成学生成绩单。 给定学生名单和他们的成绩,计算最终成绩,分配字母等级,确定班级排名,并为每个学生生成格式化的成绩单。

  2. 审查本章概述的与 Copilot 一起设计函数的周期。这个周期包括从确定函数的行为到测试生成的代码的步骤。根据一般的编程实践,确定以下哪个步骤不包括在本章描述的周期中,但通常是软件开发过程的一部分:

    1. 通过运行单元测试验证生成的代码的逻辑正确性

    2. 在 Copilot 生成后手动编辑函数的代码以简化复杂的逻辑

    3. 对生成的代码进行性能基准测试以确保效率

    4. 咨询文档或外部资源以完善函数的提示

  3. 根据提供的问题描述,为 Copilot 编写一个清晰简洁的提示,指定要完成的任务。确保你的提示包括所有必要的细节,例如预期的输入、要执行的处理以及预期的输出:

    1. 在一个宁静的小镇上,有一位谦逊的店主,他时不时地会通过折扣来取悦他的顾客。为了简化这一过程,他寻求创造一个神奇的公式。这个公式将接受他店里任何商品的原始价格和他希望提供的特殊折扣。有了这两条信息,公式就会揭示出顾客在折扣融入原始标签后需要支付的最终价格,这让店主和顾客都对交易的便捷性感到非常满意。

你能想到两个或三个输入示例来验证 Copilot 根据你的提示生成的函数的正确性吗?包括一些场景以确保函数能够正确处理不同类型的输入并计算出正确的输出。

    1. 审查函数描述和提供的测试用例,用于计算包含税费和小费的餐点总费用。基于该场景和此类函数的典型要求,你能想到哪些函数无法正确处理的输入?解释为什么处理这种输入对于确保函数在所有潜在用例中的可靠性和正确性是必要的。

    问题描述:创建一个函数,计算餐点的总费用,包括税费和小费。该函数应接受三个输入:餐点的基准价格、税率(百分比)和小费率(百分比)。该函数应计算并返回餐点的总费用。

    测试用例:

    1. 标准情况—计算一个$50 餐点的总价格,包含 10%的税费和 15%的小费:

      • 输入:base_price = 50, tax_rate = 10, tip_rate = 15

      • 预期输出:$62.50(计算为$50 + $5 税费 + $7.50 小费)

    2. 无小费情况—计算一个$30 餐点的总价格,包含 5%的税费和没有小费:

      • 输入:base_price = 30, tax_rate = 5, tip_rate = 0

      • 预期输出:$31.50(计算为$30 + $1.50 税费 + $0 小费)

    3. 高税费和小费情况—计算一个$100 餐点的总价格,包含 20%的税费和 25%的小费:

      • 输入:base_price = 100, tax_rate = 20, tip_rate = 25

      • 预期输出:$145(计算为$100 + $20 税费 + $25 小费)

    1. 审查提供给 Copilot 的初始提示和生成的 Python 函数。代码没有满足要求,因为它只将华氏度转换为摄氏度,并且缺乏根据用户指定的模式双向转换的能力。你将如何改进提示以确保生成的代码满足问题描述的全部要求?
*Prompt: Write a Python function to convert temperature between Fahrenheit and Celsius.*

def convert_temperature(temp):
    celsius = (temp - 32) * 5.0/9.0
    return celsius

摘要

  • 问题分解涉及将一个大问题分解成更小的任务。

  • 我们在程序中使用函数来进行问题分解。

  • 每个函数必须解决一个小的、定义明确的任务。

  • 函数头或签名是函数的第一行代码。

  • 参数用于向函数提供信息。

  • 函数头指示了函数的名称及其参数的名称。

  • 我们使用return从函数传递值给其调用者。

  • 文档字符串使用每个函数参数的名称来描述函数的目的。

  • 要让 Copilot 编写一个函数,我们提供函数头和文档字符串。

  • 通过为参数提供值(也称为参数)来调用函数,我们让函数执行其工作。

  • 为了测试一个函数是否正确,我们用不同类型的输入调用它。

  • 变量是一个指向值的名称。

  • 每个 Python 值都有一个类型,例如数字、文本(字符串)、真/假值(bool)或值的集合(列表或字典)。

  • 提示工程涉及修改 Copilot 的提示,以影响我们得到的代码。

  • 我们需要确保导入代码中使用的任何模块(例如,string)。

  • 函数减少重复,使测试我们的代码更容易,并降低错误的可能性。

  • 单元测试涉及检查函数在多种不同输入下是否按预期执行。

  • 辅助函数是一个小型函数,编写它的目的是为了更容易地编写更大的函数。

  • 叶函数不调用任何其他函数来完成其工作。******

第四章:. 阅读 Python 代码:第一部分

本章涵盖

  • 知道如何阅读代码的重要性

  • 如何让 Copilot 解释代码

  • 使用函数分解大问题

  • 使用变量来保留值

  • 使用 if 语句进行决策

  • 使用字符串存储和操作文本

  • 使用列表收集和操作多个值

在第三章中,我们使用 Copilot 为我们编写了几个函数。它们有什么用?也许我们的money_made函数可以是股票交易系统的一部分,我们的is_strong_password函数可以用作社交网站的一部分,或者我们的best_word函数可以用作某些 Scrabble AI 的一部分。总的来说,我们编写了一些有用的函数,它们可以是更大程序的一部分。而且,我们没有写很多代码,实际上甚至不理解代码的作用。

然而,我们认为你需要从高层次上理解代码的作用。因为这需要一些时间来学习,所以我们把这个讨论分成了两章。在本章中,我们将解释阅读代码的重要性,并介绍一个可以帮助你理解代码的 Copilot 功能。之后,我们将深入探讨你需要识别的前 10 个编程特性,以阅读 Copilot 生成的最基本代码。我们将在这章中介绍前五个,在下一章中介绍剩下的五个。不用担心:你实际上已经非正式地介绍了这 10 个——我们只是要加深你对每个特性的理解。

4.1 为什么我们需要阅读代码

当我们谈论阅读代码时,我们的意思是通过查看代码来理解代码的作用。有两种这样的理解层次,它们都很重要。

第一层是能够逐行理解程序将做什么。这通常涉及到在代码运行时追踪变量的值,以确定代码在每一步的具体行为。这很有用,但并不是全部。就像不知道食谱要做什么或其成分如何搭配的情况下,一步一步地跟随烹饪食谱。最后,你可能会有一个蛋糕,但你可能无法理解蛋糕为什么做得这么好,或者你如何能自己想出这样的食谱。你需要理解整个食谱为什么有效。

所以,这是第二层:确定程序的整体目的。也就是说,你需要能够从每一行中抽离出来,从高层次上理解程序在做什么。

在这两章的结尾,我们希望你能开始理解 Copilot 在这两个层面的代码。我们将开始逐行关注这种理解,但在本章的末尾以及本书的其余部分,你将开始能够查看一小段代码并确定其整体目的。

我们可以通过参考第三章中重新打印的best_word函数来阐述阅读代码两个层次之间的区别。

列表 4.1 为 Scrabble 的best_word函数
def best_word(word_list):
 """
 word_list is a list of words.

 Return the word worth the most points.
 """
    best_word = ""
    best_points = 0
    for word in word_list:
        points = num_points(word)
        if points > best_points:
            best_word = word
            best_points = points
    return best_word

追踪描述这个程序的功能是描述每一行。例如,我们会说我们定义了一个名为best_word的函数,它接受一个名为word_list的参数。我们有一个名为best_word的变量,我们一开始将其初始化为一个空字符串,即没有字符的字符串。(不幸的是,函数和这个变量都叫best_word,这使得指代其中一个或另一个变得复杂,但这是 Copilot 给我们的。)我们还有一个名为best_points的变量,我们将其初始化为0。然后,我们有一个遍历word_list中每个单词的for循环。在for循环内部,我们调用我们的num_points辅助函数,并将它的返回值存储在points变量中,等等。(我们将在本章和下一章中解释我们如何知道每一行代码的作用。)

相反,一个整体目的描述可能类似于我们的文档字符串描述:“从单词列表中返回具有最高 Scrabble 得分值的单词。”这个描述不是针对每一行,而是指代代码的整体目的,从高层次解释它做什么。

你将通过结合追踪和测试的实践达到整体目的的理解水平,我们希望你在书的结尾能够完全实现这一目标。在追踪层次上工作通常先于在整体目的层次上工作[1],因此在本章和下一章中,我们将通过理解每一行代码的作用来专注于追踪层次。我们希望你能够阅读代码有三个原因:

  1. *为了帮助确定代码是否正确—*在第三章中,我们练习了如何测试 Copilot 给出的代码。测试是确定代码是否正确执行的一项强大技能,我们将在整本书中继续使用它。但许多程序员,包括我们两个,只有在代码看起来合理正确的情况下才会进行测试。如果我们通过检查就能确定代码是错误的,那么我们就不费心去测试它:我们会先尝试修复代码。同样,我们希望你能识别出代码的错误,而无需花费时间去测试。你能识别出更多错误的代码(通过快速追踪或提高你的整体技能),你就能节省更多测试错误代码的时间。

  2. *为了指导测试—*理解代码逐行执行的内容本身很有用,但它也有助于提升你测试有效性的能力。例如,在下一章中,你将学习到循环——它们可以使代码重复零次、一次、两次或所需次数。你将能够将这一知识与你已经了解的测试知识相结合,帮助你确定重要的测试案例类别。

  3. *为了帮助你编写代码—*我们知道,你希望 Copilot 为你编写所有代码!我们也有同样的愿望。但不可避免的是,Copilot 可能无法正确处理某些代码,无论你如何进行提示工程。或者,也许足够的提示工程最终能够说服 Copilot 编写正确的代码,但自己动手做会更简单、更快。在编写这本书的过程中,我们两个人努力让 Copilot 尽可能多地编写代码。但是,由于我们对 Python 编程的了解,我们经常能够发现错误并直接修复,而无需通过任何繁琐的步骤让 Copilot 为我们修复。从长远来看,我们希望你能获得自主学习的编程能力,而理解 Python 编程则是我们为你提供从这本书到其他资源桥梁的方式。有研究表明,能够追踪和解释代码是能够编写代码的前提条件 [1]。

在我们深入探讨之前,我们需要明确我们追求的深度水平。我们不会教给你每一行代码的每一个细微之处。这样做会让我们回到在 Copilot 等工具出现之前传统的编程教学方式。相反,通过 Copilot 工具和我们的解释相结合,我们将帮助你理解每一行代码的精髓或总体目标。如果你未来打算编写大量程序,你需要的东西将不止于此。我们试图在“这段代码是魔法”和“我确切知道代码的每一行是如何工作的”之间找到最佳平衡点。

4.2 请求 Copilot 解释代码

到目前为止,我们使用 Copilot 为我们生成代码。这是它最知名的功能,但并非唯一。现在,我们将向你展示它的另一个最佳功能:解释 Python 代码的功能!

Copilot 扩展总是在变化中。我们这里给出的具体步骤可能会有所不同,在这种情况下,我们鼓励你查阅更通用的 GitHub Copilot 文档。

要尝试解释功能,你需要突出显示一些你希望 Copilot 为你描述的代码。让我们用我们的best_word函数(列表 4.1)来试一试。如果你没有从第三章输入这段代码,请现在输入。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/4-1.png

图 4.1 在编辑器中突出显示的best_word函数的代码

如图 4.1 所示,高亮显示代码。之后,我们有几个选项。第一个是点击活动栏中的聊天图标(位于 VS Code 的左侧),或者按 Ctrl-Alt-I。这将打开如图 4.2 所示的 Copilot 聊天界面。(还有其他聊天界面,包括你可以通过 Ctrl-Shift-I 打开的快速聊天——使用哪个聊天界面无关紧要。)Copilot 聊天能够不仅用自然语言,还能用代码块、你可以点击以调用命令的按钮/链接等方式来回答你的问题。它还建议你可能想问的问题,当然,也允许你输入任何你喜欢的与代码相关的问题。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/4-2.png

图 4.2 VS Code 中的 Copilot 聊天界面

现在,我们将使用 Copilot 聊天来解释代码。在文本框中,输入以下命令:

/explain

如果 Copilot 询问,你想要选择@workspace 选项。如果这个选项对你不起作用,你也可以尝试以下命令:

/explain how the selected code works

输入一个正斜杠(/)是告诉 Copilot 聊天你正在指定一个命令(Copilot 文档将这些称为斜杠命令),而不是自然语言。在这种情况下,我们正在请求 Copilot 运行/explain命令来解释选中的文本。

或者,你可以通过如图 4.1 所示的方式在函数中高亮显示代码来请求 Copilot 解释代码。然后,右键点击函数中的代码,你将看到如图 4.3 所示的菜单。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/4-3.png

图 4.3 当你右键点击选中的best_word函数时出现的菜单

在这个菜单中,高亮显示 Copilot,你将看到图 4.4 中显示的选项列表。这些选项是使用 Copilot 时常用的功能。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/4-4.png

图 4.4 你可以使用 Copilot 的选项列表

最后一步是选择“解释此代码”选项。Copilot 聊天窗口将打开(如图 4.2 中之前所示),你将得到代码的解释。像往常一样,Copilot 是非确定性的,所以你的结果可能与我们的大不相同。事实上,如果解释让你感到困惑,你可能再次尝试使用/explain命令,或者编写自己的提示以获得不同的解释。作为教育工作者,这是梦想成真的开始,学习者将能够请求他们需要的任何解释,以完全理解代码是如何工作的。

Copilot 的解释可能会出错

与 Copilot 生成的代码一样,Copilot 的解释也可能出错。你可能想,“等等,如果这个工具可以出错,那么理解代码的价值在哪里?!”首先,我们同意这确实可能非常令人沮丧。然而,根据我们的经验,Copilot 的解释通常足够准确,可以非常有帮助,尤其是在理解代码基础知识方面,比如我们在这两章中教授的内容。此外,当学习者对他们的代码感到困惑时,他们通常会向朋友或网站寻求关于代码的帮助,这也可能导致错误。为了帮助减轻错误的可能性,你可以多次请求 Copilot 的解释,因为你在多次尝试中不太可能得到完全错误的答案。

这是第一次请求 Copilot 时它给出的:

提供的 Python 代码定义了一个名为best_word的函数,它接受一个单词列表作为参数(word_list)。这个函数的目的是确定并返回列表中价值最高的单词。

函数首先初始化两个变量:best_wordbest_pointsbest_word是一个空字符串,最终将包含具有最高点值的单词,而best_points被设置为 0,将包含找到的最高点值。

函数随后进入一个循环,遍历word_list中的每个单词。对于每个单词,它通过调用num_points函数(在提供的代码中未显示)来计算单词的点值。num_points函数可能接受一个单词作为参数,并返回其点值。

如果当前单词的点值大于当前的best_points,函数将更新best_word为当前单词,并将best_points更新为当前单词的点值。这样,函数始终跟踪具有最高点值的单词。

在循环遍历完列表中的所有单词后,函数返回best_word,此时它应该是列表中具有最高点值的单词。如果列表为空或所有单词的点值都为 0,函数将返回一个空字符串。

对我们来说,这是对代码功能的一个非常好的描述。它首先对代码进行了整体解释,然后逐行进行更详细的说明。然而,请注意,要完全理解它,你需要了解重要的 Python 术语和特性。Copilot 的解释提到了函数、参数、变量、字符串、循环、做出决定(“如果点值为”),以及返回。如果你理解了这些术语,这个解释非常有用,但如果你不理解,那么它的用处就小得多。到这一章和下一章的结尾,你将了解解释 Copilot 典型解释所需的 Python 的主要术语和特性。

请求解释

你从 /explain 命令中获得的那种解释可能不符合你的要求。例如,假设你希望 Copilot 明确地给你每一行代码并解释其功能,这样你就可以更容易地将代码与其具体解释联系起来。你可以做到这一点!尝试这个提示:“再次解释那行代码,但逐行解释。给出每一行代码及其功能的解释。”这样做给了我们每一行代码及其解释的非常详细的分解。我们鼓励你如果内置命令没有提供你想要的详细程度,就自己制作提示。

4.3 你需要了解的前 10 个编程特性:第一部分

我们将带您快速浏览您在接下来的编程旅程中需要的 10 个 Python 特性,从本章中提到的这五个开始。Python 是一种交互式语言,这使得我们比其他语言更容易进行实验和查看各种功能。我们将利用这一点来探索编程特性。这是我们学习 Python 的方式,也是成千上万的程序员学习 Python 的方式。不要犹豫去尝试!要开始,请按 Ctrl–Shift–P 并输入 REPL,然后选择 Python:启动 REPL。这应该会导致图 4.5 中所示的情况。(REPL 代表读取-执行-打印-循环。之所以这样称呼,是因为 Python 会读取你输入的内容,执行/运行它,将结果打印回你,并在循环中反复这样做。)

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/4-5.png

图 4.5 从 VS Code 启动 REPL

这将把你带回到第三章中相同的 Python 提示符(如图 4.6 所示),但没有任何你的函数被加载。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/4-6.png

图 4.6 VS Code 中运行的 REPL

然后,我们可以开始输入 Python 代码。例如,输入

>>> 5 * 4

然后按 Enter 键。你会看到 20 的响应。我们在这里不会花费时间在简单的数学上,但你是如何与 Python 交互以了解其工作方式的,这与我们之前提到的那 10 个 Python 特性中的第一个完全相同:你输入一些代码,Python 就会响应。现在,让我们深入探讨我们之前提到的 10 个 Python 特性中的前五个。

4.3.1 #1. 函数

你在第三章已经学到了所有关于函数的知识,所以让我们只总结一下我们学到了什么。你使用函数将一个大问题分解成更小的部分。回顾一下,我们在第三章写的 best_word 函数是一个相当大的任务:它必须确定在一系列单词中哪个单词值得最多的分数。一个单词值多少分?啊——这是一个可以从该函数中分离出来的子任务,我们在之前的 num_points 函数中就是这样做的。

我们设计一个函数来接受参数,每个参数对应于函数需要执行其工作所需的数据块或数据集合。在完成它们的工作后,大多数函数使用 return 将答案发送回调用它们的代码行。当我们调用一个函数时,我们传递值,称为参数,每个参数一个值,我们通常使用变量来存储那个返回值。

对于我们编写的每个程序,我们可能需要设计几个函数,但 Python 也内置了一些函数,我们可以免费使用。我们可以像调用自己的函数一样调用它们。例如,有一个内置的 max 函数,它接受一个或多个参数,并告诉我们最大的值:

>>> max(5, 2, 8, 1)
8

还有 input 函数,我们在第三章的 get_strong_password 函数中使用过。它接受一个参数,该参数成为用户的提示,并返回用户在键盘上输入的内容:

>>> name = input("What is your name? ")
What is your name? Dan
>>> name
'Dan'

如果 input 是从键盘接收输入的函数,那么有没有一个 output 函数可以将消息输出到屏幕?嗯,是的,但它被称为 print,而不是 output

>>> print('Hello', name)
Hello Dan

4.3.2 #2. 变量

变量是一个指向值的名称。我们在第三章中使用变量来跟踪函数的 return 值。我们在这里也使用了一个变量来保存用户的姓名。每次我们需要记住一个值以供以后使用时,我们都会使用变量。

要将值赋给变量,我们使用等号(=)符号,这被称为 赋值 符号。它计算出右侧的值,并将其赋给变量:

>>> age = 20 + 4      #1
>>> age
24

#1 等号右侧的表达式将被评估,这意味着 20 + 4 被评估为 24。然后,变量 age 被赋予值 24。

Python 中的 = 符号与数学中的不同

在 Python 和其他编程语言中,等号(=)用于表示 赋值。等号左侧的变量被赋予等号右侧计算得到的值。这不是一个永久的关系,因为变量的值可以改变。对于数学能力强但编程新手的人来说,这可能会令人困惑,但只需记住 Python 中的等号表示赋值,而不是相等。

我们可以在更大的上下文中使用变量,这被称为 表达式。变量所指向的值会替换其名称:

>>> age + 3      #1
27
>>> age      #2
24

#1 年龄在 Python 提示符中仍然可用,其值为 24。24 + 3 被评估为 27。

#2 表达式 age + 3 并没有改变 age 的值,因为我们没有重新赋值 age。

变量在 Python 提示符中持续存在

我们在之前的代码块中分配了 age。为什么我们可以继续引用它?在 Python 提示符的编程会话期间声明的任何变量都会持续存在,直到你退出。这就是程序中变量的工作方式。一旦你为它们赋值,它们就可用。

但请注意,当我们说 age + 3 时,变量 age 并没有改变!要改变它,我们需要另一个 = 赋值语句:

>>> age = age + 5     #1
>>> age
29

#1 我们通过赋值(等号 = 符号)改变了 age。

让我们看看更多改变变量引用的方法。我们将在代码旁边添加一些注释来解释:

>>> age += 5      #1
>>> age
34
>>> age *= 2      #2
>>> age
68

#1 一个加的快捷方式。age += 5 等同于 age = age + 5

#2 一个乘以 2 的快捷方式。age *= 2 等同于 age = age * 2

4.3.3 #3. 条件语句

每当我们的程序需要做出决定时,我们都需要一个条件语句。例如,在第二章中,我们需要决定包含在数据中的球员(我们只想包含四分卫)。为此,我们使用了 if 语句。

记得我们在第三章中提到的较大值函数吗?我们在这里的列表中重新实现了它。

列表 4.2 确定两个值中较大值的函数
def larger(num1, num2):
    if num1 > num2:     #1
        return num1           #2
    else:                    #3
        return num2     #4

#1 如果 num1 大于 num2,则 num1 > num2 表达式将为 True;如果 num1 小于或等于 num2,则为 False。

#2 当 num1 大于 num2 时执行此行。

#3 else 关键字必须与 if 关键字配对。当 if 不执行(num1 > num2 为 False)时,else 将会执行。

#4 当 num1 不大于 num2 时执行此行。

列表 4.2 中的 if-else 结构被称为 条件语句,它允许我们的程序做出决定。在这里,如果 num1 大于 num2,则返回 num1;否则,返回 num2。这就是它返回较大值的方式!

if 后面,我们放置一个布尔条件(num1 > num2)。布尔条件是一个测试条件的表达式,其结果要么是 True,要么是 False。如果是 True,则 if 下的代码执行;如果是 False,则 else 下的代码执行。我们使用比较符号(如 >= 表示大于等于,< 表示小于,> 表示大于,== 表示等于,!= 表示不等于)来创建布尔表达式。请注意,我们不仅使用缩进来表示函数的代码,还使用缩进来表示 ifelse 语句的代码部分。缩进对于代码正确执行是必要的,所以值得注意(我们将在下一章中更多地讨论缩进)。这就是 Python 如何知道哪些代码行属于函数,哪些代码行还属于 ifelse

我们也可以在 Python 提示符中玩转条件语句——我们不需要在函数内部编写代码。以下是一个例子:

>>> age = 40      #1
>>> if age < 40:        #2
...     print("Binging Friends")
... else:                         #3
...     print("What's binging?")
...
What's binging?

#1 我们将 40 赋值给 age。

#2 因为 age 是 40,这段代码是在询问 40 < 40 是否成立。它不成立,所以代码的 if 部分被跳过。

#3 因为 if 条件为 False,所以执行 else 部分。

当你在if语句中输入时,你会注意到提示符从>>>变为...。提示符的变化让你知道你正在输入需要完成的代码。当你完成else代码后,你需要额外按一次 Enter 键,以退出...提示符并回到>>>提示符。

我们将年龄变量设置为 40。因为40 < 40False,所以 else 部分会执行。让我们再试一次,这次让 if 部分执行:

>>> age = 25             #1
>>> if age < 40:           #2
...     print("Binging Friends")
... else:                         #3
...     print("What's binging?")
...
Binging Friends

#1 我们将 25 赋值给年龄。

#2 因为年龄是 25,这是在询问 25 是否小于 40。它是,所以 if 部分的代码会执行。

#3 当上面的条件都不成立时,else 部分不会执行(我们已执行了代码的 if 部分)。

你可能会看到一些没有else部分的if语句,这是可以的:else部分是可选的。在这种情况下,如果条件为False,则if语句不会执行任何操作:

>>> age = 25           #1
>>> if age == 30:            #2
...     print("You are exactly 30!")
...

#1 我们将 25 赋值给年龄。

#2 == 检查两个值是否相等。

注意,测试两个值是否相等的方法是使用两个等号(==),而不是一个等号。(我们已知一个等号是用来执行赋值语句,将值赋给变量的。)

如果你有多于两种可能的结果怎么办?例如,假设一个人的年龄决定了他们可能会狂热的电视剧,如表 4.1 所示。

表 4.1 按年龄可能的喜欢的电视剧
Age Show
30–39 Friends
20–29 The Office
10–19 Pretty Little Liars
0–9 Chi’s Sweet Home

我们不能仅用if-else来捕捉所有这些结果,所以elif(即else-if)允许我们捕捉多于两个可能结果的条件逻辑,如下面的代码所示。我们在这里不包含 Python 提示符(>>>...),因为这会非常繁琐:

if age >= 30 and age <= 39:       #1
    print("Binging Friends")
elif age >= 20 and age <= 29:      #2
    print("Binging The Office")
elif age >= 10 and age <= 19:
    print("Binging Pretty Little Liars")
elif age >= 0 and age <= 9:
    print("Binging Chi's Sweet Home")
else: 
    print("What's binging?")     #3

#1 当年龄大于等于 30 且小于等于 39 时,此条件为真;例如,如果年龄是 35。

#2 如果上面的条件为 False,则会检查此条件。

#3 如果上面的所有条件都不成立,则此代码会执行。

我们使用and来捕捉一个复杂条件。例如,在第一行,我们希望年龄大于等于 30 且小于等于 39。Python 从上到下工作,当它找到一个成立的条件时,它会运行相应的缩进代码。然后,它停止检查剩余的elifelse——所以如果两个条件都成立,只有第一个条件的代码会运行。

尝试使用不同的年龄变量值进行实验,以观察在每种情况下正确的代码是否都能运行。实际上,如果我们认真测试这段代码,我们可以使用if语句结构来获得我们想要测试的值的良好感觉。这全部都是为了测试值的边界。例如,我们肯定想要测试 30 岁和 39 岁,以确保我们正确地用第一个条件捕捉到了完整的 30-39 岁范围。同样,我们还想测试 20 岁、29 岁、10 岁、19 岁、0 岁、9 岁,以及大于 39 岁的某个值,以测试底部的else方式。

如果你使用额外的if而不是elif,那么它们就变成了独立的if语句,而不是单个if语句。这很重要,因为 Python 总是独立地检查每个独立的if语句,而不管之前的if语句中发生了什么。

例如,让我们将我们的年龄代码中的elif改为if。这将给我们以下结果:

if age >= 30 and age <= 39: 
    print("Binging Friends") 
if age >= 20 and age <= 29:                #1
    print("Binging The Office")   
if age >= 10 and age <= 19:               
    print("Binging Pretty Little Liars") 
if age >= 0 and age <= 9:                 
    print("Binging Chi's Sweet Home") 
else:                          #2
    print("What's binging?")

#1 这个条件始终会被检查。

#2 这个else与最近的if语句相关联。

假设你在上述代码中将age设置为25并运行它。你认为会发生什么?嗯,第二个if条件age``>=``20``and``age``<=``29True,所以我们肯定会输出Binging The Office。但这并不是全部发生的事情!记住,因为我们在这里使用的是if,所以每个剩余的都会被检查。(如果它们是elif,我们就完成了。)age``>=``10``and``age``<=``19False,所以我们不会输出Binging Pretty Little Liars

最后的if条件age``>=``0``and``age``<=``9也是False,所以我们不会输出Binging Chi's Sweet Home。但是这个if有一个else!所以,我们确实会输出What's binging?。我们并没有打算这样做!我们只想为年龄至少为 40 岁的人输出What's binging?。这一切都是为了说明ifelif的行为不同,并且我们需要使用符合我们想要的行为的其中一个(如果我们想要多个代码块可能运行,则使用if;如果我们只想运行一个,则使用elif)。

4.3.4 #4. 字符串

正如我们在第三章中学到的,字符串是我们想要存储文本时使用的类型。文本无处不在——第二章中的统计数据、密码、书籍——所以字符串几乎出现在每个 Python 程序中。

我们使用引号来表示字符串的开始和结束。你会看到 Copilot 使用双引号或单引号。使用哪种引号都无关紧要;只需确保字符串以相同类型的引号开始和结束。

字符串附带了一组强大的方法。方法是与特定类型相关联的函数——在本例中是字符串。调用方法的方式与调用函数的方式略有不同。当你调用函数时,你将参数值放在括号内。对于方法,我们仍然将值放在括号内,但我们需要首先将我们要调用方法的价值放在括号外,然后是一个点。我们将在下一个例子中看到这一点!

在第三章中,Copilot 使用了一些字符串方法来实现is_strong_password。让我们在这里尝试使用这些方法,以便更好地理解它们是如何工作的:

>>> 'abc'.isupper()     #1
False                  
>>> 'Abc'.isupper()    
False                  
>>> 'ABC'.isupper()    
True                   
>>> 'abc'.isdigit()       #2
False                     #2
>>> '345bc'.isdigit()     #2
False                      #2
>>> '345'.isdigit()       #2
True                      #2

>>> 'abc6'.isalnum()        #3
True                         #3
>>> 'abc def'.isalnum()     #3
False                        #3
>>> 'abcdef#'.isalnum()     #3
False                       #3

#1 isupper需要所有字符都是大写才能返回 True。

#2 isdigit需要所有字符都是数字才能返回 True。

#3 isalnum需要所有字符都是字母数字才能返回 True(空格不被视为字母数字)。

还有许多其他的字符串方法。一些帮助你将字符串在大小写之间转换,其他帮助你搜索字符串,还有一些帮助你格式化字符串(例如,居中或右对齐)。

如果你对某个特定的字符串方法不确定,Copilot 的 Explain 功能通常很有帮助。例如,这个函数做什么:

def clean_number(phone_number):
    phone_number = phone_number.replace('(', '')    #1
    phone_number = phone_number.replace(')', '')     #2
    phone_number = phone_number.replace('-', '')    #3
    return phone_number

#1 从电话号码中移除所有(字符

#2 从电话号码中移除所有)字符

#3 从电话号码中移除所有–字符

记住,我们可以像在第 4.2 节中演示的那样要求 Copilot 解释代码。这是当我们要求 Copilot 解释这段代码时得到的结果:

提供的 Python 代码定义了一个名为clean_number的函数。这个函数旨在接受一个参数,phone_number,它预期是一个表示电话号码的字符串。这个函数的目的是从电话号码字符串中移除某些字符,特别是括号“(”和“)”以及连字符“-”。

函数使用了replace()方法,这是 Python 中字符串的一个内置方法。replace()方法用另一个指定的子串替换指定的子串。它接受两个参数:第一个是需要替换的子串,第二个是替换第一个子串的子串。

在这个函数中,replace()方法在phone_number上被调用了三次。每次调用replace()都用于移除一种类型的字符:首先移除开括号“(”,然后移除闭括号“)”,最后移除连字符“-”。它是通过将这些字符替换为空字符串""来实现的,从而有效地移除了它们。

在所有替换完成后,函数返回清理后的phone_number。这将是一个不包含任何括号或连字符的数字字符串。

此函数在需要以一致、无字符的格式处理或存储电话号码的情况下可能很有用。例如,你可能想在数据库中存储电话号码或比较电话号码的相等性时移除这些字符。

这确认了此函数的功能:从电话号码字符串中移除括号和连字符。并且它使用replace方法来完成此操作。

等等——replace听起来像我们正在将一些字符更改为其他字符(替换它们),而不是移除它们。那么replace是如何为我们移除字符的呢?解释说:“它是通过将这些字符替换为空字符串""来实现的,从而有效地移除了它们。”这是了解replace如何工作的一个线索,但我们可能还想了解更多。因此,我们接下来使用了提示“replace 方法是如何使用空字符串来移除字符的?”并从 Copilot 那里收到了以下解释:

Python 中的replace()方法在其被调用的字符串中替换所有指定的子字符串。

在这种情况下,指定的子字符串是字符“(”,”)”和“-”。方法被调用在phone_number字符串上,replace()的第二个参数是一个空字符串""。

因此,发生的情况是phone_number中指定字符的每个出现都被替换为空字符串。这实际上从字符串中移除了该字符,因为用无替换某物等同于移除它。

这在clean_number函数中进行了三次,每次移除一个字符。在所有三个字符都被移除后,清理后的phone_number被返回。

如果你想要学习更多,你可以继续向 Copilot 提问或在 Python 提示符下进行自己的测试。例如,你可以验证replace会移除所有(, ), 和 - 字符,即使每种字符有多个。你还可以探索使用replace来实际替换字符(其典型用例),而不是移除它们。这种探索如果需要区分真相和 Copilot 可能提供的不正确信息片段也是很有帮助的。这就是为什么我们需要自己的 Python 知识基础!

你也会经常看到在字符串上使用看起来像数学运算符的情况。它们与数学符号相同,但在字符串上执行不同的操作。+运算符用于将两个字符串拼接在一起,而*运算符用于将字符串重复特定次数。示例如下:

>>> first = 'This is a '
>>> second = 'sentence.'
>>> sentence = first + second      #1
>>> print(sentence)
This is a sentence.
>>> print('-=' * 5)     #2
-=-=-=-=-=

#1 将第一和第二字符串合并并将结果赋值给 sentence

#2 将“-”字符串重复五次

4.3.5 #5. 列表

当我们有一个字符序列时,例如密码或单个 Scrabble 单词,字符串就非常棒。但有时,我们需要存储许多单词或许多数字。为此,我们需要一个列表。我们在第三章使用列表来处理best_word函数,因为这个函数需要与单词列表一起工作。

*与我们在字符串的开始和结束处使用引号一样,我们使用开闭方括号来开始和结束列表。至于字符串,列表上有许多可用方法。为了给你一个关于可用列表方法和它们做什么的印象,让我们探索一些这些方法:

>>> books = ['The Invasion', 'The Encounter', 'The Message']     #1
>>> books
['The Invasion', 'The Encounter', 'The Message']
>>> books.append('The Predator')                                 #2
>>> books
['The Invasion', 'The Encounter', 'The Message', 'The Predator']
>>> books.reverse()                                               #3
>>> books
['The Predator', 'The Message', 'The Encounter', 'The Invasion']

#1 包含三个字符串值的列表

#2 在列表末尾添加一个新的字符串值

#3 反转列表(现在值是相反的顺序)

许多 Python 类型,包括字符串和列表,允许你使用索引来处理特定的值。当你想要处理字符串或列表的一部分而不是整个字符串或列表时,你需要使用索引。索引只是一个标识元素的数字。索引从 0 开始,对于第一个元素,然后一直增加到但不包括值的数量。第一个值有索引 0(不是索引 1!),第二个有索引 1,第三个有索引 2,依此类推。列表中的最后一个值在索引处,该索引是列表长度减 1。列表的长度可以通过使用len函数来确定。例如,如果我们做len(books),我们会得到一个值为4(因此有效的索引是从 0 到包括 3)。人们也经常使用负索引,这为每个值提供了另一种索引方式:最右侧的值有索引-1,其左侧的值有索引-2,依此类推。图 4.7 展示了正索引和负索引的示例。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/lrn-ai-ass-py-prog-2e/img/4-7.png

图 4.7 列表元素可以通过正索引或负索引访问。

让我们在当前的书籍列表上练习索引:

>>> books
['The Predator', 'The Message', 'The Encounter', 'The Invasion']
>>> books[0]        #1
'The Predator'
>>> books[1]
'The Message'
>>> books[2]
'The Encounter'
>>> books[3]
'The Invasion'
>>> books[4]                        #2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> books[-1]                   #3
'The Invasion'
>>> books[-2]
'The Encounter'

#1 books[0]对应于第一个元素。

#2 错误是因为索引 3 是最后一本书!

#3 books[-1]指向列表中的最后一个元素。

此外,还有一种方法可以从字符串或列表中提取多个值,而不仅仅是单个值。这被称为切片。我们指定第一个值的索引,一个冒号,以及值右侧的索引,如下所示:

>>> books[1:3]             #1
['The Message', 'The Encounter']

#1 从索引 1 开始,到索引 2 结束(不是 3!)

我们指定了 1:3,所以你可能期望得到包括索引 3 在内的值。但冒号后面的第二个索引(即值之后的索引)不包括在内。这听起来很反直觉,但却是真的!

如果我们省略起始或结束索引,Python 会根据需要使用起始或结束索引:

>>> books[:3]                                      #1
['The Predator', 'The Message', 'The Encounter']
>>> books[1:]                                      #2
['The Message', 'The Encounter', 'The Invasion']

#1 与使用书籍[0:3]相同

#2 与使用 books[1:4]相同

我们还可以使用索引来更改列表中的特定值,例如:

>>> books
['The Predator', 'The Message', 'The Encounter', 'The Invasion']
>>> books[0] = 'The Android'      #1
>>> books[0]
'The Android'
>>> books[1] = books[1].upper()      #2
>>> books[1]
'THE MESSAGE'
>>> books
['The Android', 'THE MESSAGE', 'The Encounter', 'The Invasion']

#1 将书籍[0]更改为指向字符串值“Android”

#2 将书籍[1]更改为全部大写

如果我们在字符串上尝试这样做,我们会得到一个错误:

>>> title = 'The Invasion'
>>> title[0]              #1
'T'
>>> title[1]
'h'
>>> title[-1]
'n'
>>> title[0] = 't'                   #2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

#1 查找字符没有问题。

#2 但赋值不会!

字符串被称为不可变值,这意味着您不能更改其字符。您只能创建一个全新的字符串。相比之下,列表被称为可变值,这意味着您可以更改它。如果您收到有关类型不支持项赋值错误的错误,您很可能是尝试更改无法更改的值。

在本章中,我们向您介绍了 Python 中最常见的五个代码特性。我们将在下一章继续介绍另外五个。我们还向您展示了如何使用 Copilot 解释工具来帮助您理解代码,并提供了验证这些解释真实性的指导。表 4.2 提供了本章所涵盖特性的总结。

表 4.2 本章 Python 代码特性总结
代码元素 示例 简要描述
函数 def larger(num1, num2) 允许我们管理代码复杂性的代码特性。函数接受输入,处理这些输入,并可能返回一个输出。
变量 age = 25 一个可读性高的人名,它指向一个存储的值。可以使用=赋值语句进行赋值。
条件语句 if age < 18: print("Can't vote") else: print("Can vote") 条件语句允许代码做出决策。在 Python 中,我们有与条件相关的三个关键字:ifelifelse
字符串 name = 'Dan' 字符串存储一系列字符(文本)。有许多强大的方法可用于修改字符串。
列表 list = ['Leo', 'Dan'] 一个包含任何类型值的序列。有许多强大的方法可用于修改列表。

4.4 练习

  1. 回想一下我们在列表 4.2(第 4.3.3 节)中查看的条件代码。这个程序的行为与原始程序有何不同?
def larger(num1, num2):
    if num1 < num2:
        return num1
    else:
        return num2
    1. 在以下代码中,如果年龄是 32,会打印什么?如果您将除了第一个之外的所有if改为elif,会打印什么?
if age >= 30 and age <= 39:
    print("Binging Friends")
if age >= 25 and age <= 34:      
    print("Binging The Office")
if age >= 15 and age <= 24:
    print("Binging Pretty Little Liars")
if age >= 0 and age <= 14:
    print("Binging Chi's Sweet Home")
else:
    print("What's binging?")
    1. 你会使用什么 Python 数据类型来存储传感器每分钟记录的一系列温度读数?这种数据类型会存储什么?
    1. 将以下 Python 代码块排列成程序,以计算提供的列表中所有偶数的总和。
for number in numbers:
total += number
if number % 2 == 0: 
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 
total = 0 
print(f"The sum of all even numbers is: {total}")
    1. (回忆第 4.3.1 节。)考虑以下 Python 函数调用min(input("Enter a number: "), input("Enter another number: "))。在运行此代码之前,思考这一行代码的预期功能,然后回答以下问题:

    2. 你期望运行此代码时会发生什么?描述执行过程的每个步骤。

    3. 在这个场景中,Python 是如何处理输入和min函数的?

    4. 如果输入的不是数字,而是可以表示数字的字符串(例如,52),Python 的行为会怎样改变,如果有的话?

    5. 如果一个或两个输入不是数字,结果会怎样?通过尝试不同的输入来测试您的假设。

    1. 在 4.3.2 节中,我们讨论了变量在 Python 中的持久性。变量的这种属性如何影响你编写代码的方式?考虑一个示例场景,其中在程序的不同部分保持变量状态将是有益的。
    1. 我们要求 Copilot 解释以下代码的工作原理:
age = int(input("Enter your age: "))

if age < 13:
    print("You are a child.")
elif age < 20:
    print("You are a teenager.")
elif age < 60:
    print("You are an adult.")
else:
    print("You are a senior.")

Copilot 提供了以下解释:

这段 Python 代码是一个简单的程序,它会询问用户的年龄,然后根据用户所属的年龄范围打印出一条消息。

代码的第一行使用了input()函数来提示用户输入他们的年龄。input()函数从输入中读取一行(通常来自用户的键盘),将其转换为字符串,并返回该字符串。在这种情况下,提示是“Enter your age: ”。然后使用int()函数将该字符串转换为整数并存储在变量age中。

代码的其余部分是一系列条件语句,用于检查年龄的值并打印出相应的消息。if语句检查年龄是否小于 13 岁,如果是,则打印“你是一个孩子。”如果年龄不小于 13 岁,程序将移动到下一个条件,即elif(代表else if)语句,它检查年龄是否小于 20 岁。如果是,则打印“你是一个青少年。”如果年龄不小于 20 岁,它将移动到下一个elif语句,该语句检查年龄是否小于 60 岁。如果是,则打印“你是一个成年人。”

结果表明,Copilot 的解释不完整,因为它没有描述代码对所有输入的处理。确定解释中缺少的内容,并将你的答案添加到解释中。

    1. 审查提供的 Python 代码和初始提示给 Copilot。返回的解释过于模糊,没有帮助你理解代码中使用的字符串方法。
text = "Hello, World! Programming is fun."
index = text.find("World")
new_text = text[index:].replace("World", "Everyone")

print(new_text)

给 Copilot 的提示:

你能解释一下这段代码的作用吗?

Copilot 给出的解释:

代码涉及通过查找特定单词并将其替换为另一个单词来操作存储在文本中的字符串。它使用方法来定位单词的开始位置,然后构建一个新的字符串,其中单词已被替换。最后,它打印出修改后的字符串。

你的任务是执行以下操作:

  1. 确定为什么初始提示可能导致模糊或不实用的解释。

  2. 改进提示以确保 Copilot 提供更详细和具体的解释。考虑你具体不理解字符串方法以及代码如何使用它们。

  3. 将修订后的提示写给 Copilot,并讨论你的更改如何导致更好的解释。

摘要

  • 我们需要能够阅读代码以确定其正确性,有效地测试它,并在需要时编写自己的代码。

  • Copilot 可以逐行解释代码,以解释代码正在做什么,你可以使用自己的提示来影响你获得的解释类型。

  • Python 有内置函数,如maxinputprint,我们可以像调用自己的函数一样调用它们。

  • 变量是一个指向值的名称。

  • 一个赋值语句使一个变量指向一个特定的值。

  • 一个if语句用于让我们的程序做出决策,并沿着多条路径中的其中一条继续执行。

  • 字符串用于存储和操作文本。

  • 方法是与特定类型相关联的函数。

  • 列表用于存储和操作一系列通用的值(例如,一系列数字或一系列字符串)。

  • 字符串或列表中的每个值都有一个索引;索引从 0 开始,而不是 1。

  • 字符串是不可变的(不可更改的);列表是可变的(可更改的)*。

第五章:阅读 Python 代码:第二部分

本章涵盖

  • 使用循环重复所需的代码次数

  • 使用缩进来告诉 Python 哪些代码属于一组

  • 构建字典来存储相关值的对

  • 设置文件以读取和处理数据

  • 使用模块在新的领域工作

在第四章中,我们探讨了你在继续你的编程之旅时将经常看到的五个 Python 特性:函数、变量、条件语句(if 语句)、字符串和列表。你需要了解这些特性来阅读代码,我们也解释了为什么无论是否使用 Copilot,能够阅读代码都很重要。

在本章中,我们将继续介绍五个更多的 Python 特性,这将使我们的前 10 个特性更加完整。与第四章一样,我们将通过我们自己的解释、Copilot 的解释以及在 Python 提示符下的实验来做到这一点。

5.1 你需要知道的 10 个编程特性:第二部分

本节详细介绍了你需要知道的下一个五个顶级编程特性。让我们从上一章留下的地方继续,即第 6 个特性:循环。

5.1.1 #6. 循环

循环允许计算机根据需要重复执行相同的代码块。如果我们前 10 个编程特性中的任何一个能体现为什么计算机对我们完成工作如此有用,那就是这个特性。如果没有循环的能力,我们的程序通常会按顺序逐行执行。当然,它们仍然可以调用函数并使用 if 语句来做出决定,但程序完成的工作量将与我们编写的代码量成比例。但循环不是这样:一个循环可以轻松处理成千上万的值。

有两种类型的循环:for 循环和 while 循环。一般来说,当我们知道循环需要运行多少次时,我们使用 for 循环;当我们不知道时,我们使用 while 循环。例如,在第三章中,我们的 best_word 函数(如列表 5.1 所示)使用了一个 for 循环,因为我们知道循环需要运行多少次:对 word_list 中的每个单词运行一次!但在 get_strong_password 中,我们将在列表 5.4 中再次看到它,我们使用了一个 while 循环,因为我们不知道用户在输入一个强密码之前会输入多少个坏密码。我们将从 for 循环开始,然后转向 while 循环。

列表 5.1 来自第三章的 best_word 函数
def best_word(word_list):
 """
 word_list is a list of words.

 Return the word worth the most points.
 """
    best_word = ""
    best_points = 0
    for word in word_list:       **#1
        points = num_points(word)
        if points > best_points:
            best_word = word
            best_points = points
    return best_word**

#1 这是一个 for 循环的例子。 **for 循环允许我们访问字符串或列表中的每个值。让我们先从一个字符串开始尝试:

>>> s = 'vacation'
>>> for char in s:       #1
...     print('Next letter is', char)    #2
...
Next letter is v
Next letter is a
Next letter is c
Next letter is a
Next letter is t
Next letter is i
Next letter is o
Next letter is n

#1 这将重复缩进的代码,每次对应字符串 s 中的一个字符。

#2 因为“vacation”有八个字母,所以这段代码将运行八次。

注意,我们不需要为 char 赋值语句。这是因为它是一个特殊的变量,称为循环变量,它由 for 循环自动管理。char 代表字符,这是人们用来命名循环变量的一个非常流行的名字。char 变量会自动分配字符串中的每个字符。在谈论循环时,我们经常使用单词 迭代 来指代每次通过循环执行的代码。例如,我们可以说在第一次迭代中,char 指的是 v;在第二次迭代中,它指的是 a;依此类推。注意,就像函数和 if 语句一样,我们为构成循环的代码有缩进。在这个循环的主体中,我们只有一行代码,但就像函数和 if 语句一样,我们也可以有更多。

让我们看看一个 for 循环在列表上的示例(列表 5.2),演示我们可以像处理字符串的每个值一样处理列表的每个值。我们还会在循环中放入两行代码,而不是一行,以演示这是如何工作的。

列表 5.2 使用 for 循环的示例
>>> lst = ['cat', 'dog', 'bird', 'fish']
>>> for animal in lst:               #1
...     print('Got', animal)       #2
...     print('Hello,', animal)   ** #2
...
Got cat
Hello, cat
Got dog
Hello, dog
Got bird
Hello, bird
Got fish
Hello, fish**

**#1 第一个是一个列表,所以这是一个列表上的 for 循环。

#2 这段代码在每次迭代时运行。** **列表 5.2 中的代码只是循环遍历列表的一种方式。for animal in lst 的方法在每次通过循环时将变量 animal 赋值为列表中的下一个值。作为替代,您可以使用索引来访问列表中的每个元素。为此,我们需要了解内置的 range 函数。

range 函数可以给你一个范围内的数字。我们可以提供一个起始数字和一个结束数字,它将生成从起始数字开始,但不包括结束数字的范围。要查看 range 生成的数字,我们需要在它周围放置 list 函数。以下是一个使用 range 的示例:

>>> list(range(3, 9))     #1
[3, 4, 5, 6, 7, 8]

#1 生成从 3 到 8 的范围(不是 3 到 9!)

注意,它从值 3 开始,包括 38 之间的所有值。也就是说,它包括从起始值 3 到,但不包括,结束值 9 的所有数字。

那么,range 如何帮助我们编写循环呢?嗯,而不是在范围中硬编码像 3 和 9 这样的数字,我们可以包括字符串或列表的长度,如下所示:

>>> lst
['cat', 'dog', 'bird', 'fish'] 
>>> list(range(0, len(lst)))      #1
[0, 1, 2, 3]

#1 从 0 开始,直到但不包括 1st 的长度。

注意,这里的范围值是 0、1、2、3,这是我们的 lst 列表的有效索引!因此,我们可以使用 range 来控制 for 循环,这将使我们能够访问字符串或列表中的每个有效索引。

我们可以使用 range 在列表 5.2 中执行相同的任务。查看列表 5.3 以获取新代码。

列表 5.3 使用 for 循环和 range 的循环示例
>>> for index in range(0, len(lst)):        #1
...     print('Got', lst[index])        #2
...     print('Hello,', lst[index])    ** #2
...
Got cat
Hello, cat
Got dog
Hello, dog
Got bird
Hello, bird
Got fish
Hello, fish**

**#1 使用 range 函数的 for 循环

#2 使用索引变量对列表进行索引** **在这里,我们使用了一个名为index的变量,但你也经常会看到人们为了简单起见只使用i。该变量将在循环的第一次迭代时被赋予0的值,第二次迭代时为1,第三次迭代时为2,最后一次迭代时为3。它停止在3,因为列表的长度是 4,而range在它之前停止。使用列表的索引,代码获取第一个元素,然后是第二个,然后是第三个,最后是第四个,使用递增的索引。我们也可以不写0来编写for循环;range将假设我们想要从0到提供的值的值,如下所示:

for index in range(len(lst)):    #1
    print('Got', lst[index])
    print('Hello,', lst[index])

#1 使用一个参数时,range假设我们要从 0 开始。

我们在这里就停止for循环的讨论。但我们还没有结束循环的讨论,因为还有另一种类型的循环需要我们讨论:while循环。

当我们不知道要循环多少次时,我们会使用while循环。这种情况的一个很好的例子是在第三章的get_strong_password函数中。我们在这里将其代码作为列表 5.4 重现。

列表 5.4 第三章的get_strong_password函数
def get_strong_password():
 """
 Keep asking the user for a password until it’s a strong password,
 and return that strong password.
 """
    password = input("Enter a strong password: ")
    while not is_strong_password(password):           #1
        password = input("Enter a strong password: ")
    return password

#1 当密码不够强大时持续循环

我们不知道用户会输入多少个密码,直到他们输入一个强大的密码。是第一次尝试、第二次尝试,还是第 50 次尝试?谁知道。这就是为什么我们需要一个while循环:它确保用户会不断被要求输入,直到他们提供一个强大的密码。

要编写一个while循环,我们需要在while后面跟一个布尔条件。如果它是True,Python 将运行循环的一次迭代。如果在那次迭代之后,那个布尔条件仍然是True,那么它将运行另一次迭代。它将一直这样做,直到条件变为False,此时循环终止。

现在,我们有了足够的信息来理解我们的get_strong_password函数的代码。通过查看代码,我们可以看到它首先使用input函数来要求用户输入密码。然后,有一个while循环。这个循环会一直持续,直到密码不是一个强大的密码。布尔条件not is_strong_password(password)等同于写作is_strong_password(password) == False。为了避免无限循环并要求用户再次尝试,我们在循环内部又使用了另一个input。我们需要在循环上面的input调用,以从用户那里获取第一次尝试。如果没有这个,password变量就不会存在,而while循环行会出错,因为它没有password值传递给is_strong_password函数。

不要忘记,我们可以向 Copilot 请求对这个代码的解释,使用我们在第四章第 4.2 节学到的知识。当你觉得你并不完全理解代码正在做什么或它是如何工作时,这很有用。这是我们从 Copilot 那里收到的解释:

这段 Python 代码定义了一个名为get_strong_password的函数。这个函数的目的是交互式地提示用户输入密码,并持续询问,直到用户提供一个被认为是强密码的密码。构成强密码的标准在此函数中未定义,而是由另一个名为is_strong_password的函数确定。

函数开始时使用 Python 的内置input函数提示用户输入密码。然后,输入的密码存储在变量password中。

接下来,函数进入一个while循环。这个循环的条件是not is_strong_ password(password)。这意味着只要is_strong_password函数在用当前密码调用时返回False,循环就会继续执行。换句话说,循环会一直运行,直到用户输入一个被认为是强密码的密码。

在循环内部,函数再次提示用户输入密码。新密码替换了之前存储在password变量中的旧密码。如果这个新密码仍然不够强,循环会继续提示用户。

一旦用户输入了一个被认为是强密码(即is_strong_password(password)返回True),while循环的条件变为False,循环结束。然后函数返回用户输入的强密码。

这个函数是使用while循环反复提示用户输入直到输入满足某些条件的一个很好的例子。然而,强密码的实际标准并未在此函数中定义,需要在is_strong_password函数中实现。

这个解释非常详细地描述了函数,与我们刚刚给出的描述相匹配!它还包含了一些有用的 Python 通用信息,包括在最后一段中提醒我们为什么我们使用while循环。解释还提到我们需要is_strong_password函数来告诉我们密码是否强大;我们没有强调这个函数,这就是为什么 Copilot 告诉我们需要实现它。

Copilot 解释可能会出错

我们选择之前的 Copilot 解释,因为它是在我们要求 Copilot 解释代码三次后给出的最佳答案。它给出的其中一个答案听起来相当可信,直到它开始谈论不存在的函数。我们相信,如果您多次运行并寻找共同的想法,这些解释可以作为学习辅助工具很有帮助,但本章的主要目标是为您提供理解何时出错所需的工具。

我们鼓励您继续使用 Copilot 解释,如果您感兴趣,可以要求 Copilot 解释您仍然好奇的任何前几章的代码。再次提醒,这些解释可能会出错,因此您应该要求 Copilot 提供多个解释以减少对单个错误解释的依赖。

就像现在与任何与 AI 编码助手相关的事情一样,它们可能会出错。但我们在这里给出解释,因为我们认为 Copilot 的这个功能现在是一个潜在的有力教学资源,而且随着 Copilot 的改进,这一点将变得更加真实。

在我们不知道迭代次数的情况下,我们应该使用 while 循环。但即使我们知道迭代次数,我们也可以使用 while 循环。例如,我们可以使用 while 循环来处理字符串中的字符或列表中的值。我们有时会在 Copilot 生成的代码中看到它这样做,尽管使用 for 循环可能更好。例如,我们可以使用 while 循环来处理我们之前提到的 animals 列表中的动物,如下所示。但这会多做一些工作!

列表 5.5 使用 while 循环的循环示例
>>> lst
['cat', 'dog', 'bird', 'fish'] 
>>> index = 0
>>> while index < len(lst):        #1
...     print('Got', lst[index])
...     print('Hello,', lst[index])
...     index += 1          #2
...
Got cat
Hello, cat
Got dog
Hello, dog
Got bird
Hello, bird
Got fish
Hello, fish

#1 len 函数告诉我们字符串的长度,这也是我们想要的迭代次数。

#2 这是常见的错误,很多人都会犯这个错误!

如果没有 index += 1,我们就永远不会增加字符串中的索引,我们会不断地输出第一个值的详细信息。这被称为无限循环。如果你回想一下我们是如何编写 for 循环的,你会发现我们不必手动增加任何索引变量。出于这些原因,许多程序员在可能的情况下更喜欢使用 for 循环。我们不必在 for 循环中手动跟踪任何索引,因此我们自动避免了某些类型的索引问题和无限循环。

5.1.2 #7. 缩进

在 Python 代码中,缩进至关重要,因为 Python 使用它来确定哪些代码行属于一起。这就是为什么,例如,我们总是在函数内部的代码行、if 语句的各个部分以及 forwhile 循环的代码中进行缩进。这不仅仅是格式化得更好:如果我们缩进错误,那么代码也会出错。例如,假设我们想要询问用户当前的小时,然后根据是早上、下午还是晚上输出一些文本:

  • 如果是早上,我们想要输出“早上好!”和“祝您有个愉快的一天。”

  • 如果是下午,我们想要输出“下午好!”

  • 如果是晚上,我们想要输出“晚上好!”和“祝您有个美好的夜晚。”

看看我们编写的以下代码,并尝试找出缩进的问题:

hour = int(input('Please enter the current hour from 0 to 23: '))

if hour < 12:
    print('Good morning!')
    print('Have a nice day.')
elif hour < 18:
    print('Good afternoon!')
else:
    print('Good evening!')
print('Have a good night.')     #1

#1 这一行没有缩进。

问题在于最后一行:它没有缩进,但它应该缩进!因为它没有缩进,所以无论用户输入哪个小时,我们都会输出 Have a good night.。我们需要缩进它,使其成为 if 语句的 else 部分的一部分,确保它只在晚上执行。

无论何时编写代码,我们都需要使用多级缩进来表达哪些代码片段与函数、if语句、循环等相关联。例如,当我们编写函数头时,我们需要将函数头下面的所有相关代码缩进。一些语言使用括号(例如{})来显示这一点,但 Python 只是缩进。如果你已经在函数体(一个缩进)中编写了一个循环,那么你将需要再次缩进(两个缩进)以缩进循环体,依此类推。

回顾第三章中的函数,我们可以看到这一点。例如,在我们的larger函数(重新打印为列表 5.6)中,整个函数体都是缩进的,但在if语句的if部分和else部分有进一步的缩进。

列表 5.6 比较两个值大小的函数
def larger(num1, num2):
    if num1 > num2:       #1
        return num1    #2
    else:                 #3
        return num2       #4

#1 这显示了函数体的单级缩进。

#2 这显示了函数体和 if 语句体的双重缩进。

#3 这显示了函数体的单级缩进。

#4 这显示了函数体和 else 语句体的双重缩进。

接下来,考虑我们之前在列表 5.4 中查看的get_strong_password函数:通常,函数中的所有内容都是缩进的,但while循环体的缩进更深。

在我们num_points函数的第一版中(此处从第三章的列表 5.7 中复制),甚至还有更多级别的缩进。这是因为,在遍历单词每个字符的for循环内部,我们有一个if语句。正如我们所学的,if语句的每一部分都需要缩进,从而导致额外的缩进级别。

列表 5.7 num_points函数
def num_points(word): 
 """ 
 Each letter is worth the following points: 
 a, e, i, o, u, l, n, s, t, r: 1 point 
 d, g: 2 points 
 b, c, m, p: 3 points 
 f, h, v, w, y: 4 points 
 k: 5 points 
 j, x: 8 points 
 q, z: 10 points 

 word is a word consisting of lowercase characters. 
 Return the sum of points for each letter in word. 
 """
    points = 0
    for char in word:            #1
        if char in "aeioulnstr":     #2
            points += 1          #3
        elif char in "dg":
            points += 2
        elif char in "bcmp":
            points += 3
        elif char in "fhvwy":
            points += 4
        elif char == "k":
            points += 5
        elif char in "jx":
            points += 8
        elif char in "qz":
            points += 10
    return points

#1 这是为了位于函数内部而缩进的。

#2 这再次缩进,以便位于 for 循环内部。

#3 这再次缩进,以便位于 if 语句内部。

is_strong_password函数中也有额外的缩进(此处从第三章的列表 5.8 中复制),但这只是为了将一条超长的代码行扩展到多行。注意,这些行以\结尾,这是我们可以在下一行继续代码行的字符。

列表 5.8 is_strong_password函数
def is_strong_password(password):
 """
 A strong password has at least one uppercase character,
 at least one number, and at least one punctuation.

 Return True if the password is a strong password, 
 False if not.
 """
    return any(char.isupper() for char in password) and \     #1
           any(char.isdigit() for char in password) and \     #2
           any(char in string.punctuation for char in password)

#1 这行以反斜杠结尾,以继续语句。

#2 缩进不是必需的,但有助于在视觉上布局单行返回语句。

类似地,在我们的num_points函数的第二版中(此处从第三章的列表 5.9 中复制)也有一些进一步的缩进,但这只是为了将字典扩展到多行,使其更易于阅读。

列表 5.9 num_points的替代解决方案
 def num_points(word): 
 """ 
 Each letter is worth the following points: 
 a, e, i, o, u, l, n, s, t, r: 1 point 
 d, g: 2 points 
 b, c, m, p: 3 points 
 f, h, v, w, y: 4 points 
 k: 5 points 
 j, x: 8 points 
 q, z: 10 points 

 word is a word consisting of lowercase characters. 
 Return the sum of points for each letter in word. 
 """ 
    points = {'a': 1, 'e': 1, 'i': 1, 'o': 1, 'u': 1, 'l': 1,     #1
              'n': 1, 's': 1, 't': 1, 'r': 1,        #2
              'd': 2, 'g': 2,
              'b': 3, 'c': 3, 'm': 3, 'p': 3,
              'f': 4, 'h': 4, 'v': 4, 'w': 4, 'y': 4,
              'k': 5,
              'j': 8, 'x': 8,
              'q': 10, 'z': 10}
    return sum(points[char] for char in word)

#1 我们允许将字典值写为多行。

#2 缩进不是必需的,但有助于在视觉上布局字典。

缩进对程序最终执行的结果有很大影响。例如,让我们比较使用连续的两个循环与使用缩进嵌套一个循环在另一个循环中使用的情况。这里有连续的两个循环:

>>> countries = ['Canada', 'USA', 'Japan']
>>> for country in countries:       #1
...     print(country)
...
Canada
USA
Japan
>>> for country in countries:      #2
...     print(country)
...
Canada
USA
Japan

#1 这是第一个循环。

#2 这是第二个循环(在第一个循环之后发生)。

这导致我们得到了相同的输出两次,因为我们两次分别遍历了国家列表。现在,如果我们嵌套循环,情况如下:

>>> for country1 in countries:            #1
...     for country2 in countries:        #2
...         print(country1, country2)    #3
...
Canada Canada
Canada USA
Canada Japan
USA Canada
USA USA
USA Japan
Japan Canada
Japan USA
Japan Japan

#1 这是第一个循环。

#2 这是第一个循环中的嵌套循环。

#3 print是在第二个循环中嵌套的,而第二个循环又嵌套在第一个循环中。

我们为每个for循环使用了不同的变量名,country1country2,这样我们就可以引用它们。在country1循环的第一次迭代中,country1指的是加拿大。在country2循环的第一次迭代中,country2同样指的是加拿大。这就是为什么第一行输出是加拿大 加拿大。你期望下一行输出是USA USA吗?但这并不是发生的事情!相反,country2循环继续到它的下一次迭代,但country1循环还没有移动。country1循环只有在country2循环完成后才会向前移动。这就是为什么我们在country1循环最终移动到第二次迭代之前得到了加拿大 USA加拿大 日本。当一个循环在另一个循环内部时,这被称为嵌套循环。一般来说,当有嵌套时,内循环(for country2 in countries)将在外循环(for country1 in countries)移动到它的下一步之前完成所有步骤,然后外循环将重新启动内循环。

如果你看到嵌套在另一个循环内部的循环,那么很可能这些循环正在用于处理二维数据。二维数据组织成行和列,就像你在表格中看到的那样(例如,表 5.1)。这种数据在计算机中非常常见,因为它包括基本的工作表数据,如 CSV 文件,图像如照片或视频的单帧,或者计算机屏幕。

在 Python 中,我们可以使用一个列表来存储二维数据,其中值本身是其他列表。列表中的每个子列表是整体列表中的一行数据,每行都有一个列值。例如,假设我们有关于 2018 年冬季奥运会花样滑冰奖牌的一些数据,如表 5.1 所示。

表 5.1 2018 年冬季奥运会奖牌
国家 金牌 银牌 铜牌
加拿大 2 0 2
OAR 1 2 0
日本 1 1 0
中国 0 1 0
德国 1 0 0

我们可以将这些存储为一个列表,每行一个国家:

>>> medals = [[2, 0, 2],
...           [1, 2, 0],
...           [1, 1, 0],
...           [0, 1, 0],
...           [1, 0, 0]]

注意,我们的列表列表只是存储了数值,我们可以通过引用其行和列来找到列表列表中的值(例如,日本的金牌对应于索引为 2 的行和索引为 0 的列)。我们可以使用索引来获取完整的数据行:

>>> medals[0]    #1
[2, 0, 2]
>>> medals[1]    **#2
[1, 2, 0]
>>> medals[-1]    **#3
[1, 0, 0]****

****#1 这是第一行(第一行)。

#2 这是第一行(第二行)。

#3 这是最后一行。**** ****如果我们对这个列表执行for循环,我们将逐行获取每个完整的行:

>>> for country_medals in medals:     #1
...     print(country_medals)
...
[2, 0, 2]
[1, 2, 0]
[1, 1, 0]
[0, 1, 0]
[1, 0, 0]

#1 for循环一次给我们列表中的一个值(即一次子列表)。

如果我们只想从奖牌列表中获取特定的值(而不是整个行),我们必须索引两次:

>>> medals[0][0]   #1
2
>>> medals[0][1]    #2
0
>>> medals[1][0]    #3
1

#1 这是第一行,第一列。

#2 这是第一行,第一列。

#3 这是第一行,第一列。

假设我们想要逐个遍历每个值。为了做到这一点,我们可以使用嵌套for循环。为了帮助我们确切地跟踪我们的位置,我们将使用range for循环,这样我们就可以打印出当前的行和列数字,以及存储在该处的值。

外层循环将遍历行,因此我们需要使用range (len(medals))来控制它。内层循环将遍历列。有多少列?嗯,列的数量是行中值的数量,因此我们可以使用range(len(medals[0]))来控制这个循环。

每行输出将提供三个数字:行坐标、列坐标以及在该行和列的值(奖牌数量)。以下是代码和输出:

>>> for i in range(len(medals)):          #1
...     for j in range(len(medals[i])):      #2
...             print(i, j, medals[i][j])
...
0 0 2
0 1 0
0 2 2
1 0 1
1 1 2
1 2 0
2 0 1
2 1 1
2 2 0
3 0 0
3 1 1
3 2 0
4 0 1
4 1 0
4 2 0

#1 遍历行

#2 遍历当前行的列

注意,在输出的前三行中,行保持不变,而列从 0 到 2 变化。这就是我们如何遍历第一行的方式。只有在行增加到 1 之后,我们才完成对这一新行上列 0 到 2 的工作。

嵌套循环为我们提供了一种系统地遍历二维列表中每个值的系统方法。在处理二维数据时,你经常会看到它们,例如图像、棋盘游戏和电子表格。

5.1.3 #8. 字典

记住,Python 中的每个值都有一个特定的类型。由于我们可能想要使用许多不同类型的值,因此存在许多不同的类型!我们已经讨论了使用数字来处理数值,布尔值来处理True/False值,字符串来处理文本,以及列表来处理其他值(如数字或字符串)的序列。

在 Python 中,还有一个经常出现的类型,它被称为字典。当我们谈论 Python 中的字典时,我们并不是指单词及其定义的列表。在 Python 中,字典是一种在需要跟踪数据之间的关联时非常有用的存储数据的方式。例如,想象一下,如果你想知道你最喜欢的书中使用最频繁的单词。你可以使用字典将每个单词映射到其使用的次数。这样一个字典可能非常大,但这样一个字典的小版本可能看起来像这样:

>>> freq = {'DNA': 11, 'acquire': 11, 'Taxxon': 13, \
... 'Controller': 20, 'morph': 41}

字典中的每个条目将一个单词映射到其频率。例如,我们可以从这个字典中得知单词DNA出现了 11 次,而单词Taxxon出现了 13 次。这里的单词(DNAacquireTaxxon等)被称为,而频率(11,11,13 等)被称为。因此,字典将每个键映射到其值。我们不允许有重复的键,但正如这里所示的两个11值,有重复的值是没有问题的。

我们在第二章(列表 2.1)中看到了一个字典,它存储了每个四分卫的名字和他们相关的传球码数。在第三章中,我们又看到了一个字典,这是我们的第二个num_points解决方案(在列表 5.9 中较早重现)。在那里,字典将每个字母映射到使用该字母所获得的分数。

就像字符串和列表一样,字典也有你可以用来与之交互的方法。以下是一些在freq字典上操作的方法:

>>> freq
{'DNA': 11, 'acquire': 11, 'Taxxon': 13, 'Controller': 20, 'morph': 41}
>>> freq.keys()                **#1
dict_keys(['DNA', 'acquire', 'Taxxon', 'Controller', 'morph'])
>>> freq.values()                   #2
dict_values([11, 11, 13, 20, 41])
>>> freq.pop('Controller')         #3
20
>>> freq
{'DNA': 11, 'acquire': 11, 'Taxxon': 13, 'morph': 41}**

**#1 获取所有键

#2 获取所有值

#3 删除键及其关联的值** **你也可以使用索引符号来访问给定键的值:

>>> freq['dna']  # Oops, wrong key name because it is case sensitive
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'dna'
>>> freq['DNA']       #1
11
>>> freq['morph']
41

#1 获取与键“DNA”关联的值

字典,就像列表一样,是可变的。这意味着我们可以更改字典中的键和值,这对于模拟随时间变化的数据很有用。我们可以使用索引来更改值。与'morph'关联的值目前是41。让我们将其更改为6

>>> freq['morph'] = 6     #1
>>> freq
{'DNA': 11, 'acquire': 11, 'Taxxon': 13, 'morph': 6}

#1 将与键“morph”关联的值更改为 6

我们的freq字典允许我们从任何我们想要的单词开始,并找到它的频率。更普遍地说,字典允许我们从键到值进行转换。然而,它不允许我们轻松地朝相反的方向转换,从值到键。如果我们想这样做,我们需要生成相反的字典——例如,一个键是频率,值是具有这些频率的单词列表的字典。这将使我们能够回答以下问题:哪些单词的频率正好是 5?哪些单词的频率是所有单词中的最小或最大?

就像字符串和列表一样,我们也可以使用循环来处理字典中的信息。for循环给我们字典的键,我们可以使用索引来获取每个键的关联值:

>>> for word in freq:                         #1
...     print('Word', word, 'has frequency', freq[word])    #2
...
Word DNA has frequency 11
Word acquire has frequency 11
Word Taxxon has frequency 13
Word morph has frequency 6

*#1 遍历 freq 字典中的每个键

#2 使用键(单词)和关联值(freq[word])* *### 5.1.4 #9. 文件

我们经常需要处理存在于文件中的数据集。例如,在第二章中,我们使用 NFL 统计数据文件来确定最有效的四分卫。使用文件对于其他数据科学任务也很常见。例如,如果你正在绘制关于全球地震的信息或确定两本书是否由同一作者撰写,你需要处理这些数据集,通常这些数据集会存储在文件中。

在第二章中,我们处理了一个名为 nfl_offensive_stats.csv 的文件。请确保这个文件在你的当前程序目录中,因为我们现在将使用这个文件来进一步理解第二章中使用的某些代码。

处理文件中的数据的第一步是使用 Python 的 open 函数打开文件:

>>> nfl_file = open('nfl_offensive_stats.csv')

你有时会看到 Copilot 在这里添加一个 r 作为第二个参数:

>>> nfl_file = open('nfl_offensive_stats.csv', 'r')

但我们不需要 rr 只意味着我们想要从文件中读取,但如果我们没有指定,这已经是默认的。

我们使用赋值语句将打开的文件分配给名为 nfl_file 的变量。现在,我们可以使用 nfl_file 来访问文件的内容。一个打开的文件是 Python 类型,就像数字和字符串以及到目前为止你看到的所有其他类型一样。因此,我们可以调用一些方法来与文件交互。其中一个方法是 readline,它以字符串的形式给出文件的下一行。我们现在将使用它来获取打开文件的第一行,但不用担心这一行本身,因为它非常长,包含大量我们最终不会使用的列信息:

>>> line = nfl_file.readline()     #1
>>> line
'game_id,player_id,position,player,team,pass_cmp,pass_att,pass_yds,pass_td,pass_int,pass_sacked,pass_sacked_yds,pass_long,pass_rating,rush_att,
rush_yds,rush_td,rush_long,targets,rec,rec_yds,rec_td,rec_long,
fumbles_lost,rush_scrambles,designed_rush_att,comb_pass_rush_play,
comb_pass_play,comb_rush_play,Team_abbrev,Opponent_abbrev,two_point_conv,
total_ret_td,offensive_fumble_recovery_td,pass_yds_bonus,rush_yds_bonus,
rec_yds_bonus,Total_DKP,Off_DKP,Total_FDP,Off_FDP,Total_SDP,Off_SDP,
pass_target_yds,pass_poor_throws,pass_blitzed,pass_hurried,
rush_yds_before_contact,rush_yac,rush_broken_tackles,rec_air_yds,rec_yac,
rec_drops,offense,off_pct,vis_team,home_team,vis_score,home_score,OT,Roof,
Surface,Temperature,Humidity,Wind_Speed,Vegas_Line,Vegas_Favorite,
Over_Under,game_date\n'

#1 从文件中读取一行

从这样的混乱字符串中提取单个值并不容易。因此,我们首先倾向于做的是将这样的行拆分成其单个列数据。我们可以使用字符串 split 方法来做这件事。该方法接受一个分隔符作为参数,并使用该分隔符将字符串拆分成一个列表:

>>> lst = line.split(',')    #1
>>> len(lst)
69

#1 使用逗号 (,) 作为分隔符拆分字符串

现在我们可以查看单个列名:

>>> lst[0]
'game_id'
>>> lst[1]
'player_id'
>>> lst[2]
'position '     #1
>>> lst[3]
'player'
>>> lst[7]
'pass_yds'

#1 单词末尾的空格在原始数据集中存在,但其他列标题没有空格。

我们正在查看的文件的第一行不是真实的数据行——它只是告诉我们每个列名的标题。下次我们调用 readline 时,我们得到第一行真实的数据:

>>> line = nfl_file.readline()
>>> lst = line.split(',')
>>> lst[3]
'Aaron Rodgers'
>>> lst[7]
'203'

逐行移动这种方式适合探索文件中的内容,但最终我们可能想要处理整个文件。要做到这一点,我们可以在文件上使用一个 for 循环。它会在每次迭代时给我们一行,我们可以以任何我们喜欢的方式处理它。一旦我们完成了一个文件,我们应该调用它的 close 方法:

>>> nfl_file.close()

关闭后,我们不再允许使用该文件。现在我们已经讨论了如何读取、处理和关闭文件,让我们看看一个完整的示例。在列表 5.10 中,我们提供了一个第二章程序的版本,该程序按总传球码数对四分卫进行排序。除了展示文件外,我们还使用了第四章和本章中看到的大多数 Python 功能,包括条件语句、字符串、列表、循环和字典。

列表 5.10 不使用 csv 模块的替代 NFL 统计代码
nfl_file = open('nfl_offensive_stats.csv')
passing_yards = {}                    #1

for line in nfl_file:                #2
    lst = line.split(',')
    if lst[2] == 'QB':               #3
        if lst[3] in passing_yards:                   #4
            passing_yards[lst[3]] += int(lst[7])      #5
        else:                                         #6
            passing_yards[lst[3]] = int(lst[7])       #7

nfl_file.close()

for player in sorted(passing_yards, 
                     key=passing_yards.get, 
                     reverse=True):         #8
    print(player, passing_yards[player])

#1 这个字典将四分卫的名字映射到他们的传球码数。

#2 遍历文件的每一行

#3 仅关注四分卫

#4 四分卫已经在我们的字典中了。

#5 将四分卫的总数增加;int 将类似’203’的字符串转换为整数。

#6 四分卫尚未在我们的字典中。

#7 设置初始四分卫的总数

#8 从最高到最低传球码数遍历四分卫

底部的这个循环for player in sorted(passing_yards, key=passing_yards.get, reverse=True):有很多内容。我们在注释中解释了这一行是按从高到低遍历四分卫。reverse=True使我们按从高到低排序,而不是默认的从低到高。key=passing_yards.get使排序集中在传球码数(而不是,例如,球员的名字)。如果您想进一步分解这一行代码,请随时向 Copilot 请求进一步解释。这突显了我们在这里试图保持的平衡:知道足够多的知识,能够理解代码的精髓,而不一定需要理解每一个细微之处。

这个程序运行得很好;如果你运行它,你会看到与从第二章运行代码相同的输出。不过,有时候,使用模块(我们将在下一节更深入地介绍模块)可以更容易地编写程序,这就是第二章的程序所做的事情。由于 CSV 文件非常常见,Python 自带了一个模块来简化处理它们。在第二章中,我们给出的解决方案使用了 csv 模块。因此,让我们讨论一下列表 5.10 中的代码(不使用模块)和第二章中的代码(以下列表中重新打印)之间的主要区别(我们给 Copilot 的提示没有显示)。

列表 5.11 使用 csv 模块的 NFL 统计数据代码
# import the csv module
import csv

# open the csv file
with open('nfl_offensive_stats.csv', 'r') as f:    #1
    # read the csv data
    data = list(csv.reader(f))    #2

# create a dictionary to hold the player name and passing yards
passing_yards = {}

# loop through the data
for row in data:                  #3
    # check if the player is a quarterback
    if row[2] == 'QB':
        # check if the player is already in the dictionary
        if row[3] in passing_yards:
            # add the passing yards to the existing value
            passing_yards[row[3]] += int(row[7])
        else:
            # add the player to the dictionary
            passing_yards[row[3]] = int(row[7])

for player in sorted(passing_yards, key=passing_yards.get, reverse=True):
    print(player, passing_yards[player])

#1 显示打开文件的另一种语法

#2 使用特殊的 csv 模块;读取文件中的所有数据

#3 遍历每行数据

首先,列表 5.11 使用 csv 模块使处理 CSV 文件变得更容易。csv 模块知道如何操作 CSV 文件,因此,例如,我们不必担心将行拆分成列。其次,列表 5.11 使用了with关键字,这意味着当程序完成对该文件的操作时,文件会自动关闭。第三,列表 5.11 在开始任何处理之前先读取整个文件。相比之下,在列表 5.10 中,我们读取并处理每行,一旦读取。

解决编程问题有多种方法

总是存在许多不同的程序可以用来解决同一个任务。有些可能比其他更容易阅读。代码最重要的标准是它能正确地完成工作。之后,我们最关心的是可读性和效率。所以,如果你发现自己难以理解某些代码的工作方式,花些时间查看 Copilot 的其他代码可能值得,以防那里有更简单或更易于理解的解决方案。

在计算任务中,文件被广泛使用,因为它们是常见的数据来源,需要被处理。这包括本节中的 CSV 文件,记录计算机或网站事件日志文件,以及存储你在视频游戏中可能看到的图形数据的文件等。由于文件被如此广泛地使用,因此并不奇怪有许多模块帮助我们读取各种文件格式。这引出了模块的更大主题。

5.1.5 #10. 模块

人们使用 Python 制作各种东西——游戏、网站、用于数据分析、自动化重复任务、控制机器人等应用程序。你可能想知道 Python 怎么可能让你创建如此多种类的程序。当然,Python 的创造者不可能预见到或创建所有需要的支持!

事实上,默认情况下,你的 Python 程序只能访问一些核心 Python 功能(例如我们在上一章和本章中向您展示的那些)。要获取更多功能,我们需要使用模块。而且,要使用一个模块,你需要导入它。

Python 中的模块

模块 是为特定目的设计的代码集合。回想一下,我们不需要知道一个函数是如何工作的就可以使用它。模块也是一样:我们不需要知道模块是如何工作的就可以使用它们,就像我们不需要知道电灯开关内部是如何工作的就可以使用它一样。作为模块的用户,我们只需要知道模块能帮助我们做什么以及如何编写代码来正确调用其函数。当然,Copilot 可以帮助我们编写这种代码。

当你安装 Python 时,一些模块会随 Python 一起安装,但我们需要导入它们。其他模块我们首先需要安装,然后才能导入。相信我们,如果你想在 Python 中完成特定的任务,可能已经有某人编写了一个模块来帮助你。

你可能想知道如何确定应该使用哪些 Python 模块。你怎么知道哪些模块存在呢?与 Copilot 或 Google 搜索进行简单对话通常很有帮助。例如,如果我们搜索“Python 模块创建 zip 文件”,第一个结果告诉我们所需的模块是 Python 标准库的一部分,这意味着它随 Python 一起提供。如果我们搜索“Python 模块用于可视化”,我们会了解到名为 matplotlib、plotly、seaborn 等模块。搜索这些模块应该会引导你到展示它们功能和典型用途的可视化画廊。大多数模块都可以免费下载和使用,尽管你的搜索结果可以帮助你确认模块是否免费以及其具体的使用许可。我们将在第九章中推迟安装和使用新安装的模块,但到那时,你会看到这个过程:寻找、安装和使用相关模块来帮助我们完成任务。

表 5.2 列出了一些常用的 Python 模块以及它们是否为内置模块。如果一个模块是内置的,你可以直接导入该模块并开始使用它;如果不是,你需要先安装它。

表 5.2 常用 Python 模块总结
模块 内置 描述
**#1 创建新的.zip 文件
csv 帮助读取、写入和分析 CSV 文件
zipfile 帮助创建和提取压缩的 zip 存档文件
matplotlib 用于绘图的图形库,作为其他图形库的基础,并提供高度的自定义化
plotly 一个用于创建网络交互式图表的图形库
seaborn 建立在 matplotlib 之上的图形库,可以比 matplotlib 更容易地创建高质量图表
pandas 一个专注于数据框的数据处理库,类似于电子表格
scikit-learn 包含机器学习的基本工具(即,帮助从数据中学习并做出预测)
numpy 提供高效的数据处理
pygame 一个游戏编程库,帮助在 Python 中构建交互式、图形化的游戏

在第二章中,我们的代码使用了 Python 自带的 csv 模块。让我们继续学习 Python 自带的其他模块。

当人们想要组织他们的文件,可能是在备份或上传之前,他们通常会首先将它们存档到一个.zip 文件中。然后他们可以传递这个单一的.zip 文件,而不是可能成百上千的单独文件。Python 自带了一个名为 zipfile 的模块,可以帮助你创建.zip 文件。

要尝试这个,在你的编程目录中创建一些文件,并让它们都以.csv 结尾。你可以从你的 nfl_offensive_stats.csv 文件开始,然后添加几个更多。例如,你可以添加一个名为 actors.csv 的文件,其中包含一些演员的名字和他们的年龄,如下所示

Actor Name, Age
Anne Hathaway, 40
Daniel Radcliffe, 33

你还可以添加一个名为 chores.csv 的文件,其中包含一项任务列表以及你是否完成了每一项:

Chore, Finished?
Clean dishes, Yes
Read Chapter 6, No

内容并不重要,只要你有一两个.csv 文件来测试即可。现在我们可以使用 zipfile 模块将它们全部添加到一个新的.zip 文件中!

>>> import zipfile
>>> zf = zipfile.ZipFile('my_stuff.zip', 'w',
    ↪ zipfile.ZIP_DEFLATED)     **#1
>>> zf.write('nfl_offensive_stats.csv')       #2
>>> zf.write('actors.csv')       #3
>>> zf.write('chores.csv')   #4
>>> zf.close()**

| django | 否 | 一个辅助设计网站和 Web 应用的 Web 开发库 |

#2 添加第一个文件

#3 添加第二个文件

#4 添加第三个文件** **如果你运行这段代码,你会找到一个名为 my_stuff.zip 的新文件,其中包含你的三个.csv 文件。直接使用.zip 文件在以前的其他编程语言中是一个非常专业、容易出错的任务,但 Python 并非如此。Python 自带了一些对数据科学、游戏制作、处理各种文件格式等有帮助的模块,但 Python 并不能提供一切。当我们需要更多的时候,我们会转向可下载的模块,正如我们在第九章中将要看到的。

在本章中,我们向您介绍了我们前 10 个 Python 特性的后半部分,总结如表 5.3 所示。在前一章和本章中,我们讨论了很多关于阅读代码的内容。尽管我们没有涵盖你可能会看到 Copilot 生成的一切,但你处于一个很好的位置来检查 Copilot 代码,以确定它是否尽力按照你请求的方式生成代码。我们还展示了更多使用 Copilot 解释工具的示例,以帮助你理解新代码。在接下来的章节中,我们将看到如何测试 Copilot 生成的代码,以确定其是否正确,以及当它不正确时你可以做什么。

表 5.3 本章 Python 代码特性总结
代码元素 示例 简要描述
循环 for 循环:for country in countries: print(country) while 循环:index = 0 while index < 4: print(index) index = index + 1 循环允许我们根据需要多次运行相同的代码。当我们知道迭代次数时(例如,字符串中的字符数)使用 for 循环,不知道时(例如,要求用户输入强密码)使用 while 循环。
缩进 for country in countries: print(country) 缩进告诉 Python 何时一段代码属于另一个代码块的一部分(例如,print 调用位于 for 循环内)。
字典 points = {'a': 1, 'b': 3} 字典允许我们将键与值关联起来。例如,键 'a' 与值 1 相关联。
文件 file = open('chores.csv') first_line = file.readline() 文件包含数据,存储在您的计算机上。Python 可以打开许多类型的文件并读取其内容,允许您处理文件中的数据。
模块 import csv 模块是已经存在的库,提供了额外的功能。常用的模块包括 csv、numpy、matplotlib、pandas 和 scikit-learn。一些模块包含在标准的 Python 发行版中;其他模块需要单独安装。

5.2 练习

  1. 回想一下我们在列表 5.3 中查看的 for 循环代码,用于打印列表中的动物。与章节中的原始示例相比,这段修改后的代码有何不同?具体来说,它产生了哪些额外的输出?
lst = ['cat', 'dog', 'bird', 'fish']

for index in range(len(lst)):
    print('Got', lst[index])
    if lst[index] == 'bird':
        print('Found the bird!')
    print('Hello,', lst[index])
    1. 考虑以下 while 循环代码,试图重复我们在列表 5.3 中使用 for 循环所做的操作。当我们运行代码时,我们会注意到它无限期地运行。你能识别并修复导致它无限期运行的错误吗?
lst = ['cat', 'dog', 'bird', 'fish']

index = 0
while index < len(lst):
    print('Got', lst[index])
    print('Hello,', lst[index])
    1. 将以下代码行排列成 while 循环,打印列表中的每个数字,直到遇到数字 7。注意缩进!
 index += 1
 while index < len(numbers) and numbers[index] != 7:
 index = 0
 numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
 print(numbers[index])
    1. 想一个现实场景,其中 while 循环比 for 循环更合适。描述这个场景,并解释为什么 while 循环是更好的选择。
    1. 修改get_strong_password函数(或它调用的is_strong_password函数)以提供有关输入密码不够强大的具体反馈。例如,如果密码没有大写字母,则打印“密码必须包含大写字母”,如果它不包含数字,则打印“密码必须至少包含一个数字”。
    1. 给定以下print_quarterbacks函数,你能将其重写为使用“with”语句来打开和关闭文件吗?为什么关闭文件很重要?
def print_quarterbacks():
    nfl_file = open('nfl_offensive_stats.csv')
    for line in nfl_file:
        lst = line.split(',')
        if lst[2] == 'QB':
            print(f"{lst[3]}: {lst[7]} passing yards")
    nfl_file.close()
    1. 在这个练习中,我们将进一步练习使用 zipfile 模块创建包含多个 CSV 文件的.zip 文件。按照以下步骤完成任务并回答问题:

    2. 首先,在你的当前目录中创建三个 CSV 文件:

      • nfl_offensive_stats.csv(你应该已经有了这个文件)

      • actors.csv 包含以下内容:

                      Actor Name, Age
                      Anne Hathaway, 40
                      Daniel Radcliffe, 33
        
        
      • chores.csv 包含以下内容:

                      Chore, Finished?
                      Clean dishes, Yes
                      Read Chapter 6, No
        
        
    3. 使用 Copilot(不要直接像我们在本章中那样输入代码),编写一个 Python 脚本,使用 zipfile 模块将这些三个 CSV 文件添加到名为 my_stuff.zip 的.zip 文件中。

    4. Copilot 建议的 zipfile 模块提供的其他一些功能有哪些?它们有什么用?

摘要

  • 循环用于重复执行代码,直到满足所需次数。

  • 当我们知道循环将执行多少次迭代时,我们使用for循环;当我们不知道循环将执行多少次迭代时,我们使用while循环。

  • Python 使用缩进来确定哪些代码行属于同一组。

  • 字典是从键(例如,一本书中的单词)到值(例如,它们的频率)的映射。

  • 在读取文件之前,我们需要先打开文件。

  • 一旦文件打开,我们可以使用方法(例如,readline)或循环来读取其行。

  • 一些模块,如 csv 和 zipfile,与 Python 一起提供,可以通过导入它们来使用。

  • 其他模块,如 matplotlib,需要先安装,然后才能导入和使用。***************

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐