威斯康星-CS220-数据编程笔记-全-

威斯康星 CS220 数据编程笔记(全)

001:Windows系统Python安装教程 🐍

在本教程中,我们将学习如何在Windows操作系统上安装Python编程语言,以及本课程所需的核心扩展包。我们将一步步完成从下载Python到验证安装的整个过程。


下载Python安装程序

首先,我们需要访问Python官方网站下载安装程序。以下是具体步骤。

  1. 打开浏览器,访问 python.org
  2. 将鼠标悬停在导航栏的“Downloads”选项上。
  3. 网站通常会为Windows用户自动推荐最新版本的Python安装程序。点击该下载按钮。

下载过程可能需要一些时间,请耐心等待。


运行Python安装向导

上一节我们下载了Python安装程序,本节中我们来看看如何运行安装向导并进行关键设置。

下载完成后,在文件资源管理器中找到下载的安装文件(通常名为 python-3.x.x.exe),双击运行。

在打开的安装向导界面中,请务必进行以下两项关键操作:

  • 勾选“Add Python 3.x to PATH”:这会将Python添加到系统环境变量中,方便在命令行中直接调用。
  • 勾选“Install launcher for all users”:如果计算机有多个用户账户,此选项能让所有用户都能使用Python。

完成设置后,点击“Install Now”开始安装。安装过程可能需要几分钟。


安装课程所需的扩展包

Python安装完成后,我们需要安装本课程将要用到的几个重要扩展包。我们将使用Python自带的包管理工具 pip 来完成这个任务。

首先,我们需要打开Windows PowerShell。点击开始菜单,输入“PowerShell”,然后选择打开。

在PowerShell窗口中,我们可以通过输入 pip 命令来验证Python和pip是否安装成功。如果看到一系列pip的使用说明,则表明安装正确。

接下来,我们将使用pip一次性安装四个核心包。请在PowerShell中输入以下命令:

pip install jupyter numpy requests matplotlib

请注意,jupyter 的拼写是 J-P-Y-T-E-R,与木星(Jupiter)的拼写不同。命令中无需逗号,直接列出所有包名即可。

执行此命令后,pip会开始下载并安装这些包及其依赖项。首次安装可能需要较长时间(例如8分钟或更久),请等待其完成。


验证安装并启动Jupyter Notebook

所有包安装完毕后,我们可以通过启动Jupyter Notebook来验证整个环境是否配置成功。

在PowerShell中,输入以下命令:

jupyter notebook

此命令会启动Jupyter Notebook服务,并自动在您的默认网页浏览器中打开其界面。您会看到一个文件浏览器界面。

需要说明的是,Jupyter Notebook此时运行在您的本地计算机上(地址为 localhost),并非真正的互联网。它只是借助浏览器的功能来提供一个交互式的编程环境。

要创建一个新的Python编程文件,您可以点击页面右上角的“New”按钮,然后选择“Python 3”即可。


总结

本节课中,我们一起学习了在Windows系统上搭建Python数据编程环境的完整流程。我们首先从官网下载Python安装程序,接着在安装时务必勾选“Add Python to PATH”选项。然后,我们使用 pip install 命令安装了 jupyternumpyrequestsmatplotlib 这四个核心扩展包。最后,我们通过启动Jupyter Notebook成功验证了所有安装。如果您在任何一个步骤遇到问题,请及时寻求帮助。

002:课程概述与硬件

在本节课中,我们将要学习CS220《数据编程1》的课程概述,并初步了解计算机硬件的基本组成部分。我们将探讨数据在现代世界中的重要性,以及如何利用计算能力从数据中学习。课程将使用Python语言,并强调实践与项目的重要性。


课程介绍与背景

大家好,欢迎来到威斯康星大学麦迪逊分校的CS220《数据编程1》课程。我是Mike Dosher,本课程的联合讲师之一。

近年来,本课程的学生人数急剧增长,这反映出数据技能在当今职业生涯中的重要性日益凸显。我们生活在一个数据爆炸的时代,无论是新闻、医学、物理学还是工程学,数据都无处不在。本课程的核心目标是教会大家如何从数据集中提取知识,而答案就在于计算

过去,“计算”曾是一个职业头衔,指进行数学计算的人。如今,我们依靠机器进行计算。我们的目标是学会利用强大的计算能力来解决实际问题。

计算机的优势在于速度更快、更可靠。正如谷歌联合创始人拉里·佩奇所说:“找到世界上的杠杆,这样你就可以更懒。” 这句话的深层含义是,我们应该让计算机自动化处理它能完成的工作,从而将时间投入到更有价值的创造性任务中。

要实现自动化,我们需要告诉计算机该做什么。这就好比成为“双语者”:既要精通自己专业领域(如生物学、新闻学)的语言,也要掌握计算机编程的语言。数据科学目前是一个极佳的就业方向,在2019年曾被评为美国最佳职业。

与传统的以Java或C++为主、侧重理论的计算机科学导论课不同,本课程将使用更易学习的Python语言,并紧密结合来自各学科的真实数据问题。课程将重度依赖数据,几乎所有的项目都会涉及数据集的处理与分析。


讲师介绍

本节我们来认识一下教学团队。

我是Mike Dosher。我的背景比较多元:本科学习生物和化学,辅修数学;在南卡罗来纳大学获得了物理化学博士学位;之后在华盛顿的海军研究实验室工作了三年;随后在堪萨斯的本尼迪克特学院教授了八年化学。出于对计算机的好奇心,我来到威斯康星大学进修,并获得了计算机科学硕士学位,之后在麦迪逊的一家初创公司担任软件工程师。我已经在威斯康星大学任教三年。我的爱好包括障碍赛跑和搏击运动。

我的联合讲师是Meena Kumar。她去年刚获得计算机科学博士学位,拥有在思科、微软等公司的行业经验,并在本校任教三年。她的研究兴趣是互联网测量。她也喜欢跑步。

我们还要感谢Tyler,他是本课程的前任讲师,开发了我们正在使用的许多课件和工具。他对于优化工具效率很有热情,例如使用更符合人体工学的DVORAK键盘布局,这体现了本课程的一个理念:熟练使用工具至关重要。


关于学生

了解课堂的构成对我们很重要。因此,我们设置了一份调查问卷。请尽快完成这份调查,这对于等待名单上的同学尤其重要。

重要提示:填写问卷需要使用你的学校邮箱(Wisc ID)登录谷歌账户。如果遇到权限问题,请尝试在浏览器的无痕模式下打开谷歌并重新登录。

根据以往学期的调查数据,大约40%的同学是第一次学习计算机科学课程,约50%的同学此前编写的代码不超过10行。本课程正是为这些初学者设计的。历史成绩表明,即使是零编程经验的同学,只要努力学习,超过90%的人都能获得B-或更好的成绩,其中约50%的人能获得A-或A。这证明,在本课程中取得好成绩是完全可能的。

当然,如果你已有大量编程经验(例如编写过上千行代码),这门课对你来说可能过于简单。欢迎你通过邮件与我们讨论这是否是最适合你的课程。


课程结构概述

上一节我们介绍了课程背景和学生情况,本节中我们来看看课程的整体结构。

本课程大致分为三个部分,每部分约持续五周:

  1. 控制流:这部分的核心问题是“计算机现在在做什么?接下来要做什么?”。我们将学习代码的执行顺序,包括使用条件语句(if/else)进行分支选择,以及使用循环(for/while)进行重复操作。我们还会学习如何编写和调用函数来组织代码。
  2. 状态:这部分关注计算机如何管理数据。我们将学习如何使用Python内置的列表字典等数据结构来组织数据,如何对数据进行排序,以及如何从文件中读取数据或将数据保存到文件。
  3. 数据科学专题:在掌握了编程基础后,我们将聚焦于具体的数据科学主题。这包括处理表格数据(如CSV文件)、从互联网获取并分析数据、使用数据库高效查询数据,以及利用绘图库将数据可视化,以便更直观地发现规律。

前两部分是任何编程入门课都会涉及的内容,但本课程会减少理论深度,增加对数据的侧重。最后一部分则完全围绕数据科学实践展开。


教学风格与支持

我的授课风格倾向于引导式学习。视频中会经常出现“暂停视频,尝试解决这个问题”的环节,然后我再演示解决方法。课程包含概念讲解、示例演示、大量练习,以及“现场编程”——我会在大家面前实时编写代码,过程中可能会犯错,但这正是学习调试和理解思路的好机会。

课程支持包括阅读材料、发布的幻灯片PDF、以及实时问答环节。请务必利用这些资源。

学习编程就像爬梯子,每一级知识都建立在下一级之上。如果中间缺失了任何一块(就像瑞士奶酪上的洞),爬到顶端就会非常困难。因此,如果你对某个主题不理解,请务必及时提问,不要掉队。不要因为课程规模大或觉得讲师忙碌而不敢提问。

我们有一个庞大的支持团队:15名助教、14名本科生朋辈导师,加上我和Meena,共31人支持近800名学生。我们会提供“实验时间”,这不是强制性的,但你可以在此连接到Blackboard Collaborate会话,向在场的助教或朋辈导师咨询策略性问题或寻找项目伙伴。


课程资料与沟通

课程阅读材料主要来自三个来源,均为免费资源:

  1. 《Think Python》第二版(针对Python 3)。
  2. 《Automate the Boring Stuff with Python》。
  3. 我们自己编写的课程笔记,主要涵盖数据科学部分。

有效的沟通对于大规模课程至关重要。我们将使用以下工具:

  1. Piazza(在线论坛):这是提问的主要场所。提问前请先搜索是否已有相同问题。请尽量将问题设为公开(可匿名),以便他人受益。严禁在Piazza上发布超过5行与作业相关的代码,这将被视为作弊。
  2. 电子邮件:你会收到课程邮件列表的通知。如有课程内容或作业问题,请先联系助教。他们的邮箱格式是 cs220-help@cs.wisc.edu,并在主题中加上 +TA姓名。如果是课程安排等行政问题,可以直接联系讲师。
  3. 反馈与表扬:我们设有匿名反馈表单,用于收集改进建议。另有一个表扬表单,如果你觉得某位助教或朋辈导师提供了特别大的帮助,可以在此告知我们。
  4. Canvas:主要用于查看成绩、参加每周测验和三次考试。

项目提交和评分有专门的工具。你提交项目后,助教会运行测试并检查代码风格,可能会因“硬编码答案”(即直接输出结果而非通过计算得出)等问题扣分。你可以在提交前运行我们提供的测试程序来预测分数。


评分与考核

本节我们来详细了解课程的评分方式。

成绩构成如下:

  • 编程项目:共13个,占总成绩约50%。P1较简单,其余权重相同。我们会提供评分测试,因此在提交前你就能知道大致得分。
  • 每周测验:共12次,去掉最低的2次,每次占2%。目的是帮助你检查学习效果。
  • 考试:共3次,各占10%。计划在24小时窗口内开放,你有2小时完成答题。
  • 课堂参与:占1%,包括完成调查、遵守指引等。
  • 额外学分:学期末有机会获得,用于帮助处于成绩边缘的同学。

评分标准保证:至少95分获得A,至少85分获得B。这个标准看似严格,但因为项目提供了测试且允许修改后重新提交,大部分同学应该在项目部分取得接近满分的成绩。因此,考试成为区分对知识掌握程度的关键。

关于项目合作:允许两人一组完成。最佳合作模式是两人同时在线协作,共同讨论和编写代码。应避免“乒乓模式”(一人做完另一人接着做)或一人包揽所有工作。每组只需提交一份代码文件。

关于学术诚信:我们严肃对待作弊行为。允许与项目伙伴共享代码,也允许讨论思路、绘制伪代码、分享非项目特定的在线资源(但需引用)。严禁复制/粘贴他人代码、逐行抄袭、或拍摄代码照片。我们会使用名为Moss的自动化工具检测代码相似性。首次作弊的处罚通常是该作业零分并上报学校,多次作弊可能导致开除。


计算机硬件基础

在开始编程之前,了解计算机的基本硬件组成很有帮助。我们可以将硬件分为几个核心部分。

输入/输出设备

计算机通过输入设备接收信息,通过输出设备发送信息。

  • 输入设备示例:键盘、鼠标、摄像头、麦克风、网络接口。
  • 输出设备示例:显示器、扬声器、网络接口。

核心内部组件

以下是计算机机箱内的主要部件:

  1. 中央处理器:CPU是计算机的“大脑”,负责执行所有计算和逻辑运算。它并不负责存储数据。现代CPU速度极快,以吉赫兹(GHz)计量,且通常包含多个核心,可以同时处理多个任务。CPU运行时会产生大量热量,因此需要散热器或风扇冷却。

    • 公式示例运算速度 ≈ CPU时钟频率 × 核心数量
  2. 内存:通常指RAM。它是CPU的工作区,用于临时存储正在运行的程序和数据。访问速度远快于硬盘,但断电后数据会丢失(易失性)。内存容量通常以吉字节(GB)计量。

    • 代码示例:思考一个程序运行时,其指令和数据就加载在RAM中。
  3. 存储器:即硬盘驱动器(HDD)或固态硬盘(SSD)。用于永久存储操作系统、程序、文档和数据集。即使断电,数据也不会丢失(非易失性)。SSD速度更快但更贵,HDD容量更大更便宜。

    • 示例:你的Python程序文件和课程数据集就保存在硬盘上。
  4. 主板:一块大型电路板,所有其他组件(CPU、内存、硬盘、网络卡等)都连接在上面,它负责组件间的通信。

  5. 网络:网络接口卡(有线或无线)允许计算机通过互联网与其他计算机通信。典型的交互模式是客户端-服务器模型:你的电脑(客户端)向远程服务器(如YouTube)发送请求或接收数据。


课程网站导览与下一步

我们的课程网站是获取所有信息的中枢。最重要的部分是课程日程表,上面会按周列出所有讲座视频链接、幻灯片PDF、实验指导和项目说明。

请务必阅读教学大纲,其中详细说明了评分政策、合作规则、学术诚信规定和迟交政策等。

资源页面,你可以找到调查问卷链接、助教匹配工具、旧考题等。

下一步行动

  1. 完成“Who are you”调查问卷。
  2. 仔细阅读课程大纲。
  3. 在你的电脑上安装设置Python环境。
  4. 开始着手项目P1,它将于下周三截止。

本节课中我们一起学习了《数据编程1》课程的总体框架、教学目标、评分方式以及计算机硬件的基础知识。我们了解到,本课程旨在通过Python编程解决实际数据问题,并配备了强大的支持系统。接下来,我们将从Python编程基础开始,一步步构建数据处理的技能。祝你学习顺利!

003:第2讲 - 终端与控制流程 🖥️

在本节课中,我们将学习如何使用终端(命令行界面)与计算机交互,并初步了解程序控制流程的基本概念。我们将从终端的历史和基本操作开始,逐步深入到文件系统导航、常用命令,最后通过一个工作表练习来理解控制流程的核心思想。

课程公告与帮助渠道 📢

在开始之前,有两个重要的公告。

首先,感谢大家关于视频长度的反馈。本学期的目标是让每节课的平均时长保持在50分钟左右。有些课程可能会更长,有些会更短。如果在录制过程中需要增加额外的示例,可能会略微超时。如果这对您造成困扰,请提供更多反馈。

其次,关于如何获得帮助和参加答疑时间。您可以通过课程网站上的链接找到所有联系方式。实验时间(Lab hours)是开放的集体答疑时间。要预约一对一的答疑,请使用“答疑时间请求工具”。提交请求后,您可以在仪表板上查看排队情况。当轮到您时,答疑人员会将您从等候列表移至“进行中”,并通过Canvas的Blackboard Collaborate将您拉入一个分组讨论室。

终端简介与历史 🕰️

对于许多人来说,使用终端可能是一种与计算机交互的新方式。我们花一些时间确保大家熟悉它。

回顾历史,在计算机刚发明时,它们非常昂贵。大多数人没有自己的计算机。一个系(如计算机科学系)可能会购买一台价值数百万美元的大型机。为了充分利用这台计算机,他们会购买这些基于文本的“哑终端”连接到强大的主机。这些终端基本上只是一个屏幕和一个键盘。用户在终端输入,指令被发送到主机进行处理,结果再发送回屏幕显示。

如今,我们拥有更强大、更快速的计算机。我们仍然可以通过终端模拟器来使用类似终端的特性。问题是,为什么我们还想使用终端或终端模拟器?原因在于,很多时候使用键盘输入比使用鼠标操作更快。例如,在文件系统中导航时,如果我知道文件的确切位置,通过键盘输入路径名通常比用鼠标点击菜单和图标更快。

这里有两个职业建议:

  1. 评估工具价值:问问自己,学习使用终端是否是一个好的投资。虽然现在可能不熟悉,使用文件资源管理器或访达(Finder)点击操作更快,但如果投入时间熟练掌握,使用终端是否会显著提高效率?对于许多数据编程和计算机编程工具来说,熟练掌握后能极大提升编码速度,这通常是值得的投资。
  2. 寻找随处可用的工具:例如,如果发生全球性疫情,你被困在家中,你仍然需要能够完成工作。使用终端模拟器可以让你连接到大学的计算机,从任何地方进行工作。例如,我现在坐在我的地下室,三周后我可能会在拉斯维加斯,你们甚至不会知道我离开了家,正在酒店里工作。

终端、Shell与基本命令 ⚙️

当我们在计算机上启动终端模拟器(Windows上我使用PowerShell,Mac上使用名为“终端”的应用)时,其内部运行着另一个称为Shell的程序。Shell是非常棒的程序,它基本上只问一个问题:“我应该做什么?”,你给出一个命令,它执行,然后再问“我应该做什么?”,如此循环。它主要做两件事:让我在计算机的文件和文件夹中导航,以及运行程序。

关于术语:我会经常使用“目录”这个词。虽然我作为Windows用户习惯说“文件夹”,并且它们看起来像小文件夹,但我们将要输入的所有命令的缩写(如cd代表更改目录)都使用“目录”这个词。所以为了保持一致,我们将使用“目录”。

实际上,Shell有很多选择。Windows用户可以使用PowerShell或CMD。Mac和Linux用户通常使用相同的命令集。PowerShell是Windows版本,它适配并使用了这些命令,因此对Mac和Linux用户更友好。在本课程中,我只讨论在PowerShell、Mac和Linux上通用的命令。

接下来,谈谈从Shell运行程序。这很简单。打开终端模拟器,Shell已经在那里了。从提示符(例如 C:\Users\Mike>$)开始,你只需要输入命令。例如,输入 ls(代表“列表”),它将列出当前目录中的所有程序和目录。然后,它会给你另一个提示符,询问“下一步做什么?”,如此循环。

文件系统与路径 📁

导航是指寻找我们需要的文件或目录。使用文件资源管理器或访达,我们可以在Windows或Mac上做到这一点。但在Shell中,我们将使用命令进行导航。我们会输入一个命令,该命令运行一个程序来执行某些操作,例如更改目录或创建新目录。

让我们看看文件实际存储在哪里。我们希望文件永久保存,不会在计算机关闭时丢失,因此我们将文件写入硬盘。在Windows系统上,每个驱动器都被分配一个字母,例如C盘、D盘、E盘等。

每个文件都有自己的文件名和与之关联的路径。以 C:\readme.txt 为例,readme.txt 是文件名。包含路径和文件名的完整名称称为路径名。C: 是驱动器盘符。.txt 是扩展名,它告诉我们文件的类型。一个关键区别是:Mac和Linux使用正斜杠 / 作为分隔符,而Windows使用反斜杠 \。这个差异可能导致代码在你的计算机上运行良好,但在我们评分时完全失败。我们将在学期后半部分处理文件和操作系统时重点强调这一点,目前暂时不会成为问题。

在第一讲中,我们简要讨论了输入和输出。键盘和鼠标是输入设备,扬声器和显示器是输出设备。文件也可以是输入或输出。我们可能有一个程序生成一些输出,并将其保存到文件系统中。之后再次运行程序时,我们可能希望读取这个文件,将其作为输入放回程序中。

计算机上的目录可以包含文件或其他目录。和文件一样,目录也有路径名。例如,我有三个文件:file1.docfile2.docfile3.docC:\mydir 目录中。file1.doc 的完整路径是 C:\mydir\file1.doc。目录的路径是 C:\mydir。如果我在 mydir 内添加另一个目录 subdir,并且 subdir 中也有一个 file1.doc,那么由于它们在不同的目录中,我重用了文件名,但它们是不同的文件。一个的路径是 C:\mydir\file1.doc,另一个是 C:\mydir\subdir\file1.doc

绝对路径与相对路径 🧭

我们有两种不同的路径来引用文件:相对路径和绝对路径。上面例子中的路径都是绝对路径,它们确切地告诉我们如何到达那里。

让我用一个更简单的例子。如果我问:“计算机科学大楼在哪里?” 我可以给出两个答案:

  1. 威斯康星州麦迪逊市西代顿街1210号,邮编53706。这给出了你需要知道的确切信息。
  2. 在约翰逊街对面。

答案一几乎总是有效的。你可以把它输入手机,查看到底在哪里。答案二只有当我们站在计算机科学大楼对面时才是一个好答案,然后我可以告诉你“就在街对面”。这通常更方便,对在场的人更有意义。

路径名也是如此。我们有绝对路径名和相对路径名。绝对路径总是可行的。相对路径只有在我们已经掌握一些信息(即我们知道自己在哪)时才真正好用。我们所在的位置称为工作目录,是我们当前的位置。

让我们看一些例子。假设中间部分是我们的工作目录。这是我在文件资源管理器或访达中正在查看的目录。在这里,我有一个文件的绝对路径,我想做的是根据绝对路径和工作目录找出相对路径。

在第一个例子中,如果我在C盘根目录(C:\),我要找的路径是 C:\test.txt。文件已经在C盘上,我只需要输入 test.txt。这很简单。
第二个例子类似,如果我在 C:\dirX\dirY\dirZ 目录中,我的文件在 C:\dirX\dirY\dirZ\my.doc,我只需要输入 my.doc
但是,如果我在目录树中向上移动一级,即我在 C:\dirX\dirY,而我的文件在 C:\dirX\dirY\dirZ\my.doc,我需要输入 dirZ\my.doc 才能到达那个位置。
对目录也可以做同样的事情。如果我想进入目录 C:\dirX\dirY\dirZ,而我当前在 C:\dirX,我可以输入 dirY\dirZ 作为相对路径。

特殊目录与更多示例 🔤

这里还有几个小东西,特殊的目录名。实际上有三个,我这里有两个。.. 表示向上移动一级目录。例如,如果我在 C:\dirX\dirY,执行 .. 会把我上移到 C:\dirX. 表示当前目录。

这里还有几个例子。如果我要找 test.txt,我也可以将其称为 .\test.txt。在很多情况下,加上这个前缀并非必要,但我会指出一些需要它的例子。我也可以做 .\dirY\dirZ。所以,这只是在之前的例子前面加上了 .\,对目录也有效。

然后,让我快速谈谈这个例子。如果我在目录 C:\dirX\dirY\dirZ 中,我在目录树中深入了三层。我想获取这个绝对路径 C:\dirX\my.doc。这意味着我需要向上移动。.. 基本上会移除这里的 dirZ,我将上移一级到 dirY。我需要再做一次,所以再加一个 ..,那将把我从 dirY 移出到 dirX。所以它应该看起来像 ..\..\my.doc

再看一个例子。在这种情况下,我的工作目录是 C:\A。我真正想获取的是 C:\B\file.txt。我们需要做的是:首先使用 .. 上移到C盘根目录。从那里,我需要使用 B 进入B目录。然后,file.txt 让我找到我关心的文件。

Windows与Mac路径差异 💻🍎

我想简要讨论一下Windows和Macintosh计算机在驱动器方面的几点差异。在Windows上,所有绝对路径都以驱动器名开头,如 C:\D:\,这告诉我们数据存储在哪个驱动器上。然而,在Mac上,分隔符始终是正斜杠 /,并且绝对路径总是以那个正斜杠开头。例如:/Users/Tyler/myfile.doc。它从不告诉我们这个文件实际存储在哪个驱动器上。

那么,如果我们有多个驱动器,而每个文件路径都以相同的符号开头,我们如何区分不同的驱动器呢?仅仅通过查看绝对路径是无法分辨的。基本上,所有不同的驱动器看起来就像是不同的目录。事实上,在Windows系统上,如果我在 C:\Users\Tylerfile.txt,在Mac上,它只是 /Users/Tyler/file.txt,没有驱动器字母。C:\Program Files 在Mac上可能相当于 /usr/local/bin。D盘在Windows上可能是 D:\Backup,在Mac上可能是 /Volumes/Backup。E盘也可能是 /Volumes/Movies。这些驱动器被设计成看起来一样。这样想:假设我的硬盘装满了电影,然后我有更多电影,所以我买了另一个硬盘。在Mac上,我不希望看起来有很多驱动器,路径不会告诉我我在哪个驱动器上,它们都只是看起来像目录。这样我就可以移动我的电影,把它们移到我刚买的新硬盘上,但就我查找或导航到该目录的方式而言,它们感觉仍然在同一个地方。

基本命令演示(基于Mac)⌨️

我想在幻灯片中讲解几个简单的例子。当你们打开幻灯片并浏览时,我们有一些示例。本幻灯片中我要讨论的例子都是基于Mac电脑的。然后我将用Windows机器进行演示,并强调那里的一些差异。我将只关注在PowerShell、Mac和Linux上都能工作的命令。

第一个要讨论的命令是 pwd。这会告诉我我在哪里。它代表“打印工作目录”。当我运行它时,它只会打印出我所在的确切位置。注意我的提示符:~/TRH/scratch $,这个提示符已经包含了目录名。这很常见。PowerShell默认这样做。CSL机器上的bash提示符则没有。所以 pwd 可能很有价值。我个人不喜欢在提示符中显示目录,它占用大量空间,如果你在机器中嵌入得很深,很难看到你在输入什么。有方法可以改变这一点,但本课程中我不会深入讨论。

接下来,如果我想做出改变。现在我在 scratch 目录,cd .. 将向上移动一级。现在我的当前路径是 ~/TRH。从这里,如果我想清屏,屏幕有点满了,我可以使用 clear 命令。现在我回到了漂亮、干净的黑屏,只有一个提示符。从那里,我可以使用 cd(更改目录的缩写)移回目录名。我只需要输入我想进入的目录的名称。这个目录恰好是我刚才所在的位置。所以 cd scratch,现在我又回到了起始位置。

cd / 命令将把我带到根目录,即最顶层的级别。ls 将列出当前目录的内容。所以当我输入 ls 时,它是一个程序,会打印出根目录(本例中最顶层)的所有内容。

另一个我们会发现非常有用的命令是 cat 命令。cat 接受一个参数。readme.txt 在这种情况下是文件名。它所做的就是打印出该文件的内容。所以如果我执行,这个文件显示“hello”,这就是 readme.txt 的内容。

参数。像这里的 cat 这样的命令需要一个参数。这是程序的输入。cat 是程序名。它需要知道你想让我读取哪个文件。这被称为参数。

echo 是另一个命令。让我演示一下。如果我输入 echo,它接受一个参数。这里,我给它单词“hello”,它所做的就是打印出“hello”。它只是把这一行上的任何内容打印出来给我们,这并不超级有用。但事实证明,它是创建新文件的好方法。右尖括号 > 是重定向操作符,它会将输出发送到文件。所以如果我输入 echo hello > output.txt,它将创建这个文件,并将单词“hello”存储在该文件中。注意,在第一种情况下,它打印在屏幕上。而这里它没有打印在屏幕上,只是给了我一个提示符,因为它实际上将输出重定向到了 output.txt,而不是打印在屏幕上。然后当我读取 output.txt 时,cat 是我用来读取文件的命令。所以 cat output.txt,我们就看到了“hello”。

PowerShell演示与高效技巧 🚀

接下来,我想打开PowerShell,为你们演示其中的一些内容。如果你想在Mac上跟着操作,要打开终端,你需要进入访达,然后应用程序,然后实用工具,找到终端。Mina的演示(第2讲)在今天的视频中实际上是分开的。如果你有Mac,想跟着Mac版本操作,我建议你跳过去看她的视频,然后再回来。欢迎你留下来看我的演示,你可能也会学到一些东西。

现在,我要退出这个,打开PowerShell。我只需点击Windows开始按钮,输入“power”,就找到了Windows PowerShell。就是这个,最简单的那个。点击它,打开PowerShell。我可以看到它告诉我我在PowerShell中。这里的“PS”代表PowerShell,它给了我提示符。那个尖括号是提示符的结尾。PowerShell的这个特定提示符告诉我的工作目录是什么。实际上,获取工作目录的命令是 pwd。今天我们可能只关心大约8个命令。pwd 是告诉我当前路径的命令。这是我的当前工作目录,然后它在这里给了我另一个提示符。

要获取帮助,命令是 man。让我们获取一些关于 pwd 的帮助。man 是“manual”(手册)的缩写。我可以在后面输入任何命令的名称,它会提供一些帮助。如果我阅读这个,有些可能非常复杂,但它们通常具有相同的结构。它告诉我这个命令有一些别名,gl 代表 Get-Location。我实际上可以在这台机器上输入 Get-Location,它也会做和 pwd 相同的事情。让我们现在就做一下。Get-Location。很好,所以做同样事情的多个命令被称为别名。

接下来,我们再做一个。cd 是我们讨论过的另一个命令。它也被称为 Set-Location。我们还有其他一些别名。它告诉我们可选的参数。我只想指出,这里的 man 是我现在正在运行的一个程序。它所做的就是显示信息。这里有“more”。我可以按回车键继续并完成程序。如果我返回再做一次,我也可以按 Ctrl+C 来终止当前程序。当我们编写有无限循环的程序时,会发现这非常有用。

我想花点时间谈谈效率。刚才我描述了如何使用键盘导航文件结构比点击更高效。看看这个。我的主目录里有很多东西。如果我想进入我的教学材料,我需要做的就是输入。看看我需要输入多少个字母:T,然后按 Tab 键。所以制表符补全会让我基本上完成这个词,我不需要输入所有那些字母。我进入了 teaching,然后我这里有所有这些课程。让我们看看CS 368 C++。在这里,我也有很多东西。所以 cd DCS,按 Tab,它会给我第一个匹配的选项。再按一次 Tab 会向下移动,给我第二个选项。看看还有什么,cd Homework。我有 homeworkmark1。这就是最深的了。这是我不喜欢PowerShell显示所有文件的方式的原因之一,因为当我处理我的课程时,我有所有这些目录,一切都整理得非常整齐。否则,我会丢失跟踪东西。但它占用了超过一半的屏幕。我稍后再解决这个问题。

接下来,我提到了有三个特殊命令。cd .. 将我向上移动一级。cd . 是当前目录。cd ~ 是第三个,它会把我带回我的主目录。我的主目录在 C:\Users\Mike。所以 cd ~ 会把我带回那里。无论我在哪里,我都可以一直向上,查看C盘,cd ~ 会把我带回那里。

另一种我们可以高效使用终端的方式是使用上箭头键。如果我输入过一个命令,我可以使用上箭头键返回并查看我已经使用过的命令。所以这里我可以做 cd teaching,我可以做 Get-Location。这只是另一种节省输入的方式。我有很多学生在答疑时间来找我,我看着他们输入东西,他们本可以使用制表符补全,或者他们正在输入刚刚输入过的相同命令,特别是当他们试图编译一些C代码或需要正确设置许多小选项的长命令时。上箭头键节省了大量时间。所以这些都是小技巧。学习你使用的软件的键盘快捷键,无论是Microsoft Word还是高端编程软件,学习如何使用它。

接下来是 history 命令。这会给我最近输入的所有内容的列表。然后,一旦我有了这个,我可以使用 Ctrl+R 进行搜索。这将从列表底部(比如编号30)开始,向列表更高的位置搜索。我需要做的就是开始输入一些东西。例如,输入“g”会找到“Get-Location”,看起来我做了很多次。删除它,试试“ma”。最近包含“ma”的是第12行的“man cd”,我可以直接按回车,它会再次运行该命令。按 Ctrl+C 终止它。这就是历史记录,或者叫反向搜索。

运行其他程序与路径设置 🌐

接下来,让我们退出这里。如何运行其他我们可能熟悉的程序?比如Notepad,我经常使用。如果我输入“notepad”,它会打开。我可以从这里读取我的文件。关闭它。Chrome呢?我经常使用我的网页浏览器。但当我这样做时,我知道Chrome在电脑上,但它不被识别。原来,它不在PowerShell寻找程序的地方之一,它不在正确的路径上。所以我现在想带你们了解如何将Chrome添加到系统路径中,这样你就可以直接从终端输入“chrome”,它就会工作。这仅适用于Windows的PowerShell。Mac版本不同。如果你想在Mac电脑上添加这个,去看Mina的视频,她的第二个视频。

想法是这样的:我想将Chrome添加到我的路径中,这样我就可以从终端输入“chrome”并启动它。首先,我要去找Chrome。它就在这里。点击Windows按钮,输入“chrome”。右键点击它,第二个选项是“打开文件位置”。点击它。实际上有两个步骤。第一步可能有点误导性,我实际上看到的是开始菜单程序,这些只是开始菜单中的所有东西,这实际上不是程序本身,而是程序的快捷方式。但这很好,因为快捷方式知道它位于哪里。所以我要点击那个快捷方式,右键点击,然后点击“属性”。其中一项是“目标”。复制这个目标,它实际上很长。现在我们应该能够进入那个目录。回到PowerShell,看看这个。我想 cd 进入那个目录。右键点击,因为在PowerShell中,右键点击既是复制也是粘贴,非常方便。它已经在引号里了,但我不想要带 chrome.execd,我只想 cd 进入一个目录。所以删除一些内容。现在,这应该会把我带到正确的目录。它就在那里,第三个,chrome。很好。我输入“chrome”,仍然报错,不被识别。部分问题是,由于目录的组织方式,我需要将路径添加到这个文件。它没有被识别。

想法是这样的:回到Windows,搜索“path”,这是我需要编辑的地方。“编辑系统环境变量”中的“Path”。点击它。这是一个环境变量“Path”。点击它。然后我可以看到我有用户(Mike)的本地变量和所有人的系统变量。我就在这里为我编辑它。点击“Path”,点击“编辑”。然后我看到这里所有不同的路径。当我输入像“chrome”或“notepad”这样的命令时,它会搜索所有这些目录,看看那个程序是否在那里。我只需滚动到底部,添加一个新路径,粘贴Chrome的路径。我不想要 chrome.exe,只想要那个目录。现在它知道在那个目录中查找。然后我需要启动一个新的PowerShell实例。它就在我的热键栏上。现在我应该可以输入“chrome”了。它打开了另一个窗口,太棒了。让我试试这种方式。看看最后一个Chrome实例是否在这里,它会在另一个显示器上弹出。相信我,它完全正常。等一下,再试一次。再试一次。好的,我有几个Chrome实例打开,它们都在另一个显示器上,所以它试图在另一个显示器上打开它们。但看看这个:输入“chrome”回车,它完全正常。我也可以输入我想访问的网站名称。例如,输入“chrome www.example.com”。让我们试试那个。一只小狗,很可爱。刚才有一瞬间,我的心跳有点加速,想着如果出现什么不好的东西怎么办?不能放在YouTube上。

文件与目录操作演示 📄

接下来,我想做更多关于更改目录的操作,包括创建一个新目录,然后使用 echo 命令创建一些文件,复制、移动它们,并用 cat 读取。这将结束这部分。让我快速演示一下,然后移到桌面。我会缩小一点,这样你们就能看到发生了什么。

我刚刚导航到桌面。如果我输入 echo 并输入一个单词,它只会打印到控制台,到屏幕上。如果我使用尖括号 >,它会让我写入一个文件,那应该出现在我的桌面上。我们看看它去哪了。是的,在左下角,很小,hi.txt。我也可以创建一个目录,使用 mkdir。就叫它 mydir。它告诉我刚刚创建了目录,它就在顶部。然后,让我们复制。用 cp 复制 hi.txt,我们把它移到 mydir 中。然后我需要给它一个名字,就叫 hello.txt。所以 cp 接受两个参数:第一个是我想复制的文件,第二个是我想复制到的目标位置。看看,hi 还在这里。如果我打开 mydir,我看到它在那里:hello.txt。点击这个,Notepad会打开它,它确实写着“hello”。我应该在创建它的时候在这里打开 hi.txt,那也写着“hello”。看看这个,如果我输入 echo world > hi.txt。然后回到这里,看看。它不再说“hello”,只说“world”。我基本上破坏了那个文件的内容。让我们做一个稍微不同的版本,双尖括号 >> 会追加到文件末尾,不会直接覆盖旧文件重新开始。所以现在如果我这样做,它说“world peace”。再看看还有什么。cat 是读取文件的命令,所以我可以输入 cat hi.txt,它会打印出该文件的内容。看起来我的屏幕快满了,清屏。我还没讲 move。如果我想移动一个文件,那会让我把它从一个地方移到另一个地方,或者它可以让我重命名东西。让我演示一下。如果我有 hi.txt 作为源文件,我想把它重命名为“Christmas list.txt”。你们圣诞节想要什么?然后它改变了下面的名字,我可以看到。如果我双击那个,我可以打开,仍然写着“world peace”。现在我可以看到,文件名改变了。

控制流程入门:顺序执行 🔄

我觉得这个视频有点长了,但我想在最后找点乐子。首先,我要创建一个文件。echo win > aecho lose > b。我非常仔细地注意。我要把b移到c。mv b c。我要把a移到b。mv a b。我要把c移到a。mv c a。然后我要 cat a。暂停视频,告诉我会发生什么。猜一下,写下来,确认,然后回来。

我带着答案回来了。我要按回车。我们看看输出什么。“lose”。所以发生了这样的事:我们需要非常仔细地跟踪这个顺序,才能弄清楚发生了什么。当“win”存储在a,“lose”存储在b时,然后b被移到c。所以b没了,我们重命名了它,现在是c。我有两个文件:a和c。a是那个写着“win”的文件。然后我们把a移到b。a没了,b被创建了。我现在有两个文件:b和c。b是那个写着“win”的文件。然后我把c移到a。当我使用 move 时,c会消失,没了。我有了a。我现在有两个文件:b和a。a是那个写着“lose”的文件,b是那个写着“win”的文件。我希望这讲得通,希望这有点意思。

我要去拿配合最后这部分的工作表,马上回来。

控制流程工作表练习 📝

最后这部分,我将讲解工作表上的一些问题。所以去网页,点击工作表,打开它,也许甚至打印出来或拿些草稿纸,这样你可以和我一起完成。

这个工作表有很多问题,我们只看其中四个。这里的想法是,我们将假装自己是计算机,按照这些指示逐步执行。我们将学习控制流程,这是本课程前五周的内容。数据是第二个五周,然后是数据编程的专题,是课程的最后三分之一。所以控制流程是关于计算机现在在做什么,下一步做什么,再下一步做什么。这就是我们要做的,逐步执行这些指令。

我希望你们先做这个。看看这个问题,逐步执行代码,暂停视频。我马上回来,自己过一遍,然后我们可以讨论。现在暂停。

欢迎回来。希望你们有机会做了。当我逐步执行时,首先,我的X框在那里。第一行说“给X框加1”,然后这个框应该看起来像划掉的10,变成11,因为我用PowerPoint做这个。划掉容易做吗?不,我要删除并写上11。抱歉,我没有完全按照指示做,因为我没有用笔和纸。希望这仍然讲得通。接下来,我要给X框加2。它说11,我加2,得到13。最后,我要将X框中的值加倍,所以13乘以2是26。最终答案:26。现在,这里的问题是:如果我的朋友得到了不同的答案,24。他是个捣蛋鬼,不喜欢按正确顺序做事。问题是,要确保每个人都得到相同的答案,有什么好规则?如果教室里的每个人都带着笔记本电脑,给他们相同的代码,他们应该都得到相同的答案。

这里的想法是,这是控制流程的第一部分,即我们将按顺序逐步执行。一次一步,并且每一步只执行一次。所以我们做第一步,然后做第二步,然后做第三步。我们不会打乱顺序,不会多做,也不会跳过任何步骤。所以这是控制流程的第一条规则:顺序执行。

条件语句(If语句) 🤔

我忘了我为这个做了幻灯片。顺序执行,第一条规则。让我调出第二个例子。接下来,我们做问题4。伙计们,我在这里为我们设置好了例子,暂停视频,花点时间,逐步执行这些步骤,然后在你得出答案后,回来按播放。

欢迎回来。让我逐步执行这个。第一步:如果x中的值是负数(它确实是负数),继续到第2步。否则,跳到第3步。它是负数,所以继续。将x中的值乘以-1,并将结果放回x。所以到这里,让我抓住正确的位置。我需要乘以-1,这使它变成正4,很好。然后我将继续,因为我的控制流程第一条规则(顺序执行)说,我要做第3步。第3步说:将x的值复制到ABS框中。所以我现在那里是0,它需要变成4。这个函数实际上是做什么的?它是在计算一个数的绝对值。如果x开始时是正4,就像现在这样,正4,让我们再走一遍。如果x中的值是负数,继续到第2步。它不是负数。否则,跳到第3步。所以我们现在跳下去做第3步。我们直接将x的值复制到ABS框中。它已经在那里了。

所以这里的想法是:控制流程的第二条规则是一种我们可以用来跳过某些步骤的技术。在这种情况下,这被称为条件语句。这里的关键词是“if”。所以这些将被称为if语句或条件语句。这是我们控制流程的第二部分。

迭代(循环) 🔁

接下来看下一个例子。做问题6。所以暂停视频,自己逐步执行,然后回来查看答案。

我逐步执行一下。所以把1放入total框。让我点击这里。我要放一个1进去。如果n等于1,它不等于,它是4。如果n等于1,跳过,否则继续到第3步。所以我要做第3步:将total中的值乘以n中的值,将结果放回total。所以total是1,n是4,把它们相乘,把4放回total。然后我想将n中的值减1。所以在这里,它从4变成3。然后我要转到第2步。回到第2步。如果n等于1,它不是,是3。所以我不跳到第6步。否则,我继续到第3步:将total中的值乘以n中的值,将结果放回total。所以是3乘以4,等于12。接下来,下一条规则说我们要顺序执行。所以我们到第4步:将n中的值减1。所以3变成2,然后到第2步。第2步说:如果n等于1,跳到第6步,它不是,否则到第3步。第3步:将total中的值乘以n中的值,将结果放回total。所以2乘以12等于24。然后我返回并将n中的值减1。到第2步。如果n等于1,哈,它等于,跳到第6步。我甚至不需要读剩下的部分,我只需要到第6步:将total中的值复制到answer框中。很好,一切看起来都很好。看起来这段代码实际上是在计算阶乘。所以4的阶乘是24,即4×3×2×1。

我们在这里看到的是一个迭代的例子。当我们逐步执行代码时,我们做了第2、3、4、5步,然后第2、3、4、5步,然后第2、3、4、5步,然后第2、6步。所以我们反复做了那些步骤,在一个循环中。所以控制流程的下一个部分是迭代或循环的概念,即我们将反复执行一系列步骤。再次强调,控制流程是逐步执行代码现在在做什么。如果它在一个循环中,它将多次执行那系列步骤。

函数调用 📞

最后,再做一个来结束。看看这个例子,自己先做,然后马上回来。

欢迎回来。我只做这部分。这个视频已经很长了,但让我至少从这里开始。主代码:把2放入moves框。现在我们将执行move code下的步骤。然后继续到第3步。所以我们跳到这里到move code。如果moves是0,它不是。停止执行move code中的这些步骤,并回到你上次在主代码中的位置以完成更多步骤。它不是0,所以我们继续步骤B:将机器人沿箭头所指方向向前移动一格。这是机器人,就是那个有箭头的。等一下,让我做这个。机器人移动了。接下来,第3步说:将moves中的值减1。然后我们回到步骤A。这看起来像迭代。我们刚讨论过。所以如果moves是0,在此之前停止。它不是0。所以B:将机器人沿箭头所指方向向前移动一格。所以我们拿出机器人,向上移动。然后我们将moves中的值减1。回到步骤A。如果moves是0,停止执行并回到我们来的地方。那是在上面的第2行:执行move code下的步骤,然后继续到第3步。接下来,将机器人向右旋转90度。让我看看能不能轻松做到。看起来不错。然后我们把3放入moves框。现在,当我们执行时,它说执行move code下的步骤,然后继续到第6步。所以我下去,move code。如果moves是0,它不是。将机器人沿箭头所指方向向前移动一格。将moves减1,所以它会变成2。我希望你们能看到,它会再向前移动一步,然后再向前移动一步,之后moves会从2变成1,再变成0。我们快进一下。把机器人开到那里。它最终停在心形上。很好。

所以这里的想法是:控制流程的下一部分被称为函数。我在这里所做的是,我从主代码中提取出一系列步骤。我本可以很容易地在第2步这里直接复制粘贴所有这四行,然后在第5步它说“转到move code”的地方,我本可以把这四行放在那里。但那样的话,同样的代码就会出现两次。如果我需要修改,就必须做两处更改才能让它工作。所以这里的想法是,我将跳出顺序执行,去做一些事情,然后回来。这让我能够提取出那些需要反复执行的相同代码块。这就是函数调用的概念。

最后总结一下,让我点击这个,填入我们将要讨论的所有四个控制流程部分,以及如何在接下来的四周半内在Python中实现它们。

总结 🎯

本节课中,我们一起学习了终端的基本使用和控制流程的初步概念。

首先,我们了解了终端的历史和重要性,认识到在特定场景下使用命令行可以提高效率。我们学习了如何启动终端或PowerShell,以及Shell的基本工作原理。

其次,我们深入探讨了文件系统和路径。理解了绝对路径和相对路径的区别,以及如何在命令行中使用 cdlspwd 等命令进行导航。我们还特别指出了Windows和Mac在路径分隔符上的关键差异。

接着,通过演示,我们实践了创建目录 (mkdir)、创建文件 (echo >)、复制文件 (cp)、移动文件 (mv)、读取文件 (cat) 等常用命令,并学习了制表符补全、命令历史等提高效率的技巧。

最后,我们通过工作表练习引入了程序控制流程的核心思想:

  1. 顺序执行:代码按顺序一步一步执行。
  2. 条件语句(If语句):根据条件决定执行哪些代码块。
  3. 迭代(循环):重复执行一段代码多次。
  4. 函数调用:将一段代码封装起来,以便在需要时重复使用,使代码更模块化和可维护。

这些控制流程的概念是编程的基础,我们将在接下来的课程中在Python语言中深入学习和应用它们。请完成工作表的其余问题进行练习,如果有任何疑问,欢迎在实时问答或答疑时间提出。祝大家学习愉快!

004:Windows环境下的项目一操作指南 🖥️

在本教程中,我们将学习如何在Windows系统上完成CS220课程的第一个项目。我们将涵盖从环境设置、文件下载、代码编写到最终提交的完整流程。

环境设置与文件准备

上一节我们介绍了课程背景,本节中我们来看看如何为项目一准备环境。

首先,你需要确保已经按照课程要求正确安装了Python。接下来,我们将下载项目文件并整理到指定文件夹中。

以下是创建项目文件夹的步骤:

  1. 打开“文件资源管理器”。
  2. 导航到“此电脑” -> “文档”。
  3. 创建一个名为 CS220 的新文件夹。
  4. 进入 CS220 文件夹,再创建一个名为 P1 的子文件夹。

现在,我们需要从课程网站下载测试脚本 test.py。该脚本用于自动测试你的作业代码。

以下是下载 test.py 的正确方法:

  1. 在课程网页上找到 test.py 文件的链接。
  2. 将鼠标悬停在链接上,左键单击进入文件页面。
  3. 在文件页面,找到并右键单击 Raw 按钮。
  4. 在弹出的菜单中选择“链接另存为...”。
  5. 将文件保存到之前创建的 P1 文件夹中。

启动PowerShell并导航到项目目录

上一节我们准备好了项目文件,本节中我们来看看如何打开命令行工具并定位到工作目录。

有几种方法可以在正确的目录中打开PowerShell。

以下是三种打开PowerShell并导航到 P1 文件夹的方法:

  • 方法一(推荐):在文件资源管理器中打开 P1 文件夹,按住 Shift 键的同时在空白处右键单击,然后选择“在此处打开 PowerShell 窗口”。
  • 方法二:通过Windows开始菜单搜索并打开PowerShell,然后使用 cd 命令手动导航到目标目录。例如:
    cd Documents\CS220\P1
    
  • 方法三:在文件资源管理器的地址栏左键单击以选中 P1 文件夹的完整路径,按 Ctrl+C 复制。然后打开一个新的PowerShell窗口,输入 cd 和一个空格,接着右键单击以粘贴路径,最后按回车。

启动Jupyter Notebook并创建代码文件

上一节我们进入了正确的工作目录,本节中我们来看看如何启动Jupyter Notebook并开始编写代码。

在PowerShell中,输入以下命令启动Jupyter Notebook:

jupyter notebook

执行此命令后,PowerShell窗口会开始运行Jupyter服务,并自动在你的默认浏览器中打开一个本地网页(地址通常是 http://localhost:8888)。请勿关闭这个PowerShell窗口,否则Jupyter服务会停止。

在Jupyter的网页界面中,你可以看到 P1 文件夹里的文件。要创建一个新的Python笔记本,请点击右上角的“New”按钮,然后选择“Python 3”。

创建新笔记本后,第一件事是将其重命名为 main.ipynb,因为测试脚本 test.py 会寻找这个特定名称的文件。你可以在网页顶部的“Untitled”处点击进行重命名。

编写、运行与测试代码

上一节我们创建了代码文件,本节中我们来看看如何根据题目要求编写代码并进行测试。

根据项目说明,第一个问题(Q1)要求你复制一段代码并运行。在Jupyter笔记本的第一个单元格中,粘贴提供的代码(以 # Q1 开头的部分),然后按 Shift + Enter 执行该单元格。你应该会看到输出“Hello world”。

核心概念:测试脚本通过搜索特定的标记(如 # Q1)来定位你的答案,并根据对应单元格的输出或变量值进行评分。

完成代码编写后,务必及时保存。你可以点击菜单栏的“File” -> “Save and Checkpoint”,或使用快捷键 Ctrl + S

要测试你的代码是否正确,需要打开另一个新的PowerShell窗口(不要关闭运行Jupyter的那个),并导航到 P1 目录。然后运行测试脚本:

python test.py

运行后,脚本会输出测试结果,显示你通过了哪些题目。例如,如果只完成了Q1,可能会显示50%的分数。

接着,按照项目说明完成第二个问题(Q2)。在Jupyter中新建一个单元格,粘贴Q2的代码并运行。再次保存文件Ctrl + S),然后回到PowerShell重新运行 python test.py。如果一切正确,现在应该显示100%通过。

添加个人信息并提交项目

上一节我们完成了所有编码任务,本节中我们来看看如何添加个人信息并最终提交项目。

在提交之前,需要在Jupyter笔记本的最顶部添加一个单元格,用于填写项目信息和提交者姓名。根据项目说明粘贴提供的模板代码,并确保将 netid1netid2 替换为你和搭档的实际信息(如果没有搭档,netid2 应设为 None)。

重要提示:如果与搭档合作,只需由其中一人提交一次即可。两人都提交会导致自动查重系统将你们的作业标记为高度相似,引发不必要的审查。

添加信息后,保存文件并再次运行 test.py 以确保没有引入错误。

最后,登录课程的项目提交网站。选择“Project 1”,点击“选择文件”,找到并选中你的 main.ipynb 文件,然后点击提交。提交成功后,页面会显示确认信息。你可以在网站上查看自己的提交记录和后续的评分结果。

故障排除与小贴士

上一节我们完成了项目提交,本节中我们来看看一些常见问题的解决方法。

如果在运行 jupyter notebook 命令时遇到问题,可以尝试使用以下替代命令启动:

python -m notebook

请熟记几个提高效率的Jupyter快捷键:

  • Shift + Enter:运行当前单元格。
  • Ctrl + S:快速保存。
  • A:在当前单元格上方插入新单元格。
  • B:在当前单元格下方插入新单元格。

总结

本节课中我们一起学习了在Windows系统上完成CS220项目一的完整流程。我们涵盖了从创建项目文件夹、下载测试脚本、使用PowerShell导航、启动和操作Jupyter Notebook、编写并测试Python代码,到最终添加个人信息并提交项目的所有关键步骤。记住保持工作目录有序、及时保存文件、正确填写提交信息,并确保最终提交成功,是顺利完成项目的关键。

005:Python导论

在本节课中,我们将开始实际编写Python代码。我们将学习如何运行Python代码、使用Jupyter Notebook,并了解Python中的基本运算符、数据类型和布尔逻辑。

运行Python代码的三种方式

上一节我们介绍了课程概述,本节中我们来看看如何运行Python代码。Python代码可以通过三种主要方式来运行。

交互模式

交互模式是最简单的方式,它允许你逐行输入并立即执行代码,类似于一个计算器。

以下是启动交互模式的步骤:

  1. 打开命令行(如PowerShell或终端)。
  2. 输入 pythonpython3 并回车。
  3. 你将看到Python提示符 >>>,在此处可以直接输入代码。

例如:

>>> 2 + 2
4

要退出交互模式,在Windows上按 Ctrl+Z 然后回车,在Mac上按 Ctrl+D

脚本模式

脚本模式用于运行存储在 .py 文件中的完整程序。

以下是使用脚本模式的步骤:

  1. 使用文本编辑器(如IDLE)创建一个新文件。
  2. 编写Python代码并保存为 .py 文件(例如 main.py)。
  3. 在命令行中导航到文件所在目录,运行 python main.py
  4. 或者在IDLE中,直接按 F5 运行当前脚本。

在脚本模式中,需要使用 print() 函数来输出结果。例如,文件 main.py 中的代码:

print(1 + 2)
print(3 + 4)

运行后将输出:

3
7

笔记本模式(Jupyter Notebook)

Jupyter Notebook将代码分成独立的“单元格”,便于分步执行和展示结果。

以下是使用Jupyter Notebook的步骤:

  1. 在命令行中输入 jupyter notebook 并回车。
  2. 浏览器会自动打开Jupyter界面。
  3. 点击“New” -> “Python 3”创建一个新的笔记本。
  4. 在单元格中输入代码,按 Shift+Enter 运行该单元格。

在笔记本中,最后一个表达式的值会自动显示在“Out[]”区域。例如,在一个单元格中输入:

1 + 2
3 + 4

运行后,Out[] 区域将只显示 7。若要显示多个结果,需使用 print() 函数。

Python中的基本运算符

了解了如何运行代码后,本节我们来看看Python中用于计算和比较的基本运算符。

算术运算符

算术运算符用于执行数学计算。

以下是常见的算术运算符:

  • +:加法,如 2 + 3 结果为 5
  • -:减法,如 5 - 2 结果为 3
  • *:乘法,如 3 * 4 结果为 12。注意不能使用 x· 表示乘法。
  • /:除法,返回浮点数(带小数点的数),如 4 / 2 结果为 2.03 / 2 结果为 1.5
  • //:整数除法,返回商的整数部分(向下取整),如 11 // 4 结果为 2
  • %:取模(求余数),如 11 % 4 结果为 3
  • **:幂运算(乘方),如 2 ** 3 结果为 84 ** 0.5 结果为 2.0(即平方根)。

注意:Python中不能除以零,尝试 1 / 0 会引发 ZeroDivisionError

比较运算符

比较运算符用于比较两个值,并返回布尔值(TrueFalse)。

以下是常见的比较运算符:

  • ==:等于,检查两个值是否相等。
  • !=:不等于,检查两个值是否不相等。
  • <:小于。
  • <=:小于或等于。
  • >:大于。
  • >=:大于或等于。

例如:

5 > 3      # 结果为 True
2 == 1+1   # 结果为 True
3 != 2     # 结果为 True

运算符优先级

当表达式中包含多个运算符时,Python会按照特定的优先级顺序进行计算,这与数学中的运算顺序类似。

以下是Python运算符的优先级顺序(从高到低):

  1. 括号 ():优先计算括号内的表达式。
  2. 幂运算 **
  3. 正负号 +x, -x(一元运算符)。
  4. 乘法、除法、取模、整数除法 *, /, %, //(从左到右计算)。
  5. 加法、减法 +, -(从左到右计算)。
  6. 比较运算符 ==, !=, <, <=, >, >=
  7. 逻辑非 not
  8. 逻辑与 and
  9. 逻辑或 or

例如,表达式 3 * 3 + 2 * 2 + (1 / 2) + 16 ** 0.5 的计算顺序如下:

  1. 先计算括号内 1 / 2,得 0.5
  2. 计算幂运算 16 ** 0.5,得 4.0
  3. 计算乘法 3 * 32 * 2,得 94
  4. 从左到右进行加法:9 + 41313 + 0.513.513.5 + 4.017.5

Python中的数据类型

掌握了运算符的使用,本节我们来认识Python中几种基本的数据类型。

整数和浮点数

Python可以处理不同类型的数字。

以下是两种主要的数字类型:

  • 整数:不带小数点的数,类型为 int。例如:1, -5, 1000
  • 浮点数:带小数点的数,类型为 float。例如:1.0, 3.14, -2.5

使用 type() 函数可以查看任何值的类型:

type(5)    # <class 'int'>
type(5.0)  # <class 'float'>

整数除法 // 和取模 % 在数据处理中非常有用,例如从数字中提取特定位数:

987 % 10      # 提取个位数,结果为 7
987 // 10     # 去掉个位数,结果为 98
(987 // 10) % 10  # 提取十位数,结果为 8

字符串

字符串用于表示文本数据,用单引号 ' 或双引号 " 括起来。

以下是字符串的基本操作和注意事项:

  • 创建字符串‘Hello’“World”
  • 引号嵌套:如果字符串内部包含引号,需交替使用或使用转义字符 \
    print("She said, \"Hello.\"")  # 输出:She said, "Hello."
    print('It\'s a nice day.')     # 输出:It's a nice day.
    
  • 转义字符\n 表示换行。
    print("Line 1\nLine 2")
    # 输出:
    # Line 1
    # Line 2
    
  • 字符串运算
    • + 用于拼接字符串:‘3’ + ‘4’ 结果为 ‘34’
    • * 用于重复字符串:‘Ha ‘ * 3 结果为 ‘Ha Ha Ha ‘
  • 字符串比较:基于字符的编码(如ASCII)按字典顺序进行比较。
    ‘a’ < ‘b’   # True
    ‘A’ < ‘a’   # True (大写字母编码小于小写字母)
    

布尔逻辑

最后,我们来学习布尔逻辑,它用于处理真值判断,是程序做出决策的基础。

布尔值与逻辑运算符

布尔类型只有两个值:TrueFalse(注意首字母大写)。逻辑运算符用于组合或修改布尔值。

以下是三个基本的逻辑运算符:

  • not:逻辑“非”。取反操作。
    • not TrueFalse
    • not FalseTrue
  • and:逻辑“与”。仅当两边都为 True 时结果为 True
    • True and TrueTrue
    • True and FalseFalse
    • False and TrueFalse
    • False and FalseFalse
  • or:逻辑“或”。只要有一边为 True,结果就为 True
    • True or TrueTrue
    • True or FalseTrue
    • False or TrueTrue
    • False or FalseFalse

逻辑运算符的优先级为:not > and > or。可以使用括号 () 来明确运算顺序。

True and False or True   # 先计算 and,等价于 False or True,结果为 True
not True or False        # 先计算 not,等价于 False or False,结果为 False
(True or False) and False # 先计算括号,等价于 True and False,结果为 False

总结

本节课中我们一起学习了Python编程的入门知识。我们了解了运行Python代码的三种模式:交互模式、脚本模式和Jupyter Notebook模式。我们学习了Python的基本算术运算符、比较运算符以及它们的优先级规则。我们还认识了三种重要的数据类型:整数、浮点数和字符串,并了解了如何对它们进行基本操作。最后,我们探讨了布尔逻辑,包括布尔值以及 notandor 三个逻辑运算符的使用。掌握这些基础知识是后续编写更复杂程序的关键。建议你打开Python环境,亲自尝试本节提到的所有示例,并通过完成配套练习来巩固理解。

006:变量与表达式 🐍

在本节课中,我们将学习Python编程中的核心概念:变量与表达式。我们将探讨如何存储数据、进行计算以及理解代码执行过程中的不同错误类型。


运算符优先级回顾 🔢

上一节我们介绍了不同的运算符。本节中,我们来看看当多个运算符出现在同一个表达式中时,Python如何决定计算的先后顺序,即运算符的优先级。

以下是Python中主要运算符的优先级列表,从高到低排列:

  1. 指数运算**
  2. 一元正负号+(正号), -(负号)
  3. 乘法、除法、取整除法、取模*///%
  4. 加法、减法+-
  5. 比较运算符==!=<<=>>=
  6. 布尔运算符
    • not
    • and
    • or

让我们通过一个例子来理解优先级如何工作。考虑表达式 10 - -2 // 3

  • 首先计算一元负号:-2
  • 然后进行取整除法:-2 // 3 结果为 -1(因为 -2/3 向下取整为 -1)。
  • 最后进行减法:10 - (-1) 结果为 11


布尔运算与短路求值 ⚡

在布尔运算中,Python使用一种称为“短路求值”的优化策略。这意味着在某些情况下,Python无需计算整个表达式就能得出结果。

对于 or 运算符:

  • 如果左侧为 True,则整个表达式必定为 True,Python会跳过右侧的计算。
  • 如果左侧为 False,则必须计算右侧才能确定结果。

对于 and 运算符:

  • 如果左侧为 False,则整个表达式必定为 False,Python会跳过右侧的计算。
  • 如果左侧为 True,则必须计算右侧才能确定结果。

这种特性非常有用,例如可以避免除零错误:

# 左侧为True,右侧的 1/0 永远不会被执行,因此不会引发错误
result = (1 + 1 == 2) or (1 / 0)

但需要注意,短路求值有时会导致意想不到的结果。例如,3 + 4 == 6 or 7 这个表达式,由于 3+4==6False,Python会继续计算右侧的 7,并将 7(而非 True)作为整个表达式的结果。正确的写法应该是 3 + 4 == 6 or 3 + 4 == 7


表达式、字面量与变量 📝

表达式是Python可以简化为一个值(数字、文本或布尔值)的代码片段。例如 5 + 5x > y

字面量是代码中直接写出的固定值,例如数字 5、字符串 "hello" 或布尔值 True

变量是内存中一个命名的位置,用于存储数据。与字面量不同,变量的值可以改变。例如,x = 5 将数字 5 存储在名为 x 的变量中。


变量赋值与命名规则 🏷️

我们使用单等号 = 进行变量赋值。可以将其理解为“得到”。Python会先计算等号右边的表达式,然后将结果存储在左边的变量名中。

以下是变量命名的基本规则(为确保兼容性,建议遵循):

  • 只能使用字母(A-Z, a-z)、数字(0-9)和下划线(_)。
  • 不能以数字开头。
  • 避免使用Python的关键字(如 andornotTrueFalse)或内置函数名(如 printlenint)作为变量名。

以下是命名示例:

  • 好的命名cs220Capitalized_private_var
  • 坏的命名220class(以数字开头), and(关键字), pi.3.14(包含点号), my-var(包含连字符)

编程实践与错误类型 🐛

在编程时,逐步编写并频繁测试代码是一个好习惯。这有助于及早发现错误。

Python中的错误主要分为三类:

  1. 语法错误:代码不符合Python语法规则,程序无法运行。例如 5 = x
  2. 运行时错误:代码语法正确,但在执行时出现问题。例如 1 / 0(除零错误)。
  3. 语义错误:代码能正常运行,但产生了错误的逻辑结果。例如计算正方形面积时使用了 side * 2 而不是 side ** 2。这类错误最难调试。

实战示例:计算奇数之和 ➕

假设我们有四个变量 abcd,分别存储了数字。我们的目标是只对其中的奇数求和。

我们可以利用取模运算符 %:一个数除以2的余数为1则是奇数,为0则是偶数。技巧是将每个数乘以它自身对2取模的结果。这样,偶数会变成0,奇数则保持不变,然后再将所有结果相加。

a = a * (a % 2)
b = b * (b % 2)
c = c * (c % 2)
d = d * (d % 2)
sum_odd = a + b + c + d

实战示例:复利计算 💰

计算一笔资金在固定年利率下,经过若干年复利增长后的总额。公式为:
最终金额 = 本金 * (1 + 利率) ** 年数

在代码中,我们使用变量来代表公式中的各个部分,使代码更灵活:

principal = 1000  # 本金
interest_rate = 7 # 利率百分比
years = 30        # 年数

multiplier = 1 + interest_rate / 100
final_amount = principal * (multiplier ** years)

实战示例:生成对齐的条形图 📊

我们希望根据两位玩家的得分生成一个视觉化的条形图,并且让玩家名字和冒号对齐。

步骤如下:

  1. 定义玩家姓名和得分变量。
  2. 在姓名后添加冒号。
  3. 计算每个玩家姓名(含冒号)的长度。
  4. 假设一个最大名字长度(例如7个字符)。
  5. 为每个玩家的名字字符串添加足够数量的空格,使其长度达到最大长度。
  6. 将得分数字转换为相应数量的符号(如 |)并打印。
player1 = “Alice”
player1_score = 10
player2 = “Bob”
player2_score = 8

player1 += “:”  # 添加冒号
player2 += “:”

max_name_length = 7
# 为名字添加空格以对齐
player1 += “ ” * (max_name_length - len(player1))
player2 += “ ” * (max_name_length - len(player2))

print(player1 + “|” * player1_score)
print(player2 + “|” * player2_score)

实战示例:验证数字范围 ✅

检查一个数字是否在0到100之间(不包含0和100),并输出验证结果。

我们使用一个布尔表达式来检查条件,并将结果存储在变量中:

x = 50
valid = (x > 0) and (x < 100)
print(“You may continue: ” + str(valid))

这里 (x > 0) and (x < 100) 构成了完整的范围检查条件。


本节课中我们一起学习了Python中变量与表达式的核心概念。我们回顾了运算符优先级,理解了短路求值,学会了如何命名和使用变量进行赋值,并通过多个实战示例巩固了这些知识。我们还了解了编程中常见的三种错误类型。掌握这些基础是迈向有效数据编程的关键一步。

007:函数

在本节课中,我们将要学习Python中函数的基本概念和使用方法。函数是编程中用于封装代码、执行特定任务的重要工具。我们将从调用现有函数开始,理解参数传递和返回值,并通过一个简单的“战舰”游戏项目来实践所学知识。

调用函数

上一节我们介绍了课程概述,本节中我们来看看如何调用函数。调用函数意味着告诉Python执行一段预先定义好的代码。

要调用一个函数,你需要知道它的名称,并在名称后加上一对括号 ()。括号内可以放置传递给函数的数据,这些数据称为参数

以下是调用函数的基本语法:

函数名(参数1, 参数2, ...)

例如,我们常用的 print 函数:

print("Hello, World!")

这行代码调用了 print 函数,并将字符串 "Hello, World!" 作为参数传递给它,函数执行后会在屏幕上输出这段文字。

另一个例子是 type 函数,它返回一个值(即数据的类型),我们可以将这个返回值存储在一个变量中:

x = 3.14
t = type(x)
print(t)  # 输出:<class 'float'>

在这个例子中,type(x) 是一个函数调用x 是传递给它的参数,函数执行后返回x 的数据类型,我们将其存储在变量 t 中。

参数与返回值

上一节我们学习了如何调用函数,本节中我们来深入了解函数的两个核心概念:参数和返回值。

参数是调用函数时传递给它的信息。函数内部使用参数(在函数定义时指定的变量名)来接收这些外部传入的值。
返回值是函数执行完成后,返回给调用者的结果。

考虑一个数学函数 f(x) = -x³ + 1。在编程中,我们可以这样调用它(假设它已被定义):

y = f(2)  # 调用函数f,传入参数2
print(y)  # 输出:-7

这里,2 是调用函数 f 时传递的参数。函数内部进行计算 -(2³) + 1 = -7,然后将结果 -7 返回,我们将其赋值给变量 y

函数也可以没有参数或不返回任何值。例如,print 函数的主要作用是输出信息,它通常不返回有意义的值(在Python中,它返回 None)。

使用现有函数

Python内置了许多有用的函数,我们一直在使用它们,比如 printtypeinput

input 函数用于从用户那里获取输入。它会暂停程序,等待用户在终端输入一些内容并按回车键,然后将输入的内容作为字符串返回。

name = input("请输入你的名字:")
print("你好," + name)

运行这段代码时,程序会显示“请输入你的名字:”,等待你输入。你输入“小明”并回车后,程序会输出“你好,小明”。

需要注意的是,input 函数总是返回字符串。即使你输入的是数字,它也会被当作字符串处理。如果需要进行数学计算,需要使用 int()float() 函数进行类型转换,这个过程称为类型转换强制转换

num_str = input("请输入一个数字:")  # 用户输入"5"
num_int = int(num_str)             # 将字符串"5"转换为整数5
result = num_int * 2
print(result)                      # 输出:10

函数的更多特性:关键字参数

print 函数有一些高级用法,它接受关键字参数来改变其默认行为。

默认情况下,print 用空格分隔多个参数,并在输出末尾添加换行符。但我们可以通过 sepend 参数来自定义这些行为。

sep 参数指定了分隔多个输出项的字符串:

print(1, 2, 3, sep='-')  # 输出:1-2-3
print(1, 2, 3, sep=', ') # 输出:1, 2, 3

end 参数指定了在输出末尾添加的字符串,默认是换行符 \n

print("Hello", end=' ')
print("World!")          # 输出:Hello World!(在同一行)

在第一个 print 中,我们将 end 设置为空格,所以它输出“Hello”后没有换行,而是加了一个空格。接着第二个 print 在同一行继续输出“World!”。

实践项目:简单战舰游戏

现在,让我们运用所学的关于函数、输入、输出和字符串操作的知识,来创建一个简单的文本版战舰游戏。

这个游戏的目标是猜测电脑隐藏在一张网格中的战舰位置。网格大小为8x8,战舰只占一个格子且位置固定。玩家输入坐标进行猜测,程序会显示猜测结果。

以下是构建这个游戏的核心步骤和代码逻辑:

  1. 设置游戏参数:定义网格大小、战舰位置和玩家猜测。

    width = 8
    height = 8
    ship_x = 4
    ship_y = 4
    guess_x = 1  # 可以改为从 input 获取
    guess_y = 1  # 可以改为从 input 获取
    
  2. 绘制初始网格:用点号 . 表示未知区域。

    # 打印整个网格
    print(('.' * width + '\n') * height, end='')
    
  3. 处理猜测并更新显示:这是游戏的核心逻辑。我们需要分三部分打印网格:猜测行之前的行、猜测行本身、猜测行之后的行。在猜测行中,需要判断是击中(用*表示)还是未击中(用O表示)。

    # 判断是否击中
    hit = (guess_x == ship_x) and (guess_y == ship_y)
    # 击中为1,未击中为0
    hit_num = int(hit)
    
    # 打印猜测行之前的行
    print(('.' * width + '\n') * guess_y, end='')
    
    # 打印猜测行
    # 击中显示*,未击中显示O
    symbol = '*' * hit_num + 'O' * (1 - hit_num)
    print('.' * guess_x + symbol + '.' * (width - guess_x - 1))
    
    # 打印猜测行之后的行
    print(('.' * width + '\n') * (height - guess_y - 1), end='')
    
  4. 整合与交互:将上述步骤整合,并使用 input 函数让玩家动态输入猜测坐标,替换掉代码中写死的 guess_xguess_y

    guess_x = int(input("请输入猜测的X坐标(0-7):"))
    guess_y = int(input("请输入猜测的Y坐标(0-7):"))
    

通过这个项目,你实践了如何调用函数(print, input, int)、传递参数、使用返回值,并利用这些基础构建了一个有交互的小程序。

总结

本节课中我们一起学习了Python函数的基础知识。我们了解了如何调用函数,以及参数返回值的概念。我们练习使用了 printinputtypeintfloat 等内置函数,并探索了 print 函数的 sepend 等关键字参数。最后,我们通过创建一个简单的文本版战舰游戏,综合运用了这些知识,包括获取用户输入、进行类型转换、条件判断和格式化输出。

记住,函数的核心思想是封装:你不需要知道函数内部如何实现,只需要知道它的名称、需要什么参数以及它会返回什么结果。这就像开车不需要懂得发动机原理一样。掌握如何使用现有函数是成为高效程序员的关键第一步。

008:创建函数

在本节课中,我们将学习如何创建自定义函数。我们将涵盖函数的基本语法、不同类型的参数(位置参数、关键字参数、默认参数),以及函数如何通过打印或返回值来执行操作。课程最后,我们将通过一些示例来巩固理解。


函数基础:语法与结构

上一节我们介绍了课程概述,本节中我们来看看函数的基本语法。

在数学中,我们有一个函数,例如 f(x) = x²。在Python中,我们将其转换为代码。以下是其对应关系:

  • 函数名:在数学中是 f,在Python中也是 f
  • 参数:数学函数接受 x,在Python中,x 放在括号内。
  • 定义关键字:Python使用 def 关键字来定义一个函数。
  • 冒号:Python中的冒号 : 类似于数学中的等号 =,表示后续缩进的代码属于该函数。
  • 函数体:在数学中是等号右侧的表达式。在Python中,我们需要使用 return 语句明确指定要返回的值。
  • 缩进:函数体内的所有语句必须缩进(通常是4个空格),这是Python识别代码块的方式。

让我们看另一个计算圆面积的函数。数学公式是 A = πr²。在Python中,我们可以这样写:

def area_of_circle(radius):
    pi = 3.14159
    area = pi * radius ** 2
    return area

我们也可以使用更长的、描述性的变量名,并且函数体可以包含多个步骤。例如,如果我们从直径开始计算:

def area_from_diameter(diameter):
    radius = diameter / 2
    pi = 3.14159
    area = pi * radius ** 2
    return area

所有属于函数体的代码行都必须保持相同的缩进。


函数类型:执行操作与返回值

上一节我们介绍了函数的基本结构,本节中我们来看看函数的两种主要类型。

函数主要分为两类:

  1. 执行操作的函数:这类函数执行一个动作,例如打印内容、修改变量或移动机器人。它们不一定返回一个具体的值。
  2. 返回值的函数:这类函数进行计算并返回一个结果,类似于数学函数。在课程中,我们有时称之为“有成果的函数”。

有些函数可能同时具备这两种特性。


实践:探索与创建函数

了解了函数类型后,让我们通过实践来加深理解。我们将先探索Python内置函数的行为,然后创建自己的版本。

以下是几个内置函数的例子及其自定义实现:

1. 绝对值函数 abs()

内置函数 abs(-3) 返回 3。我们可以创建自己的版本:

def absolute_value(x):
    return (x ** 2) ** 0.5

2. 平方根函数 sqrt()

平方根函数位于 math 模块中,需要先导入:

from math import sqrt
print(sqrt(25)) # 输出 5.0

自定义版本:

def square_root(x):
    return x ** 0.5

3. 幂函数 pow()

内置函数 pow(2, 3) 返回 8。自定义版本:

def power(base, exp):
    return base ** exp

参数详解:位置、关键字与默认值

上一节我们实践了创建函数,本节中我们深入探讨如何向函数传递参数。

Python提供了灵活的参数传递方式:

  • 位置参数:根据参数在定义时的顺序进行匹配。
  • 关键字参数:在调用函数时,通过参数名明确指定值。关键字参数必须位于所有位置参数之后,但彼此之间顺序可以任意。
  • 默认参数:在定义函数时为参数指定默认值。调用时如果未提供该参数的值,则使用默认值。

以下是使用示例:

def power(base, exp=2): # exp 是默认参数,默认值为 2
    return base ** exp

# 使用位置参数
print(power(4, 3)) # 64
# 使用关键字参数(顺序可调换)
print(power(exp=3, base=4)) # 64
# 使用默认参数
print(power(4)) # 16,相当于 power(4, 2)
# 错误示例:位置参数在关键字参数之后
# print(power(exp=3, 4)) # 语法错误

理解参数填充的优先级很重要:位置参数 > 关键字参数 > 默认参数


模块:组织你的代码

上一节我们讨论了函数参数,本节中我们来看看如何通过模块来组织函数。

模块是包含函数集合的Python文件(扩展名为 .py)。它们可以帮助你组织代码,使其更易于管理和复用。

使用现有模块

例如,使用我们提供的 dog.py 模块:

from dog import * # 导入 dog 模块中的所有函数
speak() # 输出: bark bark bark bark bark
stick = fetch() # fetch 函数返回字符串 'stick'
print(stick) # 输出: stick

创建自己的模块

创建一个名为 cat.py 的文件,内容如下:

def speak():
    """This function makes the cat meow.""" # 文档字符串
    print("meow")

然后在主程序中使用它:

import cat # 导入整个模块
cat.speak() # 输出: meow

为了避免函数名冲突,建议使用 import module_name 的方式,然后通过 module_name.function_name() 来调用函数。

探索模块

你可以查看模块中包含哪些函数:

import math
print(dir(math)) # 列出 math 模块中的所有属性和函数

你也可以查看特定函数的文档:

print(math.ceil.__doc__) # 查看 ceil 函数的文档字符串
print(math.ceil(2.3)) # 3,向上取整

返回值 vs. 打印输出

在编写函数时,一个重要决策是让函数返回值,还是在函数内部直接打印。

  • 在函数内部打印:函数直接控制输出。这适用于始终需要显示结果的情况,但限制了调用者对结果的使用方式(例如,无法将结果用于进一步计算)。
  • 返回值:函数将结果交给调用者处理。调用者可以决定是打印、存储还是进行其他计算。这通常使函数更加灵活和通用。

一个函数只能执行一次 return 语句。一旦执行 return,函数会立即结束,其后的代码不会运行。


代码追踪练习

理解代码执行流程至关重要。让我们手动追踪一个包含多个函数调用的程序。

考虑以下代码:

def c():
    print("C")

def b():
    print("B1")
    c()
    print("B2")

def a():
    print("A1")
    b()
    print("A2")

a()

执行顺序和输出如下:

  1. 调用 a()
  2. a() 打印 A1
  3. a() 调用 b()
  4. b() 打印 B1
  5. b() 调用 c()
  6. c() 打印 C
  7. c() 结束,返回 b()
  8. b() 打印 B2
  9. b() 结束,返回 a()
  10. a() 打印 A2

最终输出为:

A1
B1
C
B2
A2

课程总结

本节课中我们一起学习了如何创建和使用Python函数。

我们涵盖了以下核心概念:

  • 函数的基本语法,包括 def 关键字、参数、冒号和缩进。
  • 两种主要的函数类型:执行操作的函数和返回值的函数。
  • 三种参数传递方式:位置参数、关键字参数和默认参数,以及它们的优先级规则。
  • 如何通过模块来组织函数,以及如何导入和使用模块。
  • 在函数内部打印输出与返回值的区别,通常返回值的函数更具灵活性。
  • 通过代码追踪来理解程序的执行流程。

掌握这些概念是构建更复杂、模块化程序的基础。

009:函数作用域 🧭

在本节课中,我们将要学习函数作用域。这是一个关于函数与变量关系的核心概念,理解它对于编写和阅读Python代码至关重要。我们将探讨变量在何处被创建、如何被访问以及何时被销毁,并学习管理变量可见性的规则。

概述

上一节我们介绍了函数的基本概念。本节中,我们来看看函数作用域。作用域决定了在代码的哪个部分可以访问哪个变量。我们将通过10条规则来系统地学习局部变量、全局变量以及参数传递的相关知识。

局部变量规则

首先,我们来学习关于局部变量的四条规则。局部变量是在函数内部创建的变量。

规则 1:函数不执行,除非被调用

函数定义本身不会执行其内部的代码。只有在函数被调用时,其内部的语句才会运行。

def set_x():
    x = 100

print(x)  # 错误:NameError: name 'x' is not defined

在上面的代码中,set_x函数从未被调用,因此变量x从未被创建。

规则 2:函数内创建的变量在函数返回后消亡

在函数内部创建的变量(局部变量)的生命周期仅限于该函数的执行期间。一旦函数执行完毕并返回,其局部变量就会被销毁。

def set_x():
    x = 100

set_x()
print(x)  # 错误:NameError: name 'x' is not defined

即使调用了set_x函数,其内部的变量x在函数返回后也不复存在。

规则 3:每次函数调用都会创建全新的局部变量

每次调用函数时,都会为其创建一个全新的帧(frame),其中的局部变量也是全新的,不会保留上一次调用的值。

def count():
    x = 1
    x += 1
    print(x)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/1dd2d4d5aeedb1ffc400d0fee090f655_1.png)

count()  # 输出:2
count()  # 输出:2 (不是3)

两次调用count函数,每次都会创建一个新的变量x并将其初始化为1。

规则 4:函数无法看到调用者的局部变量

一个函数不能直接访问调用它的那个函数(父函数)的局部变量。这些变量对它是“不可见”或“超出作用域”的。

def display_x():
    print(x)

def main():
    x = 100
    display_x()

main()  # 错误:NameError: name 'x' is not defined

display_x函数试图访问变量x,但xmain函数的局部变量,对display_x不可见。

全局变量规则

接下来,我们学习关于全局变量的四条规则。全局变量是在所有函数之外定义的变量。

规则 5:函数可以读取全局变量(无需特殊声明)

在函数内部,可以直接读取(访问)全局变量的值。

message = “hello”

def greeting():
    print(message)  # 可以访问全局变量 message

greeting()  # 输出:hello

规则 6:在函数内赋值会创建新的局部变量(而非修改全局变量)

如果在函数内部对变量进行赋值操作(使用=),Python会默认创建一个新的局部变量,即使存在同名的全局变量。

message = “hello”

def greeting():
    message = “welcome”  # 创建新的局部变量 message
    print(message)

print(“before:”, message)  # 输出:before: hello
greeting()                 # 输出:welcome
print(“after:”, message)   # 输出:after: hello (全局变量未改变)

规则 7:Python会向前查找函数内的赋值语句

Python在解析函数时,会预先扫描整个函数体。如果发现任何对变量的赋值语句,那么在整个函数范围内,该变量都会被当作局部变量处理,即使在赋值之前引用它也是如此。

message = “hello”

def greeting():
    print(message)        # 错误!因为下一行有对message的赋值
    message = “welcome”   # 这使得整个函数内的message都是局部变量

greeting()  # UnboundLocalError: local variable ‘message’ referenced before assignment

规则 8:使用global声明可以修改全局变量

如果确实需要在函数内部修改全局变量,可以使用global关键字进行声明。

message = “hello”

def greeting():
    global message        # 声明 message 是全局变量
    print(message)        # 输出:hello
    message = “welcome”   # 修改全局变量

print(“before:”, message)  # 输出:before: hello
greeting()                 # 输出:hello (函数内打印)
print(“after:”, message)   # 输出:after: welcome (全局变量被修改)

使用global会带来“副作用”,即函数影响了外部状态,通常建议谨慎使用。

参数传递规则

最后,我们来看关于参数传递的两条规则。参数是将数据从函数外部传递到内部的桥梁。

规则 9:修改形参不会影响实参

函数内部的参数(形参)是局部变量。修改形参的值,不会影响函数外部用来传递数据的那个变量(实参)。

def f(x):
    x = “b”
    print(“inside f:”, x)  # 输出:inside f: b

val = “a”
print(“before:”, val)  # 输出:before: a
f(val)                 # 传递 val 的值 “a” 给形参 x
print(“after:”, val)   # 输出:after: a (val 未改变)

规则 10:即使形参与实参同名,它们也是不同的变量

即使函数外部的变量(实参)和函数内部的参数(形参)名字相同,它们也是两个独立的变量,位于不同的作用域。

def f(x):               # 这个 x 是局部形参
    x = “b”
    print(“inside f:”, x)  # 输出:inside f: b

x = “a”                 # 这个 x 是全局变量
print(“before:”, x)     # 输出:before: a
f(x)                    # 将全局变量 x 的值 “a” 复制给局部形参 x
print(“after:”, x)      # 输出:after: a (全局变量 x 未改变)

函数f内部的x是局部变量,修改它不会影响外部的全局变量x

总结

本节课中我们一起学习了Python函数作用域的10条核心规则。
我们了解了局部变量的生命周期和独立性,掌握了全局变量的访问与修改方法,并理解了参数传递时值复制的特性。
记住这些规则的关键在于理解“帧”的概念:每次函数调用都会创建一个新的局部帧,变量在其中诞生、被访问、然后消亡。
合理运用这些知识,可以帮助你编写出结构清晰、易于调试的代码。

010:第二轮迭代

概述

在本节课中,我们将学习高级迭代概念,包括 breakcontinue 语句。我们将探讨几种常见的循环设计模式,并通过实例理解如何高效地阅读和编写循环代码。掌握这些模式将帮助你更清晰地理解他人代码的逻辑。


课程公告与安排

在深入技术内容之前,有几个课程相关的通知。

首先,感谢大家的关心。我上周日出现胸痛,今天下午在急诊室接受了心电图和心脏压力测试。因此,我将无法参加今天的办公时间,明天恢复。对此带来的不便,我深表歉意。

关于课程安排,我们有一个合作伙伴匹配表格,适用于为即将在周三发布的下一个项目寻找搭档的同学。随着学期推进,项目会变得更长,期末项目每周可能耗时长达8小时。填写表格后,我们会为你推荐合适的搭档。

此外,我们还有一个感谢表格。如果你觉得某位助教在办公时间提供了特别帮助,请填写此表格。他们的工作值得被看见和感谢。

最后,第一次考试将在周五进行。考试详情已通过邮件发送。我强烈建议你通过复习往期试卷、阅读教材(如《Automate the Boring Stuff》的前三章)以及查看课程大纲来备考。考试期间,你可以使用网络和笔记,但禁止与他人交流。


循环设计模式

上一节我们介绍了循环的基础。本节中,我们来看看几种常见的循环设计模式。理解这些模式能让你更快地解读代码逻辑,就像熟悉五段式文章结构有助于快速理解论文一样。

模式一:执行固定次数

这种模式用于将一段代码重复执行固定的次数。

核心结构:

i = 0
while i < n:
    # 执行某些操作
    i += 1

或者从1开始计数:

i = 1
while i <= n:
    # 执行某些操作
    i += 1

分析步骤:

  1. 找到循环条件(while语句)。
  2. 识别循环控制变量(例如 i)。
  3. 找到所有改变该变量的地方(初始化和递增/递减)。
  4. 理解循环体(“三明治”的中间部分)在每个迭代中做了什么。

通过“展开”循环(想象将循环体复制粘贴N次)来思考,比逐行跟踪执行更高效。

模式二:处理数据集中的所有项

这种模式用于遍历一个已知大小的数据集(例如列表、表格中的每一行)。

核心结构:

i = 0
while i < count_rows(data_table):
    item = get_item(data_table, i) # 获取第i项数据
    # 对item进行操作(如求和、查找最大值等)
    i += 1

这里,count_rows 函数返回数据项的总数,get_item 函数根据索引 i 获取具体数据。

模式三:处理数据直到结束

这种模式适用于数据量未知或由模块提供数据流的情况。

核心结构:

while has_more_data():
    item = get_next_data()
    # 对item进行操作

模块会提供两个函数:has_more_data() 返回布尔值指示是否还有数据;get_next_data() 返回下一个数据项,并在内部管理当前读取位置。


循环嵌套分析

现在,我们来看看包含嵌套循环的代码。处理嵌套循环的关键是由外向内逐层分析。

以下是分析嵌套循环的步骤:

  1. 识别最外层的循环及其控制变量。
  2. 确定该变量的变化范围和规律。
  3. 将外层循环体(包含内层循环)视为一个整体,针对外层变量的每个取值,“展开”执行一次。
  4. 在每次“展开”中,再对内层循环进行分析。

通过这种分层“展开”的方法,可以清晰地理解复杂的嵌套逻辑。


Break语句

break 语句提供了一种提前退出循环的方式。它通常与条件判断 if 结合使用。

控制流图示:
当循环体中的 if 条件满足并执行 break 时,程序会立即跳出当前循环,继续执行循环之后的代码。

常见用途:
在数据集中搜索特定项时,一旦找到目标,就可以使用 break 终止搜索,无需遍历剩余数据。

让我们通过一个寻找范围内是否存在质数的例子来实践。

示例:检查数值范围内是否存在质数

def range_has_prime(start, end):
    i = start
    prime_found = False
    while i <= end:
        if is_prime(i): # 假设 is_prime 是已定义的函数
            prime_found = True
            break # 找到质数,立即跳出循环
        i += 1
    return prime_found

在这个函数中,一旦 is_prime(i) 返回 True,我们将 prime_found 设为 True 并用 break 跳出循环,避免不必要的后续检查。


Continue语句

continue 语句用于跳过当前循环迭代的剩余部分,直接开始下一次迭代。

控制流图示:
当循环体中的 if 条件满足并执行 continue 时,程序会跳过本次循环中 continue 之后的所有代码,直接回到循环条件判断处。

常见用途:
过滤无效输入。例如,在计算一系列分数的平均分时,可以跳过超出合理范围(如0-100以外)的分数。

让我们通过一个计算用户输入分数平均值的程序来演示。

示例:计算有效分数的平均值

total = 0
count = 0
while True:
    user_input = input("Enter a score (0-100), or 'Q' to quit: ")
    if user_input == 'Q':
        print("Exiting.")
        break
    score = int(user_input)
    if score < 0 or score > 100:
        print("Bad value. Score must be between 0 and 100.")
        continue # 跳过无效分数,继续下一轮输入
    total += score
    count += 1
    average = total / count
    print(f"Current average: {average}")

在这个例子中,continue 确保了无效分数不会被加入 totalcount,从而不影响平均值的计算。


嵌套循环中的Continue

需要特别注意,continue 语句只影响它所在的最内层循环。

例如,在下面的嵌套循环中:

i = 1
while i <= 3:
    j = 1
    while j <= i:
        if some_condition:
            continue # 这只跳过内层while的当前迭代
        j += 1
    i += 1

some_condition 为真时,continue 会跳回内层 while j <= i 的条件判断,而不会影响外层 while i <= 3 的迭代。


总结

本节课我们一起学习了高级迭代技术。我们探讨了三种常见的循环设计模式,帮助你结构化地思考循环。我们深入了解了 breakcontinue 语句,它们分别用于提前终止循环和跳过当前迭代。我们还分析了如何解读嵌套循环,并强调了 continue 在嵌套结构中的作用范围。掌握这些概念和模式,将极大地提升你编写和阅读Python循环代码的能力。

011:字符串与For循环 🐍

在本节课中,我们将要学习字符串的更多操作以及一种新的循环结构——for循环。我们将探讨字符串的比较、常用方法、序列的概念,以及如何使用for循环来遍历序列中的元素。


字符串比较 🔍

上一节我们介绍了字符串的基本操作,本节中我们来看看如何比较字符串。Python允许我们使用关系运算符(如 <, >, ==)来比较字符串,其规则基于字典序(即字母顺序)。

核心概念:字符串比较基于字符的Unicode编码顺序。大写字母的编码小于小写字母的编码。

以下是字符串比较的几个关键点:

  1. 基本字母顺序:比较时,Python会逐个字符地比较两个字符串,直到找到第一个不同的字符,然后根据这两个字符的编码决定大小。
    • 例如:"cat" < "dog"True,因为 'c''d' 之前。
  2. 大小写敏感:所有大写字母的编码都小于任何小写字母。
    • 例如:"Zebra" < "apple"True,因为 'Z' 的编码小于 'a'
  3. 数字作为字符:当数字作为字符串的一部分时,比较的是字符编码,而不是数值大小。比较从第一个字符开始。
    • 例如:"100" < "15"True,因为首先比较 '1''1'(相同),然后比较 '0''5''0' 的编码小于 '5'
  4. 前缀关系:如果一个字符串是另一个字符串的前缀,则较短的字符串被视为更小。
    • 例如:"bat" < "batman"True,因为比较完 "bat" 后,第一个字符串已结束,空字符被视为小于任何其他字符。

字符串方法 📚

字符串是Python中的一种序列类型,拥有许多内置的“方法”。方法是与特定数据类型关联的函数。我们使用点号(.)来调用它们。

核心概念:方法是作用于特定对象(如字符串)的函数,调用格式为 对象.方法名(参数)

以下是一些常用的字符串方法:

  • .upper().lower(): 将字符串中的所有字母转换为大写或小写。
    msg = "Hello"
    print(msg.upper())  # 输出: HELLO
    print(msg.lower())  # 输出: hello
    
  • .strip().lstrip().rstrip(): 移除字符串首尾的空白字符(如空格、制表符、换行符)。
    text = "  Hello World  \n"
    print(text.strip())   # 输出: "Hello World"
    print(text.lstrip())  # 输出: "Hello World  \n"
    print(text.rstrip())  # 输出: "  Hello World"
    
  • .find(sub): 返回子字符串 sub 在字符串中第一次出现的索引(位置),如果未找到则返回 -1。索引从0开始。
    s = "CS220 is awesome"
    print(s.find("is"))    # 输出: 6
    print(s.find("q"))     # 输出: -1
    
  • .startswith(prefix).endswith(suffix): 检查字符串是否以指定的前缀或后缀开头或结尾,返回布尔值。
    filename = "report.pdf"
    print(filename.endswith(".pdf"))  # 输出: True
    
  • .replace(old, new): 将字符串中所有的 old 子串替换为 new 子串。
    sentence = "I like apples"
    new_sentence = sentence.replace("apples", "bananas")
    print(new_sentence)  # 输出: "I like bananas"
    
  • .format(): 用于格式化字符串,将花括号 {} 占位符替换为提供的参数。
    name = "Mike"
    product = "Muscle Builder 4000X"
    message = "Dear {}, please buy our {} for half off!".format(name, product)
    print(message)
    # 输出: Dear Mike, please buy our Muscle Builder 4000X for half off!
    

重要提示:字符串方法不会修改原始字符串,而是返回一个新的字符串。


序列操作:索引与切片 🧩

字符串是“序列”的一种。序列是元素的有序集合,每个元素都有一个位置编号,称为“索引”。Python中索引从0开始。

核心概念:序列允许通过索引(单个位置)和切片(一个范围)来访问其元素。

  1. 索引:使用方括号 [] 和索引来获取序列中的一个元素。
    s = "ABCDE"
    print(s[0])   # 输出: 'A' (第一个字符)
    print(s[2])   # 输出: 'C' (第三个字符)
    print(s[-1])  # 输出: 'E' (最后一个字符,负索引从-1开始)
    # s[5] 会导致 IndexError,因为索引越界。
    
  2. 切片:使用 [start:end] 语法获取序列的一个子序列。start 索引是包含的,end 索引是不包含的。
    word = "pizza"
    print(word[1:4])    # 输出: "izz" (索引1,2,3)
    print(word[:2])     # 输出: "pi"  (从开头到索引2,不包含2)
    print(word[3:])     # 输出: "za"  (从索引3到结尾)
    print(word[:])      # 输出: "pizza" (整个字符串的副本)
    print(word[-3:-1])  # 输出: "zz"  (使用负索引)
    
    • 切片非常灵活,即使索引超出范围也不会报错,只会返回尽可能多的元素。
    • 这种“含头不含尾”的设计使得分割和组合字符串变得容易,例如 word[:3] + "..." + word[3:]

注意:字符串是不可变的,不能通过索引直接修改某个字符,如 s[2] = 'X' 会导致错误。必须通过创建新字符串(如使用切片和拼接)来实现修改。


For循环遍历序列 🔄

for 循环是专门为遍历序列(如字符串、列表)而设计的迭代结构。它比 while 循环更简洁,无需手动管理索引计数器。

核心概念for 循环依次将序列中的每个元素赋值给一个变量,然后执行循环体。

基本语法

for 变量 in 序列:
    # 循环体,对每个元素执行的操作

示例:遍历字符串并打印每个字符

message = "hello"
for letter in message:
    print(letter)
# 输出:
# h
# e
# l
# l
# o

在这个例子中:

  1. for 是关键字。
  2. letter 是循环变量,在每次迭代中自动被赋予序列中的下一个元素。
  3. in 是关键字。
  4. message 是要遍历的序列(这里是字符串)。
  5. 冒号 : 和缩进定义了循环体。

对比 while 循环:使用 while 循环实现相同功能需要手动管理索引:

message = "hello"
i = 0
while i < len(message):
    letter = message[i]
    print(letter)
    i += 1

for 循环隐藏了索引管理的细节,使代码更清晰、更不易出错。

常见模式:使用 for 循环构建新字符串
一个常见的编程模式是初始化一个空字符串,然后在循环中逐步构建新字符串。

# 示例:反转字符串
original = "hello"
reversed_str = ""  # 1. 初始化空字符串
for char in original:
    reversed_str = char + reversed_str  # 2. 将新字符加到前面
print(reversed_str)  # 输出: "olleh"


总结 📝

本节课中我们一起学习了:

  1. 字符串比较:基于字典序,需注意大小写和数字作为字符时的比较规则。
  2. 字符串方法:如 .upper(), .find(), .replace(), .format() 等,它们对字符串进行操作并返回新字符串。
  3. 序列操作:字符串作为序列,支持通过索引获取单个元素,通过切片获取子串。
  4. For循环:一种简洁的迭代结构,专门用于遍历序列中的每个元素,无需手动管理索引,使代码更易读、更安全。

掌握这些知识将使你能够更有效地处理和操作文本数据,并为学习更复杂的数据结构(如下一单元将介绍的列表)打下坚实基础。

012:范围循环与列表 🐍

在本节课中,我们将要学习Python中的两种重要循环结构——范围循环(for循环与range函数)和列表数据结构。我们将通过对比字符串和列表,理解序列的通用操作,并学习如何创建、访问和修改列表。


概述

上一节我们介绍了基本的for循环,用于遍历序列中的每个元素。本节中,我们将深入探讨range函数,它允许我们生成一个数字序列,常用于需要索引的循环场景。同时,我们将正式引入列表(List)这一核心数据结构,它是一种可变、有序的容器,可以存储任意类型的元素。


范围循环详解

range函数用于生成一个整数序列,常与for循环结合使用,以便在循环中获取元素的索引。它有三种主要形式:

  1. range(stop):生成从0到stop-1的整数。
  2. range(start, stop):生成从startstop-1的整数。
  3. range(start, stop, step):生成从startstop-1的整数,步长为step

以下是每种形式的示例:

# 形式1: range(stop)
for i in range(6):
    print(i)  # 输出: 0, 1, 2, 3, 4, 5

# 形式2: range(start, stop)
for i in range(2, 6):
    print(i)  # 输出: 2, 3, 4, 5

# 形式3: range(start, stop, step)
for i in range(2, 11, 2):
    print(i)  # 输出: 2, 4, 6, 8, 10

当我们需要在遍历序列(如字符串或列表)的同时知道当前元素的索引时,range循环非常有用。例如,打印字符串“Python”中每个字符及其索引:

word = "Python"
for i in range(len(word)):
    print(f"索引 {i}: 字符 {word[i]}")

列表基础

列表是Python中最基本的数据结构之一。它是一个可变、有序的序列,可以存储任意类型的元素。

创建列表

创建列表使用方括号[],元素之间用逗号分隔。

# 创建一个包含数字的列表
numbers = [22, 11, 33]

# 创建一个包含不同类型元素的列表
mixed_list = ["字符串", 3.14, 42, True, [1, 2, 3]]

访问列表元素

与字符串类似,列表支持索引和切片操作。

nums = [22, 11, 33]
print(nums[0])   # 输出: 22 (索引从0开始)
print(nums[-1])  # 输出: 33 (负索引表示从末尾开始)
print(nums[1:])  # 输出: [11, 33] (切片操作)

遍历列表

可以使用for循环直接遍历列表中的元素。

for num in nums:
    print(num)

如果需要同时获取索引和元素,可以结合rangelen函数。

for i in range(len(nums)):
    print(f"索引 {i}: 值 {nums[i]}")

序列的通用操作

字符串和列表都属于序列(Sequence),因此它们共享许多操作。以下是这些通用操作的对比:

操作 字符串示例 列表示例 说明
长度 len("321go") len([99, 11, 77, 55]) 返回序列中元素的数量。
连接 "321go" + "!!" [99, 11] + [77, 55] 将两个序列合并成一个新序列。
成员检查 "g" in "321go" 11 in [99, 11, 77] 检查元素是否存在于序列中。
重复 "321go" * 2 [99, 11] * 2 将序列重复指定次数。
索引 "321go"[1] [99, 11, 77][1] 通过位置访问单个元素。
切片 "321go"[1:4] [99, 11, 77][1:3] 获取序列的一个子序列。
遍历 for c in "321go": for x in [99, 11, 77]: 使用for循环遍历每个元素。

列表的独有特性:可变性

列表与字符串的一个关键区别是可变性。字符串是不可变的,创建后无法修改其中的字符。而列表是可变的,可以修改、添加或删除其中的元素。

修改元素(更新)

通过索引直接为列表中的某个位置赋值。

my_list = [6, 2, 8]
my_list[0] = 8  # 将索引0的元素从6改为8
print(my_list)  # 输出: [8, 2, 8]

添加元素

  • append(item):在列表末尾添加一个元素。
  • extend(iterable):在列表末尾添加另一个可迭代对象中的所有元素。
my_list = [4, 2]
my_list.append(9)        # 添加单个元素
print(my_list)           # 输出: [4, 2, 9]

my_list.extend([3, 5])   # 添加多个元素
print(my_list)           # 输出: [4, 2, 9, 3, 5]

删除元素

  • pop():移除并返回列表的最后一个元素。
  • pop(index):移除并返回指定索引位置的元素。
my_list = [4, 2, 9, 3, 5]
last_item = my_list.pop()      # 移除最后一个元素5
print(last_item)               # 输出: 5
print(my_list)                 # 输出: [4, 2, 9, 3]

second_item = my_list.pop(1)   # 移除索引1的元素2
print(second_item)             # 输出: 2
print(my_list)                 # 输出: [4, 9, 3]

排序

  • sort():对列表进行原地排序(修改原列表)。

my_list = [9, 2, 5, 8, 3]
my_list.sort()
print(my_list)  # 输出: [2, 3, 5, 8, 9]

字符串与列表的转换

在实际编程中,经常需要在字符串和列表之间进行转换。两个核心方法是split()join()

字符串 -> 列表:split()

split()是一个字符串方法,它根据指定的分隔符将字符串分割成一个列表。

sentence = "a quick brown fox"
word_list = sentence.split(" ")  # 以空格为分隔符
print(word_list)  # 输出: ['a', 'quick', 'brown', 'fox']

列表 -> 字符串:join()

join()也是一个字符串方法,它使用指定的字符串作为连接符,将一个字符串列表连接成一个单独的字符串。

letters = ['M', 'I', 'S', 'S', 'I', 'S', 'S', 'I', 'P', 'P', 'I']
separator = ''  # 使用空字符串连接
word = separator.join(letters)
print(word)  # 输出: 'MISSISSIPPI'

综合示例:文本审查器

让我们运用所学知识,编写一个简单的文本审查函数。该函数将输入字符串中的不雅词汇替换为星号*

def censor(dirty_string):
    """将字符串中的不雅词汇替换为星号。"""
    # 定义不雅词汇列表
    bad_words = ["OMG", "exam", "buggered"]

    # 将字符串分割成单词列表
    words = dirty_string.split()

    # 遍历单词列表(使用range循环以获取索引)
    for i in range(len(words)):
        word = words[i]
        # 如果单词在不雅词汇列表中,则替换为等长的星号
        if word in bad_words:
            words[i] = '*' * len(word)

    # 将处理后的单词列表重新连接成字符串
    clean_string = ' '.join(words)
    return clean_string

# 测试函数
message = "OMG that exam was so buggered!"
clean_message = censor(message)
print(clean_message)  # 输出: *** that **** was so *********

在这个例子中,我们:

  1. 使用split()将句子拆分成单词列表。
  2. 使用range(len(words))循环来获取每个单词的索引。
  3. 检查每个单词是否在bad_words列表中。
  4. 如果是,则使用字符串乘法'*' * len(word)生成等长的星号进行替换。
  5. 最后,使用' '.join(words)将列表重新组合成字符串。

总结

本节课中我们一起学习了Python中两个强大的工具:范围循环和列表。

  • 范围循环:通过range()函数,我们可以生成数字序列,这在需要索引的循环中至关重要。
  • 列表:作为一种可变、有序的序列,列表可以存储各种类型的数据。我们学习了如何创建、访问、修改列表,以及如何利用其可变性进行添加、删除和排序操作。
  • 序列通用操作:我们对比了字符串和列表,理解了它们共享的索引、切片、遍历等特性。
  • 转换与综合应用:掌握了使用split()join()在字符串与列表间转换的方法,并通过一个文本审查器的例子,将循环、列表操作和字符串方法结合起来解决实际问题。

理解列表和循环是进行有效数据编程的基础。在接下来的课程中,我们将探索更复杂的数据结构,如字典,并利用它们处理更丰富的数据集。

013:表格数据与CSV文件 📊

在本节课中,我们将要学习如何处理表格数据,特别是CSV文件。我们将从回顾列表和计算中位数开始,然后深入探讨如何将Excel电子表格转换为CSV格式,并使用Python的csv模块读取和处理这些数据。最后,我们将通过一个查找餐厅坐标的示例程序,学习如何结合命令行参数来使用CSV数据。


回顾:计算中位数

上一节我们介绍了列表的基本操作,本节中我们来看看如何使用列表计算一组数字的中位数。中位数是将所有数值排序后位于中间的值。如果列表有奇数个元素,中位数就是中间的那个数;如果有偶数个元素,中位数则是中间两个数的平均值。

首先,我们定义一个函数来计算中位数。

def median(items):
    items.sort()
    length = len(items)
    middle_index = length // 2

    if length % 2 == 1:
        # 奇数个元素,返回中间值
        return items[middle_index]
    else:
        # 偶数个元素,返回中间两个值的平均值
        first = items[middle_index - 1]
        second = items[middle_index]
        return (first + second) / 2

以下是测试该函数的示例:

# 测试奇数个元素
nums = [-2, -1, 3, 20, 30]
print(median(nums))  # 输出应为 3

# 测试偶数个元素
nums_even = [1, 2, 3, 4]
print(median(nums_even))  # 输出应为 2.5

这个函数首先对列表进行排序,然后根据元素数量是奇数还是偶数来计算中位数。注意,对于空列表或非数值列表(如字符串),此函数可能会出错,在实际应用中需要额外处理这些情况。


表格数据与CSV文件

现在,让我们转向今天的核心主题:表格数据。表格数据通常以行和列的形式组织,就像电子表格一样。在编程中,我们经常使用CSV(逗号分隔值)文件来存储和交换表格数据。

什么是CSV文件?

CSV文件是一种纯文本文件,用逗号分隔每个单元格(列),用换行符分隔每一行。它比Excel等二进制格式更简单、更通用,因为任何文本编辑器都能打开它,并且Python可以轻松读取。

CSV文件的特点包括:

  • 每行代表表格中的一行数据。
  • 每行中的单元格由逗号分隔。
  • 第一行通常是标题行,描述每一列的含义。
  • 所有数据(包括数字)在文件中都以字符串形式存储。

从Excel到CSV

通常,数据可能最初存在于Excel(.xlsx)文件中。我们可以将其另存为CSV格式,以便在Python中处理。

操作步骤如下:

  1. 在Excel中打开文件。
  2. 点击“文件” -> “另存为”。
  3. 在“保存类型”中选择“CSV (逗号分隔) (*.csv)”。
  4. 保存文件。请注意,此过程会丢失所有格式(如颜色、字体)和公式,仅保留数据。

使用Python读取CSV文件

为了在Python中读取CSV文件,我们将使用内置的csv模块。下面的代码定义了一个通用函数process_csv,它可以读取任何CSV文件并将其内容转换为“列表的列表”结构。

import csv

def process_csv(filename):
    example_file = open(filename, encoding='utf-8')
    example_reader = csv.reader(example_file)
    example_data = list(example_reader)
    example_file.close()
    return example_data

# 代码来源: Automate the Boring Stuff, Chapter 14 (第一版) / Chapter 16 (第二版)

如何使用这个函数:
调用process_csv('文件名.csv')会返回一个列表。这个外层列表的每个元素也是一个列表,代表CSV文件中的一行。例如,data[0]是标题行,data[1]是第一行数据。

访问特定数据:
如果rowsprocess_csv返回的列表,那么:

  • rows[1][0] 访问第2行,第1列(索引从0开始)。
  • rows[0][-1] 访问第1行(标题行)的最后一列。

实战示例:餐厅坐标查找程序 🍽️

我们将创建一个程序,读取包含餐厅名称及其坐标的CSV文件,然后根据用户从命令行输入的名称,输出该餐厅的坐标。

第一步:准备数据

我们有一个restaurants.csv文件,内容格式如下:

McDonald's,4,-3
Subway,1,0
The Sett,-1,0
Greenbush Donuts,0,2

第二步:编写程序 (rlookup.py)

以下是完整的程序代码,它结合了CSV处理和命令行参数。

import csv
import sys

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/4aa7dd075dbc47c16a081c8c6295aae1_1.png)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/4aa7dd075dbc47c16a081c8c6295aae1_3.png)

# --- 粘贴 process_csv 函数 ---
def process_csv(filename):
    example_file = open(filename, encoding='utf-8')
    example_reader = csv.reader(example_file)
    example_data = list(example_reader)
    example_file.close()
    return example_data
# --- 函数结束 ---

def main():
    # 1. 读取CSV数据
    rows = process_csv('restaurants.csv')

    # 2. 获取命令行参数
    # sys.argv[0] 是程序名 'rlookup.py',sys.argv[1] 是用户输入的餐厅名
    if len(sys.argv) < 2:
        print("请提供餐厅名称作为参数。")
        return

    search_term = sys.argv[1]

    # 3. 遍历数据行,查找匹配的餐厅
    for r in rows:
        if r[0] == search_term:
            # 4. 找到后,格式化输出坐标
            print(f"x = {r[1]}, y = {r[2]}")
            return

    # 5. 如果循环结束都没找到
    print(f"未找到名为 '{search_term}' 的餐厅。")

if __name__ == "__main__":
    main()

第三步:从命令行运行程序

打开终端(Windows PowerShell 或 Mac Terminal),导航到程序所在目录,然后运行:

python rlookup.py "Subway"

输出应为:

x = 1, y = 0

关键点说明:

  • import sys:导入系统模块,用于访问命令行参数sys.argv
  • sys.argv:这是一个列表,包含所有命令行参数。第一个元素sys.argv[0]是脚本名称。
  • 参数包含空格时:如果餐厅名包含空格(如“The Sett”),需要在命令行中用引号括起来。

常见问题与解决

  • 文件扩展名问题:在Windows上下载CSV文件时,有时会自动保存为.txt。你可以在文件资源管理器中显示文件扩展名,并用ren命令重命名:
    ren restaurants.txt restaurants.csv
    
  • 编码问题process_csv函数中指定了encoding='utf-8',这有助于在不同操作系统上正确读取文件。

总结

本节课中我们一起学习了:

  1. 中位数的计算:通过列表排序和索引操作,编写了处理奇偶数量元素的函数。
  2. CSV文件基础:了解了CSV作为纯文本表格格式的优点和结构。
  3. 读取CSV文件:利用csv模块和process_csv函数,将CSV数据转换为Python的“列表的列表”结构。
  4. 综合应用:创建了一个命令行程序,通过结合CSV读取和命令行参数sys.argv,实现了根据名称查找餐厅坐标的功能。

掌握这些技能是进行数据分析和处理的基础。在接下来的课程和项目中,你会频繁地使用这些技术来操作各种表格数据集。

014:字典

在本节课中,我们将要学习Python中一个非常重要的数据结构——字典。我们将了解字典的基本概念、如何创建和使用字典,以及它与列表等其他数据结构的区别。通过一个具体的编程示例,我们将看到字典如何使代码变得更简洁、更强大。


字典简介

上一节我们介绍了数据结构的通用概念,本节中我们来看看字典这一具体的数据结构。

字典是一种映射类型的数据结构,它将一个唯一的“键”与一个“值”关联起来。你可以把它想象成一本真实的字典:单词(键)对应着它的定义(值)。

在Python中,字典使用花括号 {} 定义,键和值之间用冒号 : 分隔。

核心概念公式

字典 = {键1: 值1, 键2: 值2, ...}

字典的创建与基本操作

与列表类似,字典也有创建、更新、访问和删除等基本操作。

创建字典

以下是创建字典的几种方式:

  • 使用花括号直接创建:my_dict = {'name': 'Alice', 'score': 95}
  • 使用 dict() 函数创建空字典:empty_dict = dict()
  • 仅使用花括号创建空字典(默认行为):also_empty_dict = {}

访问与更新字典元素

访问字典中的值,需要使用方括号 [] 并提供对应的键。

scores = {'Alice': 95, 'Bob': 87}
alice_score = scores['Alice']  # 获取Alice的分数,结果为95

更新字典中已有的值,语法与列表赋值类似。

scores['Bob'] = 90  # 将Bob的分数更新为90

向字典插入新元素

与列表需要使用 .append() 方法不同,字典可以直接通过赋值来添加新的键值对。

scores['Charlie'] = 88  # 插入一个新的键值对

从字典中删除元素

使用 .pop() 方法并指定要删除的键,可以移除字典中的元素,该方法会返回被删除的值。

removed_score = scores.pop('Bob')  # 删除键为'Bob'的项,并返回其值87

字典与列表、集合的对比

为了更清晰地理解不同数据结构的特点,我们可以从值、关系和操作三个方面进行对比。

以下是三种数据结构的简要对比:

  • 列表 (List):
    • : 可以是任意类型,允许重复。
    • 关系: 有序,通过整数索引(0, 1, 2...)访问。
    • 操作: 索引、切片、.append().pop()len()、排序等。
  • 集合 (Set):
    • : 可以是任意不可变类型,元素唯一
    • 关系: 无序,没有索引。
    • 操作: in(成员检查)、并集、交集等。
  • 字典 (Dict):
    • : 值可以是任意类型。必须是不可变类型(如字符串、数字、元组)。
    • 关系: 在Python 3.7+中,保持插入顺序。通过而非数字索引访问。
    • 操作: 通过键访问 dict[key].pop(key).keys().values() 等。

实战演练:重构计分程序

现在,让我们通过一个实际例子来巩固对字典的理解。我们将重构一个简单的玩家计分程序,使其从使用多个独立变量升级为使用一个字典来管理所有玩家分数。

原始程序为每个玩家(如Alice、Bob)定义了单独的变量,这限制了程序的扩展性。使用字典后,我们可以动态地添加任意数量的玩家。

重构步骤

  1. 创建分数字典: 删除独立的玩家变量,创建一个字典来存储所有玩家的分数。

    # 替换前:alice_score = 0; bob_score = 0
    # 替换后:
    scores = {'alice': 0, 'bob': 0}
    
  2. 更新“设置分数”逻辑: 将硬编码的变量赋值改为对字典的赋值。

    # 替换前:if name == ‘alice‘: alice_score = score
    # 替换后:
    scores[name] = score  # name是用户输入的名称
    
  3. 更新“获取分数”逻辑: 将硬编码的变量读取改为从字典中查找,并处理玩家不存在的情况。

    if name in scores:  # 先检查键是否存在
        print(scores[name])
    else:
        print(“未知玩家”)
    
  4. 重构“查找最高分”逻辑: 不再比较两个固定变量,而是遍历整个字典来寻找分数最高的玩家。这是一个常见的编程模式。

    best_score = -1
    best_player = None
    for player, score in scores.items():
        if score > best_score:
            best_score = score
            best_player = player
    print(f“获胜者是:{best_player}”)
    
  5. 处理并列最高分: 上述简单循环只找出一位获胜者。为了找出所有并列最高分的玩家,可以采用“两轮遍历”策略:第一轮找出最高分,第二轮找出所有得此分数的玩家。

    # 第一轮:找出最高分
    highest_score = max(scores.values())
    # 第二轮:找出所有得最高分的玩家
    winners = [player for player, score in scores.items() if score == highest_score]
    if len(winners) == 1:
        print(f“获胜者是:{winners[0]}”)
    else:
        winners_str = “, ”.join(winners)
        print(f“并列获胜者:{winners_str}”)
    

通过以上步骤,我们成功将一个僵化的程序转变为一个灵活、可扩展的程序,能够处理任意数量和名称的玩家。


总结

本节课中我们一起学习了Python字典。我们了解了字典是一种键值对映射的数据结构,掌握了其创建、访问、更新和删除的基本操作。通过对比列表和集合,我们明确了字典在存储需要通过特定标签(键)来快速查找数据的场景下的优势。最后,通过实战重构一个计分程序,我们亲身体验了字典如何使代码更简洁、更强大。

记住,当你需要将一组数据与特定的、有意义的标签关联起来,并需要快速根据标签查找数据时,字典通常是你的最佳选择。

015:项目2策略指南

在本教程中,我们将学习如何完成项目2(P6)中的问题4。我们将从下载数据文件开始,逐步讲解如何导入数据、处理数据、编写辅助函数,并最终使用循环和字符串操作来解决问题。本教程旨在帮助初学者理解每个步骤,并提供实用的调试策略。

下载与准备数据文件

首先,我们需要下载项目所需的数据文件。数据文件是一个CSV格式的文件,但有时浏览器会错误地将其保存为文本文件(.txt)。我们需要确保文件扩展名正确。

以下是下载和重命名文件的步骤:

  1. 在浏览器中右键点击数据文件链接,选择“链接另存为”。
  2. 将文件保存到你的工作目录(例如 lab P6 文件夹)。
  3. 如果文件被保存为 .txt 格式,你需要手动将其重命名为 .csv 格式。可以通过命令行(如PowerShell)或文件资源管理器完成此操作。

在PowerShell中,你可以使用 mv 命令来重命名文件,例如:mv Airbnb.txt Airbnb.csv

在Jupyter Notebook中导入数据

成功下载并重命名文件后,下一步是在Jupyter Notebook中导入数据。我们将使用一个辅助函数来读取CSV文件。

def process_csv(filename):
    with open(filename, 'r', encoding='utf-8') as f:
        reader = csv.reader(f)
        data = list(reader)
    header = data[0]  # 第一行是表头
    data = data[1:]   # 其余行是数据
    return header, data

# 使用函数读取数据
csv_header, csv_data = process_csv('Airbnb.csv')

运行上述代码后,csv_header 变量将包含列名列表,csv_data 变量将包含所有数据行(一个列表的列表)。你可以打印 csv_header 来查看所有列的名称。

编写 cell 辅助函数

为了更方便地从数据表中提取特定单元格的值,我们需要编写一个 cell 函数。这个函数接收行索引和列名,返回对应的值,并根据列名进行适当的数据类型转换。

def cell(row_idx, col_name):
    # 根据列名找到其在表头中的索引
    col_idx = csv_header.index(col_name)
    # 获取原始值(字符串格式)
    val = csv_data[row_idx][col_idx]

    # 如果原始值是空字符串,返回 None
    if val == "":
        return None

    # 根据列名进行类型转换
    if col_name == "price":
        return int(val)
    # 你可以根据需要添加其他列的类型转换,例如:
    # elif col_name == "review_rate_number":
    #     return float(val)
    # 对于已经是字符串的列,无需转换

    # 默认返回字符串值
    return val

这个函数是后续所有数据处理的基础。常见的错误包括忘记处理空字符串(返回 None)或遗漏最后的 return val 语句。

理解循环:遍历数据的三种方式

在处理表格数据时,我们经常需要遍历所有行。主要有三种循环方式:

  1. 直接遍历数据项(For-each循环):适用于只需要值,不需要索引的情况。

    for row in csv_data[:5]:  # 只遍历前5行作为示例
        print(row)
    
  2. 使用 range 的索引循环:这是本项目中最常用的方式,因为它提供了行索引,方便我们使用 cell 函数。

    num_rows = len(csv_data)
    for idx in range(num_rows):
        # 使用 idx 来获取特定行的数据
        room_name = cell(idx, "name")
        print(idx, room_name)
    
  3. while 循环:最灵活但需要手动管理索引。

    idx = 0
    num_rows = len(csv_data)
    while idx < num_rows:
        row = csv_data[idx]
        # 处理 row
        idx += 1  # 不要忘记递增索引
    

对于本项目,第二种方式(使用 range 的索引循环) 是最佳选择。

解决问题4:查找包含特定关键词的房间名

现在,我们应用所学知识来解决具体问题。问题4要求:查找所有房间名(name)中包含“Super Bowl”的房间名列表,且搜索不区分大小写。

我们的解决思路是:遍历所有数据行,使用 cell 函数获取每个房间名,检查小写后的房间名是否包含小写后的关键词“super bowl”,如果包含,则将该房间名加入结果列表。

以下是实现代码:

def find_rooms_with_keyword(keyword):
    result_list = []  # 初始化一个空列表来存放结果
    num_rows = len(csv_data)
    search_term = keyword.lower()  # 将搜索词转换为小写

    for idx in range(num_rows):
        # 1. 获取房间名
        room_name = cell(idx, "name")

        # 2. 跳过没有房间名的行(cell函数返回None)
        if room_name is None:
            continue

        # 3. 将房间名转换为小写,并进行包含性检查
        if search_term in room_name.lower():
            # 4. 将原始的(保持大小写的)房间名添加到结果列表
            result_list.append(room_name)

    return result_list

# 调用函数解决第4题
q4_answer = find_rooms_with_keyword("Super Bowl")
q4_answer

代码解析:

  • result_list = []:创建一个空列表,用于存储符合条件的房间名。
  • for idx in range(num_rows)::使用索引循环遍历所有数据行。
  • room_name = cell(idx, "name"):使用 cell 函数获取当前行的房间名。
  • if room_name is None: continue:这是一个关键步骤。如果 cell 函数返回 None(表示数据缺失),则跳过当前行。
  • if search_term in room_name.lower()::检查小写后的关键词是否出现在小写后的房间名中。这是实现不区分大小写搜索的方法。
  • result_list.append(room_name):将符合条件的原始房间名(保留原始大小写)添加到结果列表。
  • 最后,函数返回结果列表。在Jupyter中,将变量 q4_answer 单独放在一行,它的值会自动输出,供评分系统检查。

调试策略与代码复用

在编写代码时,采用增量开发和调试策略非常重要。

  • 增量开发:不要一次性写完所有代码。先写一两行,打印中间结果,确保符合预期后再添加新功能。例如,可以先写循环只打印前5行的房间名。
  • 使用“脚手架”打印语句:在关键位置插入 print 语句,输出变量状态,帮助定位错误。调试完成后记得移除它们。
  • 函数化以复用代码:正如我们在 find_rooms_with_keyword 函数中所做的那样,将通用逻辑封装成函数。问题5和问题6同样需要查找包含不同关键词的房间名,只需用不同的参数调用此函数即可,避免了代码重复和潜在的错误。
# 解决问题5:查找包含“dream room”的房间名
q5_answer = find_rooms_with_keyword("dream room")
q5_answer

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/de7c008bb45c415fa7f919f1c19d0448_5.png)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/de7c008bb45c415fa7f919f1c19d0448_7.png)

# 解决问题6:查找包含“free wifi”的房间名
q6_answer = find_rooms_with_keyword("free wifi")
q6_answer

总结与学习建议

本节课中我们一起学习了完成项目2问题4的完整流程。

我们首先学习了如何下载和准备CSV数据文件。接着,在Jupyter Notebook中导入数据,并编写了关键的 cell 辅助函数来提取和转换数据。我们详细分析了遍历数据表的三种循环方式,并确定了使用 range 的索引循环是本项目的最佳实践。最后,我们应用这些知识,通过编写一个可复用的函数解决了查找特定关键词房间名的问题,并介绍了有效的调试策略。

为了在课程中取得好成绩,建议你:

  1. 每日学习:跟上课程进度,理解每个概念。
  2. 提前复习薄弱环节:如果对字符串操作、循环或列表不熟悉,现在花10分钟复习,远比考试前夜临时抱佛脚有效率。
  3. 理解而非死记:掌握 cell 函数和循环模式的工作原理,你就能应对各种数据提取问题。

通过本教程,希望你能更自信地处理本项目及未来的数据编程任务。

016:嵌套字典

在本节课中,我们将深入学习字典,特别是如何将不同的数据结构组合在一起,例如将列表放入字典中,或者创建字典的列表。我们将探讨更多字典操作,并介绍嵌套数据结构的概念及其常见应用场景。


概述

上一节我们介绍了字典的基本操作,包括创建、索引和删除数据。本节中,我们将进一步探索字典的高级功能,如使用 lenin 关键字和 for 循环遍历字典,以及 keysvaluesgetpop 方法。核心内容将聚焦于嵌套数据结构,例如字典的列表或列表的字典,并演示其在数据分桶(Bucketing)中的实际应用。


更多字典操作

除了基本的创建、索引和删除,字典还支持其他有用的操作。以下是几个关键功能:

长度、成员检查和遍历

与列表类似,字典也支持 len 函数、in 关键字和 for 循环。但需要注意的是,in 关键字检查的是键(Key)而非值(Value)。

示例代码:

num_words = {0: "zero", 1: "one", 2: "two", 3: "three"}
print(len(num_words))          # 输出: 4
print(1 in num_words)          # 输出: True
print("one" in num_words)      # 输出: False
for key in num_words:
    print(key, num_words[key]) # 依次输出键值对

提取键和值

使用 keys()values() 方法可以分别获取字典中所有的键和值。这些方法返回的是特殊视图对象,可以转换为列表以便进一步操作。

示例代码:

num_words = {0: "zero", 1: "one", 2: "two", 3: "three"}
keys_list = list(num_words.keys())    # 转换为列表: [0, 1, 2, 3]
values_list = list(num_words.values()) # 转换为列表: ["zero", "one", "two", "three"]

使用默认值的 getpop

当尝试访问字典中不存在的键时,直接索引会导致 KeyErrorgetpop 方法允许指定默认值,避免程序崩溃。

示例代码:

suffix = {1: "st", 2: "nd", 3: "rd"}
print(suffix.get(4, "th"))  # 输出: "th"
print(suffix.pop(0, "th"))  # 输出: "th",字典不变


嵌套数据结构

嵌套数据结构是指在一个数据结构中包含另一个数据结构,例如列表的字典或字典的列表。这种组合方式能更灵活地组织和处理复杂数据。

字典的列表

一个常见用例是将CSV文件转换为字典的列表,其中每个字典代表一行数据,键是列标题,值是对应的数据。

示例代码:

import csv

def csv_to_dict_list(filename):
    with open(filename, 'r') as file:
        reader = csv.DictReader(file)
        return [row for row in reader]

data = csv_to_dict_list('tornadoes.csv')
print(data[0]['location'])  # 访问第一行数据的'location'字段

列表的字典(数据分桶)

数据分桶(Bucketing)是将数据按某个键(如年份)分组存储的过程。每个键对应一个列表,列表中包含所有符合该键的数据行。

示例代码:

def bucketize(data, key_column):
    buckets = {}
    for row in data:
        key = row[key_column]
        if key not in buckets:
            buckets[key] = []
        buckets[key].append(row)
    return buckets

# 假设data是字典的列表
buckets = bucketize(data, 'year')
print(buckets['2019'])  # 输出2019年的所有数据行

常见应用场景

嵌套数据结构在数据处理中非常实用,以下是一些典型应用:

数据分桶与分析

通过分桶,可以轻松对数据进行分组统计。例如,计算每年龙卷风的平均速度:

示例代码:

def average_speed(rows):
    total = 0
    for row in rows:
        total += int(row['speed'])
    return total / len(rows)

averages = {}
for year, rows in buckets.items():
    averages[year] = average_speed(rows)

# 按年份排序输出
for year in sorted(averages.keys()):
    print(f"{year}: {averages[year]}")

灵活的数据表示

使用字典的列表可以避免硬编码列索引,使代码更易读和维护。例如,直接通过列名访问数据,而无需记住列的位置。

示例代码:

# 直接通过键名访问,无需知道列索引
speed = data[0]['speed']
location = data[0]['location']


总结

本节课我们一起学习了字典的高级操作,包括使用 leninfor 循环,以及 keysvaluesgetpop 方法。重点介绍了嵌套数据结构的概念,如字典的列表和列表的字典,并演示了数据分桶的实际应用。掌握这些技巧将帮助你更有效地组织和处理复杂数据集。

017:JSON文件处理 📄

在本节课中,我们将学习JSON文件格式。这是一种类似于CSV的数据存储方式,但更适合处理非表格化的复杂数据结构,例如嵌套的字典和列表。


回顾与准备

上一节我们介绍了复杂数据结构的追踪。本节中,我们来看看如何将这类结构持久化保存到文件中。

首先,我们需要从课程网页下载相关文件,包括PDF幻灯片和练习工作表。我们将使用幻灯片中的代码片段来读写JSON文件。

在代码目录中,我们有一个模板文件。跟随课程,我们将逐步填充它。此外,我们还需要一张名为“loony_list”的图片,用于辅助理解一个复杂的数据结构练习题。请确保所有文件都在同一目录下。


第一部分:复杂数据结构追踪练习 🧩

让我们从一个练习题开始,以巩固对嵌套数据结构的理解。以下是题目中的初始代码:

webster = {
    "A": ["apple", "ant", "algorithm"],
    "B": ["bike", "debug"],
    "Z": {"name": "zebra", "kind": "mammal"}
}
loony_list = [8, 9, webster]
loony_list.append(loony_list)
webster["L"] = loony_list

def find(everything):
    final_letter = everything[2]["Z"]
    return final_letter

这个数据结构包含了字典、列表以及自引用,看起来有些复杂。我们的目标是手动追踪代码,预测特定索引操作的结果,而不是直接运行代码。

例如,要预测 loony_list[1] 的值,我们追踪 loony_list 这个列表,其索引1的位置是整数 9

以下是几个需要预测的表达式,请尝试自己推理:

  • loony_list[1]
  • webster["A"][-1]
  • final_letter["name"][-1] (注意:final_letter 仅在函数内有效)
  • loony_list[3][-1][3][-1][3][-1][0]

对于最后一个表达式,由于自引用,我们会不断循环进入同一个列表,最终索引 [0] 会指向第一个元素 8

通过这样的练习,我们可以加深对Python中列表索引和字典键值访问的理解,即使面对嵌套结构也能游刃有余。


第二部分:认识JSON格式 🔍

上一节我们练习了操作复杂的数据结构。本节中我们来看看,如何将这类结构保存到文件中。

为何需要JSON?

我们之前使用CSV文件存储“列表的列表”,这非常适合表格数据(例如带有姓名、X坐标、Y坐标的游戏玩家数据)。

但是,并非所有数据都如此规整。考虑以下数据结构:

players = {
    "Alice": {"age": 40, "scores": [10, 20, 19]},
    "Bob": {"age": 45, "scores": [15, 23, 17, 15]}
}

这是一个“字典的字典”,其中值还包含了列表。如果硬要用CSV存储,会遇到列数不一致等问题(Alice有3个分数,Bob有4个)。此时,JSON格式就成了更好的选择。

JSON格式简介

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它与Python的字典和列表结构非常相似:

  • 使用花括号 {} 表示对象(对应Python字典)。
  • 使用冒号 : 分隔键值对。
  • 使用方括号 [] 表示数组(对应Python列表)。
  • 字符串必须使用双引号 ""
  • 支持数字、布尔值(true/false)、null

上面 players 字典对应的JSON文件内容如下:

{
  "Alice": {
    "age": 40,
    "scores": [10, 20, 19]
  },
  "Bob": {
    "age": 45,
    "scores": [15, 23, 17, 15]
  }
}

JSON与Python的细微差别

需要注意以下几点:

  1. 布尔值:Python是 True/False,JSON是 true/false
  2. 空值:Python是 None,JSON是 null
  3. 字符串:JSON只允许双引号,Python单双引号皆可。
  4. 末尾逗号:Python允许列表或字典末尾有逗号,JSON不允许。
  5. 键的类型:Python字典键可以是多种不可变类型,JSON对象的键只能是字符串。

读写JSON文件

将JSON文件读入Python数据结构的解析过程,以及将Python数据结构写入JSON文件的序列化过程,都有现成的模块可以完成。

我们需要导入 json 模块,并使用以下两个函数:

读取JSON文件函数:

import json

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/c9292ef10030e2f9795ad628feb7e0b4_5.png)

def read_json_file(path):
    with open(path, 'r', encoding='utf-8') as f:
        return json.load(f)

这个函数接收文件路径,返回Python数据(可能是列表、字典等)。

写入JSON文件函数:

def write_json_file(path, data):
    with open(path, 'w', encoding='utf-8') as f:
        json.dump(data, f)

这个函数接收文件路径和要保存的Python数据,将其写入文件。


第三部分:实战演示——求和的分数记录器 🚀

上一节我们了解了JSON的理论和基本操作。本节中我们通过两个实际案例来应用这些知识。

演示1:从JSON文件读取数字并求和

目标: 编写一个程序,从命令行接收一个JSON文件名,该文件包含一个数字列表,程序计算并输出这些数字的总和。

步骤:

  1. 创建一个包含数字列表的JSON文件,例如 nums_a.json[1, 2, 3, 4]
  2. 在Python程序中,使用 sys.argv 获取命令行参数,其中第一个参数(索引1)就是文件名。
  3. 使用 read_json_file 函数读取数据。
  4. 使用Python内置的 sum() 函数计算列表总和并打印。

核心代码逻辑如下:

import sys
filename = sys.argv[1]  # 获取命令行传入的文件名
numbers = read_json_file(filename)
total = sum(numbers)
print(total)

演示2:分数记录器

目标: 编写一个程序,记录不同玩家的游戏分数。每次运行程序时,输入玩家姓名和本次得分,程序会更新该玩家的历史分数列表,并计算输出其平均分。数据在程序多次运行间持久化保存。

设计思路:

  • 数据结构: 使用一个字典,键是玩家姓名,值是该玩家的分数列表。
    scores = {
        "Alice": [10, 20, 15],
        "Bob": [12, 18]
    }
    
  • 持久化: 将这个字典保存为JSON文件(如 scores.json)。
  • 程序流程:
    1. 程序启动时,从 scores.json 读取历史数据。
    2. 从命令行参数获取本次输入的玩家姓名和分数。
    3. 检查该玩家是否已存在于字典中。如果不存在,为其创建一个空列表。
    4. 将本次分数追加到该玩家的分数列表中。
    5. 计算该玩家当前的平均分(总和除以分数个数)并打印。
    6. 将更新后的字典写回 scores.json 文件。

关键代码片段包括命令行参数处理和字典的惰性初始化:

import sys
# 读取历史数据
scores = read_json_file(“scores.json”)
# 获取本次输入
name = sys.argv[1]
score = int(sys.argv[2])
# 如果玩家不存在,初始化其分数列表
if name not in scores:
    scores[name] = []
# 追加分数并计算平均分
scores[name].append(score)
average = sum(scores[name]) / len(scores[name])
print(f”{name}’s average: {average}”)
# 保存数据
write_json_file(“scores.json”, scores)

通过这个例子,我们综合运用了JSON读写、命令行参数处理以及字典的复杂操作。


更多练习建议 💪

为了熟练掌握JSON文件处理,建议尝试完成以下扩展练习:

  1. 球员信息查询器: 基于一个存储球员信息(ID、姓名、年龄等)的JSON文件,编写程序通过命令行输入球员ID和字段名,查询并输出对应信息。
  2. 质数缓存计算器: 编写程序计算某数字范围内的所有质数。将已发现的质数列表保存为JSON文件。再次计算时,先读取文件中的历史质数,只计算新的部分,从而提升效率。
  3. 自动补全大写转换器: 编写一个“喊话”程序,能将输入短语转为大写并记录。当输入以星号*结尾时,程序能根据历史记录提供可能的补全选项。

完成这些练习将极大地巩固你对JSON和Python数据管理的理解。


本节课中我们一起学习了JSON文件格式,理解了它相对于CSV在处理嵌套数据结构时的优势,掌握了使用 json 模块读写JSON文件的方法,并通过实战演示将数据持久化与命令行程序结合起来。这些技能对于管理复杂程序的数据至关重要。

018:对象与引用 🧠

在本节课中,我们将要学习Python中对象与引用的核心概念。我们将探讨Python的内存模型,理解变量如何引用数据,并介绍三种新的数据类型:元组(tuple)、具名元组(namedtuple)和记录类(recordclass)。掌握这些知识将帮助我们编写更健壮、更易读且更高效的代码。


内存模型:栈与堆 🧱

上一节我们介绍了Python中变量与数据的基本关系。本节中,我们来看看Python内部是如何组织内存的。

Python内存主要分为两个部分:

  • :用于存储变量。变量本质上是引用,它们指向堆中的数据。当函数被调用时,其局部变量会被“压入”栈中;函数结束时,这些变量会被“弹出”并销毁。
  • :用于存储对象,即实际的数据,如数字、字符串、列表、字典等。对象可以独立于变量存在,只要还有引用指向它,它就会一直存在。当没有任何引用指向一个对象时,Python的垃圾回收机制会自动将其从内存中清除。

一个变量(引用)指向一个对象。重要的是,一个对象可以被多个引用同时指向。例如:

a = [1, 2, 3]
b = a  # b 和 a 现在指向同一个列表对象

在这个例子中,ab 是两个不同的引用(变量),但它们都指向堆中同一个列表对象 [1, 2, 3]


可变性与不可变性 ⚖️

理解可变性(mutability)与不可变性(immutability)是避免常见错误的关键。

  • 不可变对象:一旦创建,其内容就不能被修改。尝试修改会创建一个新对象。例如:整数(int)、浮点数(float)、字符串(str)、元组(tuple)
  • 可变对象:创建后,其内容可以被修改。例如:列表(list)、字典(dict)、集合(set)、记录类(recordclass)

区分“修改变量”和“修改对象”非常重要:

  • 修改变量:使用赋值操作符 =。这会改变变量(引用)指向的对象。
    x = 5
    x = 10  # 变量 x 现在指向新的整数对象 10
    
  • 修改对象:使用对象的方法或索引操作来改变对象内部的数据。
    my_list = [1, 2]
    my_list.append(3)  # 修改了 my_list 指向的列表对象,现在它是 [1, 2, 3]
    my_list[0] = 99    # 修改了列表的第一个元素
    

一个关键例子

s = “hello”
l = [‘a‘, ‘b‘, ‘c‘]

# 以下操作总会失败,因为字符串是不可变的
# s[-1] = ‘.‘  # TypeError: ‘str‘ object does not support item assignment

# 以下操作可能失败,取决于字符串长度和列表长度
# 如果 len(s) 超出了列表 l 的索引范围,就会失败
# l[len(s)] = ‘.‘  # IndexError: list assignment index out of range

新数据类型1:元组(Tuple) 📦

元组是一种与列表非常相似的序列类型,但它是不可变的。

创建元组:使用圆括号 ()

my_tuple = (200, 300, 400)

与列表的对比

my_list = [200, 300, 400]
my_tuple = (200, 300, 400)

# 都可以通过索引访问
x = my_list[1]   # x = 300
y = my_tuple[1]  # y = 300

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/48fd548bbcb689acca24e4e314236c72_7.png)

# 列表可以修改
my_list[0] = 99   # 成功,my_list 变为 [99, 300, 400]

# 元组不能修改
# my_tuple[0] = 99  # TypeError: ‘tuple‘ object does not support item assignment

为什么使用元组?

  1. 数据安全:防止意外修改不应改变的数据(如历史记录、配置常量)。
  2. 字典键:由于字典的键必须是不可变的,因此元组可以作为字典的键,而列表不能
    # 列表作为键会报错
    # location_dict = {[2, 4]: “Psychology Building“}  # TypeError: unhashable type: ‘list‘
    
    # 元组作为键是可行的
    location_dict = {(2, 4): “Psychology Building“}
    print(location_dict[(2, 4)])  # 输出:Psychology Building
    

注意:创建单个元素的元组时,必须在元素后加一个逗号,以区分于普通的括号运算。

single_tuple = (42,)   # 这是一个元组
not_a_tuple = (42)     # 这只是一个整数 42

新数据类型2:具名元组(Namedtuple) 🏷️

具名元组是 collections 模块提供的一个工厂函数,用于创建自定义的、不可变的数据类型。它结合了元组的不可变性和字典的“键值对”可读性。

创建和使用具名元组

from collections import namedtuple

# 1. 创建一个新的类型,名为 ‘Person‘,它有三个属性
Person = namedtuple(‘Person‘, [‘first_name‘, ‘last_name‘, ‘age‘])

# 2. 创建这个类型的对象(实例)
alice = Person(first_name=‘Alice‘, last_name=‘Anderson‘, age=30)
bob = Person(‘Bob‘, ‘Baker‘, 25)  # 也可以使用位置参数

# 3. 访问属性:使用点号,而不是索引或键,代码可读性更高
print(f“Hello, {alice.first_name} {alice.last_name}“)  # 输出:Hello, Alice Anderson
print(bob.age)  # 输出:25

具名元组的优点

  • 代码清晰:通过属性名访问数据,无需记住数据在元组中的位置(如 person[0] 是名字吗?)。
  • 轻量级:比完整的类更简洁。
  • 不可变:数据安全,且可作为字典的键。

新数据类型3:记录类(Recordclass) 📝

记录类是 recordclass 库提供的一个数据结构,它是具名元组的可变版本。你需要先安装它:pip install recordclass

创建和使用记录类

from recordclass import recordclass

# 1. 创建一个新的可变类型,名为 ‘Person‘
Person = recordclass(‘Person‘, [‘first_name‘, ‘last_name‘, ‘age‘])

# 2. 创建对象
alice = Person(first_name=‘Alice‘, last_name=‘Anderson‘, age=30)

# 3. 访问属性(和具名元组一样)
print(alice.age)  # 输出:30

# 4. 关键区别:可以修改属性!
alice.age = 31  # Alice 过生日了
print(alice.age)  # 输出:31

记录类的适用场景:当你需要类似具名元组的结构清晰的数据类型,但又需要能够更新其中的数据时。


引用与“is”运算符 🔗

由于多个引用可以指向同一个对象,Python 提供了 is 运算符来检查两个引用是否指向内存中的同一个对象。这与 == 运算符不同,== 检查的是两个对象的是否相等。

list_a = [1, 2, 3]
list_b = [1, 2, 3]  # 创建了一个值相同的新列表对象
list_c = list_a      # list_c 指向 list_a 所指向的同一个对象

print(list_a == list_b)  # True,值相等
print(list_a is list_b)  # False,不是同一个对象

print(list_a is list_c)  # True,指向同一个对象

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/48fd548bbcb689acca24e4e314236c72_9.png)

# 修改 list_a 会影响 list_c
list_a.append(4)
print(list_c)  # 输出:[1, 2, 3, 4]

is 的常见用途:检查一个变量是否为 None

if my_var is None:
    # 做点什么

注意:Python 会对一些小整数和短字符串进行内部优化(驻留),使得它们可能是同一个对象,但这不应作为依赖的逻辑。


函数参数传递与副作用 ⚠️

在Python中,函数参数的传递本质上是传递引用。这意味着,如果你向函数传递了一个可变对象(如列表),并在函数内部修改了它,那么这个修改会影响到函数外部的原始对象。

def add_exclamation(items):
    items.append(“!“)  # 这会修改传入的列表对象本身

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/48fd548bbcb689acca24e4e314236c72_11.png)

my_words = [“hello“, “world“]
add_exclamation(my_words)
print(my_words)  # 输出:[‘hello‘, ‘world‘, ‘!‘] 原始列表被改变了!

这种行为有时是我们期望的(“中心化更新”),但有时会导致意想不到的副作用和难以调试的bug。

如何避免意外的副作用?

  • 在函数内部对传入的可变对象进行复制
    def safe_add_exclamation(items):
        new_list = items.copy()  # 或者 list(items)
        new_list.append(“!“)
        return new_list
    
  • 明确文档说明函数是否会修改传入的参数。

总结 🎯

本节课中我们一起学习了:

  1. Python内存模型:理解了栈(存储变量/引用)和堆(存储对象)的区别,以及引用与对象的关系。
  2. 可变性与不可变性:学会了区分哪些数据类型可以原地修改,哪些不能。这是写出正确代码的基础。
  3. 三种新的数据结构
    • 元组(tuple):不可变的序列,用于保护数据和作为字典键。
    • 具名元组(namedtuple):创建具有命名属性的不可变数据类型,提升代码可读性。
    • 记录类(recordclass):创建具有命名属性的可变数据类型,兼具清晰的结构和修改的灵活性。
  4. 引用比较:掌握了 is== 的区别,用于判断两个变量是否指向同一对象。
  5. 函数参数传递的副作用:认识到向函数传递可变对象时,在函数内部修改它会影响到外部原始数据,并学会了如何谨慎处理。

理解对象和引用是成为高级Python程序员的关键一步。它帮助你预测代码行为,避免隐蔽的bug,并设计出更优雅的数据结构。

019:对象、引用与数据复制 🧠

在本节课中,我们将深入复习Python中的对象与引用概念,这是理解Python数据模型的核心。随后,我们将探讨如何复制数据结构,包括浅拷贝与深拷贝的区别及其应用场景。

概述:对象与引用的核心概念

在Python中,一切皆为对象。变量并不直接“存储”对象,而是存储指向对象的引用。理解这一点是避免常见编程错误的关键。

变量与对象的关系

变量包含对对象的引用。例如,当执行 x = [1, 2, 3] 时,变量 x 存储的是一个指向列表对象 [1, 2, 3] 的引用(可以想象为一个箭头)。

可变与不可变对象

  • 可变对象:创建后内容可以改变,例如列表(list)、字典(dict)、记录类(recordclass)。
  • 不可变对象:创建后内容不可改变,例如整数(int)、字符串(str)、元组(tuple)。

对象存储位置

  • 变量:存在于栈帧中,生命周期与函数调用相关。
  • 对象:存在于中,可以被多个变量引用。

复习练习:测试你的理解

以下是几个快速问题,帮助你巩固对核心概念的理解。

问题A:变量真正包含什么?

  1. 对象本身。
  2. 对对象的引用。

问题B:如何标注类型层次结构中的空白?
(假设图示左边是基类,右边是子类)

  1. 左边填 namedtuple,右边填 tuple
  2. 左边填 tuple,右边填 namedtuple

问题C:以下哪项实际存在于栈帧中?

  1. 对象。
  2. 变量。

答案与解析:

  • 问题A:答案是 2。变量存储的是指向对象的引用(地址)。
  • 问题B:答案是 1namedtupletuple 的子类,它允许我们创建带有命名字段的自定义类型。
  • 问题C:答案是 2。变量存在于栈帧中,而对象存在于堆内存中。

数据结构的可变性与特性

上一节我们回顾了基本概念,本节中我们来看看不同数据结构的特性对比。以下是常见数据结构的属性比较表:

特性 列表 (list) 元组 (tuple) 命名元组 (namedtuple) 记录类 (recordclass)
是否可变 True False False True
是否预安装 True True True (需 import) False (需 pip install)
是否内置 True True False False
是否可创建新类型 False False True True
是否可命名字段 False False True True

关键点解析:

  • 预安装:指是否随Python标准库一起提供,无需额外安装。
  • 内置:指是否为语言关键字或可直接使用的内置类型,无需导入。
  • 创建新类型:指能否作为“工厂”定义全新的数据类型(如 PersonHurricaneData)。
  • 命名字段:指能否为数据的每个部分赋予一个名称(如 .name),而非仅使用数字索引。

赋值与参数传递的本质

理解了数据结构特性后,我们需要明确赋值操作的真正行为。一个常见的误解是赋值会创建数据的独立副本。

引用赋值

代码 y = x 不会创建 x 所指对象的新副本。它只是让变量 y 指向 x 当前所指的同一个对象

x = [‘A‘, ‘B‘, ‘C‘]  # 创建一个列表对象,x指向它
y = x                 # y 指向 x 所指的同一个列表对象
y.append(‘D‘)         # 通过 y 修改列表
print(x)              # 输出:[‘A‘, ‘B‘, ‘C‘, ‘D‘],x“看到”了变化

图示说明:
执行 y = x 后,内存中存在一个列表对象,但有两个变量(xy)引用它。通过任何一个变量修改对象,另一个变量访问时都会看到修改后的结果。

函数参数传递

函数参数传递遵循相同的引用赋值规则。

def append_number(some_list):
    some_list.append(42)

my_list = [1, 2, 3]
append_number(my_list)
print(my_list)  # 输出:[1, 2, 3, 42]

过程解析:

  1. 调用 append_number(my_list) 时,形参 some_list 被赋值为实参 my_list 的引用。
  2. 因此,some_listmy_list 指向同一个列表对象。
  3. 在函数内部通过 some_list 修改列表,函数外部的 my_list 也会反映这个变化。

重要结论: 在Python中,赋值 (=) 和参数传递创建的都是引用副本,而非对象副本。

对象的可见性与创建

既然赋值不创建新对象,那么何时才会创建新对象呢?本节我们来看看哪些操作会真正在堆上创建新对象。

会创建新对象的操作(产生独立数据):

  • 字面量创建[], {}, (), "", 123
  • 构造函数list(), dict(), tuple()
  • 切片操作list_copy = original_list[:]
  • 连接操作new_list = list1 + list2
  • 某些方法返回新对象:如 some_string.upper()

不会创建新对象的操作(操作现有对象):

  • 引用赋值y = x
  • 原地修改方法list.append(), list.pop(), dict.update()

示例分析:

x = [‘A‘, ‘A‘, ‘B‘, ‘B‘, ‘B‘]
y = x[:]        # 切片操作:创建新列表对象,y指向它
x.pop(0)        # 通过 x 修改原列表
print(len(y))   # 输出:5,y 所指的列表未受影响

本例中,切片 x[:] 创建了一个包含相同元素的新列表对象,因此后续对 x 的修改不会影响 y

动手练习:绘制引用图

理论学习后,最好的巩固方式是动手画图。以下是几个练习题,请尝试在纸上画出变量、引用和对象的关系图。

练习题2(示例):

x = [1, 2, 3]
y = [1, 2, 3]
z = x
z.append(4)

答案图示:

  • x --> 列表对象 [1, 2, 3, 4]
  • y --> 另一个列表对象 [1, 2, 3]
  • z --> 与 x 指向同一个列表对象 [1, 2, 3, 4]

练习题3:

nums1 = [1, 2]
nums2 = nums1
x = nums2.pop()

关键点: nums2.pop() 返回被移除的元素 2 并赋值给 x,同时原地修改 nums1nums2 共同指向的列表。

练习题4:

x = [1, 2]
y = [3]
z = x + y
y.append(4)

关键点: x + y 是连接操作,会创建包含 [1, 2, 3]新列表。后续对 y 的修改不影响 z

练习题5(涉及字典):

people = {‘Alice‘: 30, ‘Bob‘: 25}
x = people
y = people[‘Bob‘]
x[‘Alice‘] = 31
y = 26

关键点: 字典的值(如整数25)也是独立对象。y = people[‘Bob‘]y 引用整数 25。后来 y = 26 是让 y 引用一个新的整数对象 26,这与字典中的值 25 无关。

练习题6(涉及函数调用):

def f(items):
    return items.pop(0)

nums = [1, 2, 3]
nums.append(f(nums))

逐步分析:

  1. 调用 f(nums),形参 items 引用实参 nums 所指的同一列表 [1, 2, 3]
  2. items.pop(0) 移除并返回列表的第一个元素 1,列表变为 [2, 3]
  3. 函数返回 1
  4. nums.append(1)1 追加到列表末尾,列表最终变为 [2, 3, 1]

记录类(Record Class)示例

我们之前介绍了 namedtuple,现在看看它的可变版本——记录类。它结合了定义新类型和可变性的优点。

from recordclass import recordclass

# 1. 创建新的数据类型 Person
Person = recordclass(‘Person‘, [‘name‘, ‘score‘, ‘age‘])

# 2. 创建该类型的对象(实例)
alice = Person(‘Alice‘, 10, 30)
bob = Person(‘Bob‘, 25, 28)

# 3. 将对象放入其他数据结构
team = [alice, bob]  # 列表存储的是对象的引用
players = {‘A‘: alice, ‘B‘: bob}  # 字典值存储的也是对象的引用

# 4. 通过任一引用修改对象
alice.score += 5
print(team[0].score)      # 输出:15
print(players[‘A‘].score) # 输出:15

核心概念: teamplayers 中存储的并非 alice 变量本身,而是该变量所引用的 Person 对象的引用。因此,通过 aliceteam[0]players[‘A‘] 中任何一个访问并修改对象,所有其他引用都会看到变化。

对象的复用(驻留)

Python有时会出于性能考虑,让不同的引用指向同一个不可变对象,这称为“驻留”。

# 小整数通常会被驻留
x = 2 ** 4  # 16
y = 2 ** 4  # 16
z = y
print(x is y)  # 可能输出 True,因为16是小整数,Python复用对象

# 大整数可能不会驻留
x = 2 ** 100
y = 2 ** 100
z = y
print(x == y)  # True,值相等
print(x is y)  # False,可能是两个不同的对象(取决于Python实现和优化)

注意: 驻留是解释器的内部优化行为,不应在编程中依赖 is 进行值比较,而应始终使用 ==

数据复制:浅拷贝与深拷贝

复习完对象与引用后,我们进入本节课的新主题:如何真正复制数据。根据不同的需求,我们有三种主要的复制方式。

1. 引用复制(Reference Copy)

即普通的赋值操作,只复制引用,不复制对象。这是最快但“最危险”的方式,因为所有引用共享同一份数据。

import copy
original = [{‘name‘: ‘A‘, ‘score‘: 88}, {‘name‘: ‘B‘, ‘score‘: 111}]
reference_copy = original  # 仅复制引用

2. 浅拷贝(Shallow Copy)

创建新对象,但仅复制第一层引用。如果元素是可变对象(如列表中的字典),则拷贝的是这些元素的引用,而非元素本身。

import copy
original = [{‘name‘: ‘A‘, ‘score‘: 88}, {‘name‘: ‘B‘, ‘score‘: 111}]
shallow_copy = copy.copy(original)  # 或 original[:] 对于列表
# shallow_copy 是一个新列表,但它的两个元素指向与原列表相同的两个字典

3. 深拷贝(Deep Copy)

创建新对象,并递归地复制所有嵌套的可变对象。生成一个完全独立的副本。

import copy
original = [{‘name‘: ‘A‘, ‘score‘: 88}, {‘name‘: ‘B‘, ‘score‘: 111}]
deep_copy = copy.deepcopy(original)
# deep_copy 是一个新列表,它的两个元素也是全新的字典对象

深度(Depth)概念: 指从顶层变量访问到目标数据需要经过的引用层级。深拷贝会复制所有深度的数据。

如何实现自定义深度的拷贝?

浅拷贝只复制第一层,深拷贝复制所有层。如果想精确复制到第N层,可以组合使用浅拷贝和循环。

import copy

original = [{‘name‘: ‘A‘, ‘score‘: 88}, {‘name‘: ‘B‘, ‘score‘: 111}]

# 目标:复制列表(第一层)和其中的字典(第二层),但不复制字典内的字符串(第三层,不可变,无需复制)
# 这相当于“深度为2”的拷贝

# 1. 浅拷贝第一层(列表)
custom_copy = copy.copy(original)

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/02a9bfd4fcc8929b9a0331d228f60aa1_23.png)

# 2. 遍历列表,深拷贝第二层(每个字典)
for i in range(len(original)):
    custom_copy[i] = copy.deepcopy(original[i])

经过上述操作,custom_copy 是一个新列表,其中的每个字典也是新字典,但字典内的字符串(如 ‘A‘)可能仍与原始对象共享(因为字符串不可变,共享是安全且高效的)。

拷贝方式的选择与应用场景

选择哪种拷贝方式取决于你的具体操作和对数据独立性的要求。

场景一:仅读取数据 -> 使用引用复制

任务: 查找最高分,不修改数据。

def max_score(people_list):
    highest = None
    for person in people_list:  # people_list 是原始数据的引用
        if highest is None or person[‘score‘] > highest:
            highest = person[‘score‘]
    return highest

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/02a9bfd4fcc8929b9a0331d228f60aa1_27.png)

players = [{‘name‘: ‘A‘, ‘score‘: 88}, ...]
highest = max_score(players)  # 传递引用,高效安全

理由: 函数只读取数据,不修改。引用复制速度快,内存占用低。

场景二:需排序或临时重排 -> 使用浅拷贝

任务: 计算中位数分数,需要排序,但想保持原列表顺序。

def median_score(people_list):
    import copy
    people_sorted = copy.copy(people_list)  # 浅拷贝:新列表,旧字典引用
    people_sorted.sort(key=lambda p: p[‘score‘])  # 排序新列表,不影响原列表
    middle_index = len(people_sorted) // 2
    return people_sorted[middle_index][‘score‘]

理由: 我们需要改变列表元素的顺序,但不改变字典内容。浅拷贝创建了新列表,排序操作不会影响原始列表的顺序。

场景三:需保存历史快照并独立修改 -> 使用深拷贝

任务: 保存游戏上半场分数快照,然后继续比赛更新分数。

import copy

# 上半场结束时的数据
players_first_half = [{‘name‘: ‘A‘, ‘score‘: 88}, ...]

# 深拷贝保存快照
snapshot = copy.deepcopy(players_first_half)

# 继续比赛,更新分数
players_first_half[0][‘score‘] += 10  # A球员在下半场得了10分

# 分析上下半场表现
print(f“Player A‘s score changed by: {players_first_half[0][‘score‘] - snapshot[0][‘score‘]}“)
# 输出:Player A‘s score changed by: 10

理由: 我们需要一份完全独立的数据副本。后续对比赛数据的任何修改都不应影响已保存的快照,只有深拷贝能保证这种独立性。

总结

本节课中我们一起学习了:

  1. 对象与引用:Python变量存储的是对象的引用(指针)。赋值和参数传递传递的是引用副本。
  2. 可变与不可变:理解哪些类型可以原地修改,哪些不能,这对理解程序行为至关重要。
  3. 可视化思维:通过画图(变量框、引用箭头、对象圈)来理解复杂的数据关系。
  4. 数据复制三剑客
    • 引用复制 (=): 共享数据,修改互通。
    • 浅拷贝 (copy.copy()list[:]): 复制容器,但共享内部元素。适用于需要独立容器结构但可共享内部数据的场景。
    • 深拷贝 (copy.deepcopy()): 完全独立克隆。适用于需要完全隔离数据版本的场景。
  5. 如何选择:根据是否修改数据、修改哪一层数据以及对性能的要求,选择合适的拷贝策略。

掌握这些概念是成为熟练Python程序员的关键一步,它能帮助你避免许多难以调试的共享数据错误,并写出更高效、更可靠的代码。

020:递归

在本节课中,我们将要学习递归这一核心编程概念。递归是指一个事物直接或间接地引用自身。我们将探讨递归的定义、递归数据结构,并重点学习如何编写递归函数来解决问题。


课程回顾与定位

上一节我们讨论了对象与引用。本节中,我们来看看递归。

在课程的前五周,我们学习了编程基础,包括控制流(如 if 语句、whilefor 循环)。第二部分是关于数据结构,我们介绍了列表和字典,以及集合和元组。之后,我们学习了数据文件(CSV和JSON格式)以及对象与引用的概念。接下来,我们将学习三个关于函数的专题:今天的递归、周一的生成器以及周三的“函数即对象”。这些是考试的重点内容。


什么是递归?

递归的核心是自我引用。递归定义在字典中会包含被定义的词本身(这通常不是好做法)。递归数据结构包含相同类型的其他数据结构(例如列表的列表)。递归函数则是会调用自身的函数。

递归可以非常强大,但理解其工作原理需要一些技巧。


复制操作回顾

在深入递归之前,我们先快速回顾一下复制操作,这对于理解递归函数中的数据传递很重要。

以下是三种复制方式的示例:

import copy

x = [[[‘Hello‘, ‘World‘]]]
  • 引用复制:创建新变量,指向同一对象。
    y = x  # 不创建新对象
    
  • 深拷贝:创建全新副本,递归复制所有嵌套对象。
    y = copy.deepcopy(x)  # 创建所有嵌套对象的新副本
    
  • 浅拷贝:仅复制第一层,嵌套对象仍被共享。
    y = copy.copy(x)  # 仅创建最外层列表的新副本
    

浅拷贝在某些场景下有用,例如当你想重新排列列表但保持内部字符串不变时。


递归定义与结构

递归定义在数学和编程中很常见,但需要有意义,避免无限循环。

示例:正偶数定义

  • 基础情况:2是正偶数。
  • 递归情况:如果 x 是正偶数,那么 x + 2 也是正偶数。

递归数据结构示例
列表中可以包含其他列表,形成递归结构。

rows = [[‘A‘, [1]], [‘B‘, [3, 4, 5]], [‘C‘, [7, 8]]]

自然界和计算机系统中也存在递归结构:

  • :树枝可以分出更多树枝(递归情况),也可以终止于树叶(基础情况)。
  • 文件系统:目录可以包含文件和更多目录。
  • JSON格式:字典中的值可以是另一个字典。

关键点:递归允许结构任意大,但必须是有限的,这样才能解决问题。我们需要一个基础情况来终止递归过程。


递归函数

递归函数是直接或间接调用自身的函数。

直接递归示例

def f():
    # 一些代码
    f()  # 调用自身
    # 更多代码

间接递归示例

def g():
    # 一些代码
    h()

def h():
    # 一些代码
    g()

为什么使用递归?

  1. 处理大小未知的数据集。
  2. 将大问题分解为更小的相似问题。
  3. 递归和迭代在能力上等价,但某些问题用递归解决更直观。

编写递归函数:四步法

我们将以计算阶乘为例,演示编写递归函数的系统方法。

1. 列举示例
从简单情况开始:

  • 1! = 1
  • 2! = 1 × 2
  • 3! = 1 × 2 × 3
  • 4! = 1 × 2 × 3 × 4

2. 寻找自引用模式
观察规律:

  • 4! = 3! × 4
  • 3! = 2! × 3
  • 2! = 1! × 2

3. 归纳递归定义

  • 基础情况1! = 1
  • 递归情况n! = (n-1)! × n (当 n > 1 时)

4. 转化为Python代码

def factorial(n):
    if n == 1:          # 基础情况
        return 1
    else:               # 递归情况
        p = factorial(n - 1)  # 函数调用自身,问题规模减小
        return p * n

递归调用必须向基础情况推进(此处 n 不断减小至1)。


递归执行与调用栈

当递归函数调用自身时,每次调用都会创建一个新的栈帧,用于存储该次调用的参数和局部变量。栈帧遵循后进先出(LIFO)原则。

factorial(3) 为例:

  1. 调用 factorial(3),创建栈帧 n=3
  2. 执行 p = factorial(2),创建新栈帧 n=2,暂停当前帧。
  3. 执行 p = factorial(1),创建新栈帧 n=1,暂停前一帧。
  4. factorial(1) 满足基础情况,返回 1,其栈帧被销毁。
  5. 控制权回到 factorial(2) 的栈帧,计算 p * n = 1 * 2 = 2,返回 2,栈帧销毁。
  6. 控制权回到 factorial(3) 的栈帧,计算 p * n = 2 * 3 = 6,返回 6,栈帧销毁。

常见递归错误

  1. 缺少基础情况或基础情况无法到达:导致无限递归,最终耗尽内存(栈溢出)。
    def factorial_no_base(n):
        # 缺少 if n == 1 的判断
        return n * factorial_no_base(n - 1)  # 无限调用!
    
  2. 递归未向基础情况推进:例如对负数调用阶乘函数,n 会越来越小,永远达不到1。

重要建议先编写基础情况,确保递归有终止条件。


实战示例:美化打印嵌套列表

我们将编写一个 pretty_print 函数,以清晰的缩进格式打印任意深度的嵌套列表。

目标输出格式

* A
*   * 1
*   * 2
* B
*   * 4
*       * i
*       * ii
*       * iii
*       * iv

代码实现与讲解

def pretty_print(items, indent=0):
    """
    递归地打印嵌套列表,每层增加缩进。
    items: 要打印的列表(可能包含嵌套列表)。
    indent: 当前缩进级别(空格数)。
    """
    for item in items:
        if isinstance(item, list):
            # 递归情况:遇到子列表,增加缩进后递归调用
            pretty_print(item, indent + 2)
        else:
            # 基础情况:打印非列表项,加上当前缩进和星号
            spaces = ‘ ‘ * indent
            print(f‘{spaces}* {item}‘)

# 测试数据
data = [
    ‘A‘,
    [1, 2],
    ‘B‘,
    [4, [‘i‘, ‘ii‘, ‘iii‘, ‘iv‘]]
]

pretty_print(data)

代码解析

  • indent=0 是默认参数,首次调用时缩进为0。
  • isinstance(item, list) 检查当前项是否为列表。
  • 基础情况:如果是字符串等非列表项,直接按格式打印。
  • 递归情况:如果是列表,则以更大的缩进(indent + 2)递归调用自身来处理这个子列表。
  • 递归确保了无论列表嵌套多深,都能正确缩进打印。

练习:递归列表搜索

尝试编写一个函数 contains(number, data_structure),检查给定的数字是否存在于一个任意深度的嵌套列表结构中。

思路提示

  1. 遍历当前列表的每个元素。
  2. 基础情况:如果元素等于目标数字,返回 True
  3. 递归情况:如果元素是子列表,递归调用 contains 在该子列表中搜索。
  4. 如果遍历完所有元素都没找到,返回 False


总结

本节课中我们一起学习了递归的核心概念。我们了解了递归的定义和递归数据结构,并通过四步法系统地学习了如何编写递归函数。我们以阶乘计算为例,剖析了递归的执行过程与调用栈的工作原理,并指出了常见的错误。最后,我们通过“美化打印嵌套列表”的实战示例,巩固了递归函数的编写技巧。递归是一种强大的工具,它将复杂问题分解为相似的子问题,是处理嵌套或分层数据的有效方法。

021:函数引用

在本节课中,我们将要学习函数作为对象的概念。我们将探讨如何将函数赋值给变量、将函数作为参数传递给其他函数,以及如何将函数存储在列表和字典中。这些高级技巧能够极大地提升代码的灵活性和效率。

概述

在Python中,函数不仅仅是可执行的代码块,它们本身也是对象。这意味着我们可以像处理其他数据(如整数、字符串、列表)一样处理函数。理解这一点是掌握Python高级编程技巧的关键。

函数作为对象

上一节我们介绍了函数的基本概念,本节中我们来看看函数作为对象的含义。这意味着变量可以引用函数对象,就像它们引用列表或字符串一样。

变量引用函数

我们可以创建一个变量,让它指向一个函数。这类似于让一个变量指向一个列表。

def say_hi():
    return "hi"

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/4f3b3f0f5776bb4f233c8e51bcbd293a_1.png)

F = say_hi  # 变量F现在引用了函数say_hi
G = F       # 变量G也引用了同一个函数对象

在这段代码中,F = say_hi 这一行并没有调用函数,而是创建了一个名为 F 的变量,它指向 say_hi 这个函数对象。G = F 则让 G 也指向同一个函数对象。FG 是同一个函数对象的别名。

调用引用的函数

当我们想执行这个函数时,需要使用括号 ()

z = F()  # 调用F引用的函数,返回值"hi"被赋给z
z = G()  # 同样,调用G引用的函数

F()G() 都会执行相同的函数代码并返回结果。关键在于,带括号是调用函数,不带括号是引用函数对象本身。

将函数放入数据结构

既然函数是对象,我们就可以将它们放入列表或字典等数据结构中。

以下是创建函数列表的示例:

def say_hi():
    print("Hello there")

def say_goodbye():
    print("Wash your hands and stay well")

function_list = [say_hi, say_hi, say_goodbye, say_goodbye]

在这个列表中,我们存储了对函数对象的引用。我们可以遍历这个列表并调用其中的每个函数。

for func in function_list:
    func()  # 依次调用列表中的每个函数

这种方法允许我们动态地管理和调用一组函数。

函数作为参数

参数本质上是特殊的变量,因此我们也可以将函数作为参数传递给另一个函数。这为实现通用性强的代码提供了可能。

创建通用函数

假设我们想多次调用某个函数,可以编写一个通用函数来处理这个任务。

def call_n_times(func, n):
    """调用传入的函数n次"""
    for i in range(n):
        func()

# 使用示例
call_n_times(say_hi, 2)
call_n_times(say_goodbye, 3)

在这里,call_n_times 函数接受一个函数对象 func 和一个整数 n 作为参数。它会在循环中调用 funcn 次。通过传递不同的函数,我们可以复用同一段逻辑。

实用案例:处理列表中的每个元素

一个常见的编程任务是:对列表中的每个元素应用某个转换函数。我们可以编写一个通用函数来完成这个任务。

实现 apply_to_each 函数

def apply_to_each(input_list, func):
    """对输入列表中的每个元素应用函数func,并返回新列表"""
    output_list = []
    for value in input_list:
        transformed_value = func(value)  # 调用传入的函数处理每个元素
        output_list.append(transformed_value)
    return output_list

这个函数遍历 input_list,对每个元素调用参数 func 指定的函数,并将结果收集到 output_list 中返回。

应用示例

我们可以用这个通用函数处理各种数据。

  1. 将字符串列表转换为整数列表:

    str_list = ["1", "234", "56"]
    int_list = apply_to_each(str_list, int)  # 使用内置的int函数
    print(int_list)  # 输出: [1, 234, 56]
    
  2. 移除字符串中的美元符号:

    def strip_dollar(s):
        if s.startswith("$"):
            return s[1:]  # 返回去掉第一个字符($)的字符串
        else:
            return s
    
    money_list = ["$123", "$456", "789"]
    cleaned_list = apply_to_each(money_list, strip_dollar)
    print(cleaned_list)  # 输出: ['123', '456', '789']
    
  3. 将字符串列表转换为大写:

    lower_list = ["aaa", "bbb", "ccc"]
    # 注意:需要使用 str.upper,而不是直接写 upper
    upper_list = apply_to_each(lower_list, str.upper)
    print(upper_list)  # 输出: ['AAA', 'BBB', 'CCC']
    

使用内置的 map 函数

Python 内置了 map 函数,其功能与我们的 apply_to_each 类似,但返回一个“生成器”。我们可以用 list() 将其转换为列表。

str_list = ["1", "234", "56"]
int_list = list(map(int, str_list))
print(int_list)  # 输出: [1, 234, 56]

map(func, iterable) 会对可迭代对象中的每个元素应用函数 func

实用案例:自定义排序

列表的 .sort() 方法和内置的 sorted() 函数都接受一个 key 参数。这个参数需要传入一个函数,该函数用于从每个元素中提取用于比较的“键”。

按特定规则排序元组列表

假设我们有一个(名, 姓)的元组列表,想按姓氏排序。

names = [("Catherine", "Baker"), ("Alice", "Clark"), ("Bob", "Adams")]

def get_last_name(name_tuple):
    return name_tuple[1]  # 返回姓氏

# 按姓氏排序
sorted_by_last = sorted(names, key=get_last_name)
print(sorted_by_last)  # 输出: [('Bob', 'Adams'), ('Catherine', 'Baker'), ('Alice', 'Clark')]

sorted() 函数会为列表中的每个元素调用 key 函数(这里是 get_last_name),然后用返回的值进行排序比较。

按字典中的值排序

对于更复杂的数据结构,如字典列表,同样适用。

hurricanes = [
    {"name": "A", "year": 2000, "speed": 150},
    {"name": "B", "year": 1980, "speed": 100},
    {"name": "C", "year": 1990, "speed": 250}
]

def get_year(hurricane_dict):
    return hurricane_dict["year"]

def get_speed(hurricane_dict):
    # 使用.get()方法提供默认值,防止键不存在时报错
    return hurricane_dict.get("speed", 0)

# 按年份排序
sorted_by_year = sorted(hurricanes, key=get_year)
print(sorted_by_year)
# 输出: [{'name': 'B', ...}, {'name': 'C', ...}, {'name': 'A', ...}]

# 按速度降序排序
sorted_by_speed_desc = sorted(hurricanes, key=get_speed, reverse=True)
print(sorted_by_speed_desc)
# 输出: [{'name': 'C', ...}, {'name': 'A', ...}, {'name': 'B', ...}]

排序时忽略大小写

对字符串列表进行不区分大小写的排序。

letters = ["D", "b", "C", "a"]
# 使用 str.lower 作为key函数,将所有字母转换为小写后再比较
sorted_letters = sorted(letters, key=str.lower)
print(sorted_letters)  # 输出: ['a', 'b', 'C', 'D']

通过传递 str.lower 函数,排序时比较的是每个字符串的小写版本,从而实现了不区分大小写的排序。

总结

本节课中我们一起学习了Python中“函数作为对象”这一强大概念。我们掌握了如何将函数赋值给变量、将函数存储在数据结构中、以及将函数作为参数传递给其他函数。通过 apply_to_each 和自定义排序 key 函数等实用案例,我们看到了这些技巧如何让代码变得更通用、更简洁、更易于维护。理解并运用这些概念,是迈向Python高级编程的重要一步。

022:生成器

在本节课中,我们将要学习Python中一个强大的概念——生成器。我们将从回顾递归函数和函数对象开始,然后深入探讨生成器的定义、工作原理及其与迭代器和可迭代对象的关系。通过简单的例子,我们将理解yield关键字如何改变函数的行为,并学习如何有效地使用生成器来处理数据流。

回顾:递归与函数对象

上一节我们介绍了递归函数和函数对象。本节中我们来看看两个具体的例子,以巩固对这些概念的理解。

示例1:递归函数

def mystery(x, y):
    if y == 1:
        return x
    return x * mystery(x, y-1)

这个函数通过递归调用自身来计算xy次幂。例如,mystery(2, 3)会返回8

示例2:函数对象

def raise_10(x):
    return mystery(10, x)

result = list(map(raise_10, [1, 2, 3]))

这里,raise_10是一个函数对象,它被传递给map函数,用于将列表[1, 2, 3]中的每个元素转换为10的相应次幂。

生成器简介

现在,让我们转向今天的新主题——生成器。生成器是一种特殊的函数,它使用yield关键字来返回值,并且可以在多次调用之间保持其状态。

生成器与普通函数的区别

普通函数会一次性执行所有代码并返回结果。例如,下面的函数会生成一个包含数字0到9的列表:

def get_one_digit_nums():
    print("start")
    nums = []
    i = 0
    while i < 10:
        nums.append(i)
        i += 1
    return nums

for x in get_one_digit_nums():
    print(x)

这段代码会先打印一次“start”,然后一次性生成整个列表,最后再遍历打印每个数字。

然而,生成器函数则不同。它使用yield来“惰性”地产生值,每次只生成一个,并在产生值后暂停执行,直到下一次被请求。

生成器的工作原理

以下是一个简单的生成器示例:

def simple_generator():
    yield 1
    yield 2
    yield 3

for value in simple_generator():
    print(value)

这个生成器会依次产生值1、2和3。每次循环迭代时,它都会从上次暂停的地方继续执行。

使用next()函数

我们可以使用next()函数手动从生成器中获取值:

gen = simple_generator()
print(next(gen))  # 输出 1
print(next(gen))  # 输出 2
print(next(gen))  # 输出 3

当生成器没有更多值可产生时,它会引发StopIteration异常。

生成器的状态保持

生成器能够记住每次yield之后的状态,包括局部变量和执行位置。这使得生成器非常适合处理无限序列或大型数据流。

迭代器与可迭代对象

在深入理解生成器之前,我们需要明确两个相关概念:迭代器和可迭代对象。

可迭代对象

可迭代对象是任何可以放在for循环中的对象。例如,列表、字符串、元组和range对象都是可迭代的。

my_list = [1, 2, 3]
for item in my_list:
    print(item)

迭代器

迭代器是一个具有next()方法的对象,用于逐个访问可迭代对象中的元素。我们可以使用iter()函数从可迭代对象创建迭代器。

my_list = [1, 2, 3]
it = iter(my_list)
print(next(it))  # 输出 1
print(next(it))  # 输出 2
print(next(it))  # 输出 3

生成器作为迭代器

生成器对象本身就是迭代器,因为它们实现了next()方法。因此,生成器可以直接用在for循环中,也可以手动调用next()

实际示例与测试

让我们通过一些例子来测试不同类型的对象是可迭代的还是迭代器。

示例1:列表

x = [1, 2, 3]
# 测试是否为可迭代对象
it = iter(x)  # 成功,列表是可迭代的
print(next(it))  # 输出 1
# 测试是否为迭代器
next(x)  # 失败,列表不是迭代器

示例2:enumerate对象

y = enumerate([1, 2, 3])
# 测试是否为可迭代对象
it = iter(y)  # 成功
# 测试是否为迭代器
print(next(y))  # 输出 (0, 1),成功,enumerate对象既是可迭代的也是迭代器

示例3:整数

z = 3
iter(z)  # 失败,整数不是可迭代对象
next(z)  # 失败,整数不是迭代器

总结

本节课中我们一起学习了生成器的核心概念。我们了解到:

  1. 生成器是包含yield关键字的函数,它们可以“惰性”地产生值,并在多次调用之间保持状态。
  2. 生成器对象是迭代器,因此可以直接用在for循环中或使用next()函数获取值。
  3. 可迭代对象是任何可以用于for循环的对象(如列表、字符串),而迭代器是具有next()方法的对象,用于逐个访问可迭代对象的元素。
  4. 通过iter()函数可以从可迭代对象创建迭代器。

生成器是处理大型数据集或无限序列的强大工具,因为它们不需要一次性将所有数据加载到内存中。在接下来的课程中,我们将看到生成器在文件处理等实际场景中的应用。

023:错误处理 🐛

在本节课中,我们将要学习Python中的错误处理。我们将探讨如何通过assert语句让程序在遇到逻辑错误时主动“崩溃”,以及如何使用try...except结构来优雅地处理运行时异常,使程序在遇到预期内的错误时能够继续运行。

概述

程序错误主要分为两类:运行时错误和语义错误。运行时错误会导致程序立即停止(崩溃),而语义错误则会让程序产生看似合理但实际错误的结果。本节课的目标是学习如何将危险的语义错误转化为更容易调试的运行时错误,以及如何捕获和处理预期的异常,提升程序的健壮性。


引入示例:披萨分析程序 🍕

我们将通过一个“披萨分析程序”来演示错误处理。这个程序会询问用户披萨的直径(英寸)和切片数量,然后计算每片披萨的面积。

首先,我们来看看程序的核心代码结构:

import math

def pizza_size(radius):
    return math.pi * radius ** 2

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/dabdf1973156fbf4fa52c2ecb35d6528_9.png)

def slice_size(radius, slice_count):
    total_size = pizza_size(radius)
    return total_size * (1 / slice_count)

def main():
    for i in range(10):
        # 获取用户输入
        user_input = input(“输入披萨直径(英寸)和切片数量,用逗号分隔:”)
        # ... 处理输入和计算的代码
        # 输出结果
        print(“每片披萨的面积是:{:.2f} 平方英寸”.format(size))

if __name__ == “__main__”:
    main()

上一节我们介绍了程序的基本功能,本节中我们来看看用户可能输入哪些“坏数据”导致程序出错。

以下是用户可能输入的一些“坏数据”示例:

  • 切片数量为0:这会导致1 / slice_count时发生除零错误(运行时错误)。
  • 输入非数字字符:例如输入a, b,在将字符串转换为浮点数或整数时会引发ValueError
  • 遗漏逗号:例如输入10 4,程序在尝试分割字符串并访问第二个元素时会引发IndexError
  • 切片数量为浮点数:例如输入10, 2.5,在转换为整数时会引发ValueError
  • 切片数量为负数:例如输入10, -4。程序能正常运行并给出一个负的面积,这是一个语义错误
  • 直径为负数:例如输入-10, 6。由于半径被平方,程序会输出一个正数面积,这同样是一个具有欺骗性的语义错误

让程序“崩溃”更多:使用 assert 语句 💥

我们更希望像“负直径”或“负切片数”这样的语义错误能直接导致程序崩溃,而不是产生错误答案。Python的assert(断言)语句可以帮助我们做到这一点。

assert语句的语法非常简单:assert 布尔表达式。如果表达式为True,程序继续执行;如果为False,程序会立即崩溃并抛出AssertionError

以下是一些assert语句的例子:

  • assert x > 0:确保变量x是正数。
  • assert items is not None:确保items不是空值。
  • assert len(nums) % 2 == 1:确保列表nums的长度是奇数。

现在,让我们将断言添加到披萨程序中,在计算前检查输入的有效性:

# 在slice_size函数中或计算前添加
radius = diameter / 2
assert radius > 0, “披萨直径必须为正数”
assert slices > 0, “切片数量必须为正整数”

添加后,当输入负直径或非正切片数时,程序会主动崩溃并显示我们指定的错误信息,从而将语义错误转化为了运行时错误。


让程序“崩溃”更少:使用 try...except 结构 🛡️

并非所有错误都需要让整个程序停止。对于用户输入错误这类预期内的问题,我们更希望提示用户重新输入。这时可以使用try...except结构来捕获并处理异常。

try...except的基本结构如下:

try:
    # 尝试执行的代码块
    可能出错的代码
except:
    # 如果try块中发生任何异常,则执行此代码块
    处理异常的代码

Python会先执行try块中的代码。如果一切正常,则跳过except块。如果try块中发生异常,程序会立即跳转到对应的except块执行,而不会让整个程序崩溃。

上一节我们学习了如何让程序主动崩溃,本节中我们来看看如何有选择地防止崩溃,让程序从错误中恢复。

捕获特定类型的异常

使用except Exception:会捕获所有异常,这可能过于宽泛,甚至会捕获我们无法处理的错误(如KeyboardInterrupt,即Ctrl+C)。最佳实践是只捕获我们预期并能处理的特定异常类型。

我们可以通过元组指定要捕获的异常类型:

try:
    # 尝试执行的代码
except (ValueError, IndexError) as e:
    # 只处理ValueError和IndexError
    print(f“输入错误:{e}”)

在披萨程序中,我们可以这样改进输入处理部分:

try:
    args = user_input.split(“,”)
    diameter = float(args[0].strip())
    slices = int(args[1].strip())
except (ValueError, IndexError) as e:
    print(f“输入格式错误,请确保输入‘直径,切片数’,例如‘10,6’。错误详情:{e}”)
    continue # 跳过本次循环,让用户重新输入

这样,程序只会捕获因用户输入格式问题引发的ValueError(如非数字转换)和IndexError(如缺少逗号),而其他类型的错误(如我们代码本身的bug)则会导致程序正常崩溃,便于我们调试。


主动抛出异常:raise 语句 🎯

除了使用assert,我们还可以使用raise语句主动抛出(引发)一个指定类型的异常,并附带自定义的错误信息。这比通用的AssertionError能提供更精确的错误上下文。

raise语句的语法是:raise 异常类型(“错误信息”)

例如,在披萨程序中替换assert

# 替换 assert radius > 0
if radius <= 0:
    raise ValueError(“披萨直径必须为正数”)
# 替换 assert slices > 0
if slices <= 0:
    raise ValueError(“切片数量必须为正整数”)

然后,我们可以在调用此函数的地方,用更具体的except ValueError:来捕获这个异常并进行相应处理。


Python异常层次结构 📊

Python内置了许多异常类型,它们形成了一个层次结构。最顶层的基类是BaseException,其下包括SystemExitKeyboardInterrupt等。我们通常处理的Exception类是所有内置、非系统退出类异常的基类。

了解这个层次结构有助于我们更精确地捕获异常。例如:

  • LookupErrorIndexError(列表索引越界)和KeyError(字典键不存在)的父类。
  • 捕获Exception比只写except:更具体,因为它不会捕获KeyboardInterrupt
  • 最佳实践是捕获最具体的异常类型(如ValueError),避免隐藏意料之外的错误。


总结

本节课中我们一起学习了Python错误处理的核心概念:

  1. 使用assert:在开发阶段主动检查程序状态,将潜在的语义错误转化为运行时错误,使问题更容易暴露。
  2. 使用try...except:捕获并处理预期内的运行时异常(如用户输入错误),防止程序不必要的崩溃,提升用户体验。
  3. 使用raise:主动抛出特定类型的异常,提供清晰的错误信息,便于上层代码进行针对性处理。
  4. 异常处理原则:始终追求特异性,只捕获你能处理的异常;并且总要输出信息,记录异常的发生,避免 silently failing(静默失败)。

通过合理运用这些技术,你可以编写出既健壮(不易崩溃)又易于调试(错误信息明确)的Python程序。

024:文件处理 📂

在本节课中,我们将要学习如何在Python中处理文件。文件是存储数据的重要方式,无论是读取现有数据还是保存程序生成的结果,文件操作都是编程中的核心技能。我们将从基础的文件读写开始,逐步深入到目录操作、异常处理以及文件编码等高级主题。


文件基础与协议

首先,我们需要理解文件对象。在Python中,文件对象是一种迭代器,这意味着我们可以对其使用next()函数来逐行读取数据。

获取文件对象的方法是调用内置的open()函数。这个函数需要一个参数:文件路径(一个字符串),并返回一个文件对象。

f = open('file.txt')

文件路径可以是相对路径或绝对路径。相对路径是相对于当前运行Python脚本的目录。

一旦我们获得了文件对象,通常会对它做两件事:读取数据或写入数据。完成操作后,必须调用close()方法来关闭文件,以释放系统资源并确保数据被正确保存。

处理文件遵循一个简单的三步协议:

  1. 调用open()函数打开文件。
  2. 使用文件对象进行读取或写入。
  3. 调用close()方法关闭文件。

读取文件 📖

上一节我们介绍了文件的基本协议,本节中我们来看看如何从文件中读取数据。

读取文件最简单的方法是使用文件对象的read()方法,它会将整个文件的内容作为一个字符串返回。

f = open('file.txt')
content = f.read()
f.close()

需要注意的是,read()方法在第一次调用后会“消耗”文件内容。再次调用read()将返回空字符串,因为文件指针已经移到了末尾。如果需要重新读取,必须先关闭文件再重新打开。

由于文件对象是迭代器,我们还可以使用其他更灵活的方式来读取:

以下是几种常见的读取模式:

  • 使用next()函数:逐行获取数据。
    f = open('file.txt')
    first_line = next(f)
    f.close()
    
  • 使用for循环迭代:这是逐行处理文件的推荐方式。
    f = open('file.txt')
    for line in f:
        print(line)
    f.close()
    
  • 转换为列表:将文件的所有行一次性读入一个列表。
    f = open('file.txt')
    lines = list(f)
    f.close()
    

写入文件 ✍️

学会了读取文件后,接下来我们学习如何将数据写入文件。

要写入文件,需要在调用open()函数时指定模式参数mode='w'(写模式)。默认模式是'r'(读模式)。

f = open('output.txt', 'w')

重要提示:以写模式('w')打开一个已存在的文件会立即清空该文件的所有内容。如果文件不存在,则会创建一个新文件。

写入数据使用文件对象的write()方法。与print()函数不同,write()方法不会自动在字符串末尾添加换行符,需要手动添加\n

f.write('Hello world\n')
f.write('This is a new line.\n')
f.close()

如果希望向现有文件追加内容而不覆盖原有数据,可以使用追加模式'a'

f = open('output.txt', 'a')
f.write('This line is appended.\n')
f.close()

在实践中,有两种常见的写入模式:

  • 一次性写入:在内存中构建完整的字符串,然后一次性写入文件。适用于数据量不大的情况。
  • 增量写入:在程序运行过程中不断打开文件并写入数据。适用于生成日志文件或处理可能中断的长时运行任务,可以避免数据丢失。

使用OS模块管理文件和目录 🗂️

在文件操作中,经常需要与目录交互,例如列出文件、检查路径是否存在等。Python的os模块提供了这些功能。

首先需要导入os模块。

import os

以下是os模块中一些最常用的函数:

  • os.listdir(path):列出指定目录下的所有文件和子目录名称。参数'.'表示当前目录,'..'表示上级目录。
    files = os.listdir('.')  # 列出当前目录内容
    
  • os.mkdir(name):在当前目录下创建一个新目录。
    os.mkdir('new_folder')
    
  • os.path.exists(path):检查指定的路径(文件或目录)是否存在。
    if os.path.exists('myfile.txt'):
        print("File exists.")
    
  • os.path.isfile(path):检查指定路径是否是一个文件。
  • os.path.isdir(path):检查指定路径是否是一个目录。
  • os.path.join(...)这是最重要的函数之一。它用于拼接路径,并自动根据当前操作系统(Windows使用\,Linux/Mac使用/)选择正确的路径分隔符,从而保证代码的跨平台可移植性。
    # 可移植的路径拼接方式
    file_path = os.path.join('data', 'subfolder', 'file.txt')
    f = open(file_path)
    
    警告:请务必使用os.path.join()来拼接路径,而不是手动使用/\。手动拼接的路径在跨平台(例如,将代码从Windows提交到Linux测试环境)时会导致文件找不到的错误。

文件操作中的异常处理 ⚠️

文件操作极易引发异常,因为许多错误(如磁盘空间不足、文件被其他程序删除、权限不足等)无法在操作前被完全预测。因此,使用try...except语句来处理文件异常是必要的。

一个常见的异常场景是尝试创建一个已存在的目录。

import os
os.mkdir('my_dir')  # 第一次运行成功
os.mkdir('my_dir')  # 第二次运行会引发 FileExistsError

为了避免程序因目录已存在而崩溃,我们可以捕获特定的异常:

import os
try:
    os.mkdir('my_dir')
except FileExistsError:
    pass  # 如果目录已存在,则忽略这个错误,继续执行

在异常处理中,最好只捕获你预期中会发生的特定异常(如FileExistsError),而不是使用空的except:语句捕获所有异常,后者会隐藏其他潜在的程序错误。


文件编码 🔤

计算机以二进制(0和1)存储数据,而人类阅读文本文件。编码(Encoding)就是字符(如‘A’,‘中’)与二进制数据之间相互转换的规则。

不同的编码标准(如UTF-8, GBK, CP1252)使用不同的规则。关键原则是:必须使用相同的编码来写入和读取文件,否则会导致乱码或解码错误。

在Python中,可以在open()函数中通过encoding参数指定编码。现代应用的推荐编码是UTF-8,它在Linux、Mac和现代Windows系统上得到广泛支持。

# 使用UTF-8编码写入文件
with open('file.txt', 'w', encoding='utf-8') as f:
    f.write('一些文本')

# 使用相同的UTF-8编码读取文件
with open('file.txt', 'r', encoding='utf-8') as f:
    content = f.read()

如果你的程序输出中出现了奇怪的符号(如���Â),很可能就是编码不匹配造成的。


实战演示与总结

本节课中我们一起学习了Python文件处理的核心知识。我们了解了文件操作的基本协议(打开、使用、关闭),掌握了读取和写入文件的不同方法,学会了使用os模块管理目录和构建可移植的路径,认识了文件操作中异常处理的重要性,并理解了文件编码的概念。

让我们回顾一个关键的综合技巧:使用with语句可以自动管理文件的打开和关闭,即使发生异常也能确保文件被正确关闭,这是更优雅和安全的做法。

# 使用 with 语句,无需手动调用 close()
with open('file.txt', 'r') as f:
    data = f.read()
# 文件在此处会自动关闭

通过结合循环、条件判断和异常处理,你可以构建出强大且健壮的程序来处理各种文件任务。记得在接下来的项目和练习中多加实践,熟练掌握这些技能。

025:Pandas入门(第一部分)

在本节课中,我们将开始学习Python数据分析的核心库——Pandas。我们将了解Pandas是什么,它如何作为处理表格数据的强大工具,并学习其基础数据结构Series的创建、索引和基本操作。


概述

Pandas是一个用于处理表格数据的Python库,可以看作是Excel的编程替代品。它引入了两种新的数据结构:SeriesDataFrame。本节课我们将重点学习Series,它是一种介于Python列表和字典之间的混合数据结构,支持强大的数据索引和元素级操作。


课程回顾与定位

在课程的前五周,我们学习了控制流,包括顺序执行、条件语句、循环和函数。接下来的五周,我们深入探讨了数据结构,特别是列表和字典。现在,我们进入课程的第三部分,专注于数据编程和数据科学。

虽然接下来的主题(如Pandas、数据库、Web编程)相对独立,但它们都依赖于前10周所学的Python基础知识。请确保巩固这些知识。


什么是Pandas? 🐼

Pandas是一个Python模块,专门用于处理表格数据。它提供了比使用“列表的列表”更高效、功能更丰富的方式来操作数据。

核心优势:与Excel等电子表格软件相比,在Jupyter Notebook中使用Pandas可以让我们清晰地看到所有代码逻辑、逐步开发解决方案,并以表格形式打印结果,整个过程更加透明和可复现。

要使用Pandas,首先需要安装它。在终端或PowerShell中运行以下命令:

pip install pandas

安装完成后,在Python中导入它。通常使用缩写pd

import pandas as pd


Pandas Series:混合数据结构

Series是Pandas的基础数据结构之一。你可以把它理解为一个带有标签的一维数组,它融合了Python列表和字典的特性。

重要概念与命名

  • 索引: 类似于字典的。用于通过标签查找数据。
  • 整数位置: 类似于列表的索引。用于通过位置(0, 1, 2...)查找数据。
    Pandas的术语可能有些混淆,但掌握这些核心对应关系至关重要。

从字典创建Series

我们可以从一个Python字典轻松创建Series。字典的键会成为Series的索引,值则成为Series的数据。

import pandas as pd

# 创建一个Python字典
d = {'a': 7, 'b': 8, 'c': 9}
print("Python字典:", d)

# 从字典创建Pandas Series
s = pd.Series(d)
print("Pandas Series:")
print(s)

输出结果会以表格形式显示,左侧是索引(‘a‘, ‘b‘, ‘c‘),右侧是对应的值。dtype: int64表示数据以64位整数格式存储,这是Pandas底层为追求效率而采用的数据类型。


从列表创建Series

我们也可以直接从列表创建Series。此时,列表的索引(0, 1, 2...)会自动成为Series的索引。

# 创建一个Python列表
my_list = [100, 200, 300]
print("Python列表:", my_list)

# 从列表创建Pandas Series
s_from_list = pd.Series(my_list)
print("Pandas Series (来自列表):")
print(s_from_list)

访问Series中的数据

由于Series具有“索引”和“整数位置”两套查找系统,因此有多种访问数据的方式。

使用.loc[](按索引/标签访问)

.loc[]用于通过索引标签进行查找,类似于字典的键。

s = pd.Series([100, 200, 300], index=['x', 'y', 'z'])
print(s.loc['y'])  # 输出: 200

使用.iloc[](按整数位置访问)

.iloc[]用于通过整数位置进行查找,类似于列表的索引。

s = pd.Series([100, 200, 300])
print(s.iloc[1])   # 输出: 200
print(s.iloc[-1])  # 输出: 300 (支持负索引)

快捷方式(但需注意)

Pandas会尝试自动判断你的意图:

s = pd.Series([100, 200, 300], index=[5, 6, 7])
# 如果索引是整数,使用方括号[]默认按索引查找
print(s[6])  # 输出: 200 (查找索引为6的值)
# 明确使用.iloc按位置查找
print(s.iloc[1])  # 输出: 200 (查找第2个位置的值)

重要提示:当索引也是整数时,会产生歧义。为了代码清晰,建议始终使用.loc.iloc来明确查找方式。尤其在使用负索引时,必须使用.iloc


选择多个值

我们可以一次性选择多个值,返回一个新的Series。

s = pd.Series([10, 20, 30, 40], index=['a', 'b', 'c', 'd'])

# 通过索引标签列表选择
print(s.loc[['a', 'c']])  # 输出索引‘a‘和‘c‘对应的值

# 通过整数位置列表选择
print(s.iloc[[0, 2]])     # 输出第1个和第3个位置的值

Series的切片

切片操作与列表类似,但有一个关键区别:Series切片会保留原始索引,但会重新编号整数位置

letters_list = ['A', 'B', 'C', 'D']
letters_series = pd.Series(letters_list)

# Python列表切片
list_slice = letters_list[2:]  # 整数位置2到最后
print("列表切片:", list_slice)          # 输出: ['C', 'D']
print("列表切片索引0:", list_slice[0]) # 输出: 'C' (新列表索引从0开始)

# Pandas Series切片
series_slice = letters_series[2:] # 索引2到最后
print("Series切片:")
print(series_slice)               # 输出索引2(‘C‘)和3(‘D‘)的值,但显示索引2和3
print("Series切片.iloc[0]:", series_slice.iloc[0]) # 输出: 'C' (使用.iloc[0]访问新Series的第一个元素)
# print(series_slice.loc[0]) # 这会报错,因为索引中没有0

元素级操作

这是Pandas Series最强大的特性之一,允许我们直接对整个Series进行数学或逻辑运算,而无需编写循环。

与标量(单个值)运算

运算会应用到Series中的每一个元素。

s = pd.Series([1, 2, 3])
print(s + 3)   # 输出: [4, 5, 6]
print(s * 2)   # 输出: [2, 4, 6]
print(s / 2)   # 输出: [0.5, 1.0, 1.5]

注意:这些操作会生成一个新的Series,不会修改原始Series。

两个Series之间的运算

运算会按整数位置对齐对应元素。

s1 = pd.Series([1, 2, 3])
s2 = pd.Series([4, 5, 6])
print(s1 + s2)  # 输出: [5, 7, 9] (1+4, 2+5, 3+6)
print(s1 * s2)  # 输出: [4, 10, 18]
print(s1 < s2)  # 输出: [True, True, True] (生成布尔型Series)

索引对齐

当两个Series进行运算时,Pandas会优先按索引进行匹配,而不是整数位置。

s1 = pd.Series([10, 20], index=['a', 'b'])
s2 = pd.Series([1, 2], index=['b', 'a']) # 索引顺序不同
print(s1 + s2)
# 输出:
# a    12 (10 + 2)
# b    21 (20 + 1)
# dtype: int64

如果索引无法匹配,结果会是NaN(Not a Number)。

s1 = pd.Series([1, 2, 3])
s2 = pd.Series([4, 5]) # 长度不同
print(s1 + s2)
# 输出:
# 0    5.0
# 1    7.0
# 2    NaN
# dtype: float64 (自动转换为浮点型以容纳NaN)

合并Series

使用pd.concat函数可以连接多个Series,生成一个更长的Series。

s1 = pd.Series([1, 2, 3])
s2 = pd.Series([4, 5])
combined = pd.concat([s1, s2])
print(combined)

布尔索引:强大的数据筛选工具

布尔索引允许我们根据条件快速筛选数据。其核心思想是:用一个布尔值(True/False)的Series来指示原Series中哪些元素应该被选中。

基础用法

s = pd.Series([10, 2, 3, 15])
# 创建一个布尔Series,标记哪些元素大于8
condition_series = s > 8
print(condition_series) # 输出: [True, False, False, True]

# 使用布尔Series进行索引,只选择值为True的元素
filtered = s[condition_series]
print(filtered) # 输出: [10, 15]

一步到位的链式操作

通常我们将条件创建和筛选合并成一行:

s = pd.Series([10, 2, 3, 15])
# 筛选出大于8的元素
print(s[s > 8])

# 筛选出奇数
print(s[s % 2 == 1]) # 19, 11, 35 (假设s包含这些值)

组合条件筛选

在Pandas中,逻辑运算符and, or, not被替换为&, |, ~,并且每个条件必须用括号括起来

s = pd.Series([10, 19, 11, 30, 35])

# 筛选出小于12 或 大于33的数
print(s[(s < 12) | (s > 33)])

# 筛选出大于12 且 小于33的数
print(s[(s > 12) & (s < 33)])

# 筛选出“不大于12且不小于33”的数(即上面结果的补集)
print(s[~((s > 12) & (s < 33))])

字符串元素级操作

对于包含字符串的Series,我们可以使用.str访问器来调用字符串方法,对整个Series进行元素级操作。

words = pd.Series(['Apple', 'boy', 'Cat', 'dog'])
# 将所有单词转换为大写
print(words.str.upper())

# 筛选出原本就是大写的单词
# 原理:比较原始单词和其大写形式是否相等
uppercase_words = words[words == words.str.upper()]
print(uppercase_words)

总结

本节课我们一起学习了Pandas库的基础。我们首先了解了Pandas在数据科学工作流中的定位,然后深入探讨了其核心数据结构Series

我们掌握了:

  1. 创建Series:从字典和列表创建。
  2. 数据访问:使用.loc[]按索引访问,使用.iloc[]按整数位置访问,以及多值选择和切片。
  3. 强大操作元素级运算(与标量或另一个Series),以及基于索引对齐的运算规则。
  4. 核心技巧布尔索引,这是一种通过逻辑条件快速、高效筛选数据的强大方法。
  5. 扩展应用:对字符串Series进行元素级操作。

Series是构建Pandas中表格数据对象DataFrame的基石。在下一讲中,我们将学习DataFrame,开始真正的表格数据处理之旅。请务必熟悉本节课关于Series索引和操作的概念,它们是理解后续内容的关键。

026:Pandas 1-2 📊

在本节课中,我们将深入学习Pandas库,这是处理表格数据的强大工具。我们将从回顾Series开始,然后重点介绍Pandas的第二个核心数据结构——DataFrame。我们将学习如何创建、操作和查询DataFrame,并通过一个电影数据集的实际例子来应用这些概念。


回顾Series 📝

上一节我们介绍了Pandas的Series,它是一种介于列表和字典之间的混合数据结构。本节中,我们通过一些练习来巩固对Series,特别是布尔索引的理解。

以下是几个关于Series操作的练习题及其解析:

问题1: 给定一个Series v = pd.Series([-1, 1, 200, 191, 4])v < 0 的结果是什么?

  • 这个关系运算符会进行逐元素比较,生成一个布尔Series。
  • 结果:0 True, 1 False, 2 False, 3 False, 4 False

问题2: v * v == 1 的结果是什么?

  • 首先计算 v * v,得到新Series [1, 1, 40000, 36481, 16]
  • 然后判断每个元素是否等于1。
  • 结果:0 True, 1 True, 2 False, 3 False, 4 False

问题3: v[v > 100] 的结果是什么?

  • v > 100 生成布尔Series [False, False, True, True, False]
  • 使用该布尔Series索引原Series v,只保留值为True对应的行。
  • 结果:2 200, 3 191

问题4: v[v % 2 == 0] 的结果是什么?

  • v % 2 计算每个元素除以2的余数。
  • v % 2 == 0 判断哪些元素是偶数,生成布尔Series。
  • 使用该布尔Series进行索引。
  • 结果:2 200, 4 4

问题5: v[(v > 0) & (v < 100)] 的结果是什么?

  • (v > 0)(v < 100) 分别生成布尔Series。
  • 使用Pandas的按位与运算符 & 将两个条件组合。
  • 结果:1 1, 4 4

数据对齐与关系运算符 🔗

在进行Series运算时,数据对齐是一个关键概念。当两个Series的索引不完全相同时,Pandas会尝试根据索引进行对齐。

import pandas as pd
from pandas import Series, DataFrame

x = Series({'a': 10, 'b': 100})
s1 = Series({'a': 20, 'b': 300})
s2 = Series({'b': 20, 'a': 300}) # 索引顺序不同

print(x * s1) # 索引已对齐,正常计算
print(x * s2) # 根据索引‘a’和‘b’对齐,结果与上一行相同

然而,直接使用关系运算符(如 <, >)比较索引未对齐的Series可能会引发错误,因为Pandas为了提高性能,要求比较的Series必须具有完全相同的索引标签。

解决方法有两种:

  1. 先对索引进行排序:x.sort_index() < s2.sort_index()
  2. 使用Series对象的关系方法(如 .lt(), .gt()),这些方法内部会处理对齐。

以下是常用的关系方法:

x.lt(s2)   # 小于 (less than)
x.gt(s2)   # 大于 (greater than)
x.le(s2)   # 小于等于 (less than or equal)
x.ge(s2)   # 大于等于 (greater than or equal)
x.ne(s2)   # 不等于 (not equal)
x.eq(s2)   # 等于 (equal)

引入DataFrame 🗂️

DataFrame是Pandas中用于表示二维表格数据的核心数据结构。你可以将其视为多个共享相同索引的Series的集合,或者一个由列组成的字典。

有多种方式可以创建DataFrame:

1. 从字典的列表创建:

# 键成为列名,每个内层列表是一列的数据
data = {'name': ['Alice', 'Bob', 'Cindy', 'Dan'],
        'score': [4, 5, 6, 7]}
df1 = DataFrame(data)

2. 从列表的列表创建:

# 每个内层列表代表一行数据
data = [['Alice', 4],
        ['Bob', 5],
        ['Cindy', 6],
        ['Dan', 7]]
df2 = DataFrame(data)
# 此时列名和行索引都是默认的整数

3. 从字典的字典创建:

# 外层字典的键成为行索引,内层字典的键成为列名
data = {'A': {'name': 'Alice', 'score': 4},
        'B': {'name': 'Bob', 'score': 5},
        'C': {'name': 'Cindy', 'score': 6},
        'D': {'name': 'Dan', 'score': 7}}
df3 = DataFrame(data)

4. 从字典的列表创建:

# 每个字典代表一行数据,键指定了该数据属于哪一列
data = [{'name': 'Alice', 'score': 4},
        {'name': 'Bob', 'score': 5},
        {'name': 'Cindy', 'score': 6},
        {'name': 'Dan', 'score': 7}]
df4 = DataFrame(data)

创建DataFrame后,我们可以使用 indexcolumns 参数来指定行标签和列名:

df = DataFrame(data, index=['A', 'B', 'C', 'D'], columns=['name', 'score'])

数据查询与切片 🔍

与Series类似,DataFrame也有多种数据访问方式,但需要同时考虑行和列。

访问整行或整列:

  • df.loc[行索引]: 通过行标签获取整行(返回Series)。
  • df.iloc[行位置]: 通过整数位置获取整行(返回Series)。
  • df[列名]: 通过列名获取整列(返回Series)。这是最常用的方式,便于进行列级计算,如 df[‘score’].mean()

访问单个值或赋值:
需要同时指定行和列。

# 获取‘B’行的‘score’列的值
value = df.loc['B', 'score']
# 将‘D’行的‘score’列的值改为17
df.loc['D', 'score'] = 17
# 使用iloc实现同样效果(最后一行,第1列)
df.iloc[-1, 1] = 17

切片:
可以获取DataFrame的一个矩形子集。

# 使用iloc切片(左闭右开)
df.iloc[1:3, :] # 获取第1到2行,所有列
# 使用loc切片(双闭区间)
df.loc['B':'C', :] # 获取行标签‘B’到‘C’,所有列
# 使用列表选择特定行
df.loc[['B', 'D'], :] # 获取‘B’行和‘D’行

注意: 对DataFrame切片得到的是原数据的“视图”,可以直接通过赋值修改原数据,例如 df.iloc[:, 1] += 100 会给所有行的‘score’列加100。


布尔索引 🎯

布尔索引是筛选数据的强大工具。我们可以创建一个布尔Series(True/False值),然后用它来选择DataFrame中对应的行。

# 创建一个布尔Series,标记哪些行的分数大于5
boolean_series = df['score'] > 5
# 使用布尔Series索引DataFrame,只保留分数大于5的行
high_scores_df = df[boolean_series]

通常我们会将条件直接写在索引中:

high_scores_df = df[df['score'] > 5]

实战:分析电影数据集 🎬

现在,让我们将这些知识应用到一个真实的电影数据集(IMDB)中。

1. 从CSV文件读取数据:

movies_df = pd.read_csv('imdb.csv')

注意: 如果CSV文件本身包含索引列,用pd.read_csv读取时会将其作为普通数据列,并生成新的默认整数索引,导致索引重复。写入CSV时,默认也会写入索引。可以使用 index=False 参数避免写入索引:movies_df.to_csv(‘output.csv’, index=False)

2. 初步查看数据:

movies_df.head() # 查看前5行
movies_df.tail(3) # 查看后3行
movies_df['rating'].mean() # 计算平均评分
movies_df['runtime'].max() # 找到最长电影时长

3. 复杂查询示例:找出“时长高于平均时长”的电影中“评分最高”的电影。
我们将问题分解为清晰的步骤:

# 步骤1: 计算平均时长
avg_runtime = movies_df['runtime'].mean()
# 步骤2: 创建布尔Series,标记时长高于平均的电影
long_movies_mask = movies_df['runtime'] > avg_runtime
# 步骤3: 获取所有长电影的子集
long_movies = movies_df[long_movies_mask]
# 步骤4: 在长电影中找到最高评分
highest_rating = long_movies['rating'].max()
# 步骤5: 找出评分为最高评分的电影
top_long_movies = long_movies[long_movies['rating'] == highest_rating]
print(top_long_movies[['title', 'rating', 'runtime']])

虽然可以将这些步骤组合成一行代码,但分步编写更易于理解和调试。

4. 快速获取描述性统计:
describe() 方法可以快速生成数值列的各种统计信息(计数、均值、标准差、最小值、四分位数、最大值)。

stats = movies_df.describe()
# 从统计结果DataFrame中提取特定值,例如平均时长
mean_runtime = stats.loc['mean', 'runtime']

本节课中我们一起学习了Pandas DataFrame的创建、数据查询、切片、布尔索引等核心操作,并通过一个电影数据分析的实例综合运用了这些技能。DataFrame是数据科学中处理表格数据的基石,熟练掌握它将为后续学习网络爬虫、数据库和绘图等内容打下坚实基础。

027:数据库1-1 🗄️

在本节课中,我们将要学习数据库的基础知识。我们将了解数据库是什么,它与我们之前学过的CSV和JSON文件有何不同,以及如何从数据库中提取数据。我们将使用SQLite3模块和Pandas库来操作数据库,并通过一个实际的麦迪逊公交数据示例来实践。

概述

数据库是用于存储大量结构化数据的系统。与CSV文件相比,数据库可以包含多个表,每个表都有明确的列名和严格的数据类型。这使得数据管理更加高效和可靠。本节课,我们将学习如何连接到数据库、查看其结构,并使用SQL查询语言从中提取所需的数据。

数据库与CSV/JSON文件的比较

上一节我们介绍了课程目标,本节中我们来看看数据库与CSV、JSON文件的主要区别。

  • CSV文件:通常是一个单独的表格。可能包含表头,也可能没有。所有数据都是字符串类型,读取时需要手动转换数据类型。
  • JSON文件:支持多种数据类型(字符串、整数、浮点数、布尔值等)。但结构灵活,同一字段在不同条目中可以是不同类型,甚至可能缺失,缺乏严格的一致性。
  • SQL数据库:包含多个命名的表。每个表的列都有名称。每列的数据类型是预先定义且严格强制的(例如,整数列不能插入文本),这保证了数据的完整性和一致性。

为什么使用数据库?

了解了基本区别后,我们来看看在什么情况下应该选择使用数据库。

  1. 并发访问:多个程序可以同时安全地读取和写入数据库,而不会导致数据损坏。CSV文件在并发写入时容易产生问题。
  2. 查询便捷:使用SQL(结构化查询语言) 从数据库中提取或汇总数据通常比用Python编写复杂逻辑来处理CSV文件更简单、更高效。例如,找出出演电影最多的演员,在SQL中可能只需一行代码。
  3. 性能优越:数据库内部使用索引等技术来优化数据存储。例如,如果数据已按某列排序,查询该列的条件(如“分数小于23”)会非常快,因为数据库不需要检查每一行。数据库会自动选择最佳方式来快速回答查询。

当然,数据库也有其缺点:它比CSV或JSON文件更复杂。如果数据量很小(例如只有100行),或者问题很简单,使用CSV或JSON文件可能更直接、更合适。

常见SQL数据库简介

在开始实践之前,我们先快速了解几种常见的SQL数据库系统。

  • 商业数据库:如 Microsoft SQL ServerOracle。功能强大,但通常需要付费。
  • 开源数据库:如 MySQLPostgreSQL。免费且功能丰富,非常流行。
  • SQLite:这是我们本课程将使用的数据库。它轻量、易用,许可证非常宽松,可以自由修改和分发。它已经内置在Python中,无需额外安装,并被广泛应用于无数产品(如Android、iOS、Dropbox等)中。

数据集介绍:麦迪逊公交数据

我们将使用美国威斯康星州麦迪逊市的公开公交数据集进行演示。该数据库包含两个表:

  1. routes:包含公交线路信息,如线路ID、短名称、相关URL等。
  2. boarding:包含公交站点信息,如站点ID、所属线路、经纬度坐标,以及最重要的——每个站点日均上车人数(这是12天内的平均值)。

我们的目标就是学习如何从这个数据库中提出有意义的问题并获取答案。

连接与查看数据库

现在,让我们开始动手操作。首先,我们需要导入必要的模块并连接到数据库文件。

import sqlite3
import pandas as pd
import os

# 1. 确保数据库文件存在
db_path = 'bus.db'
assert os.path.exists(db_path), f"文件 {db_path} 不存在"

![](https://github.com/OpenDocCN/dsai-notes-zh/raw/master/docs/wscs-cs220-dtprog/img/687f127ae8881642e80e63779312314a_5.png)

# 2. 创建数据库连接对象
conn = sqlite3.connect(db_path)
print(conn)  # 确认连接对象已创建

sqlite3.connect() 函数类似于用 open() 打开文件,它返回一个连接对象。如果指定的文件不存在,SQLite会创建一个新的空数据库文件。完成后,记得使用 conn.close() 关闭连接。

连接成功后,我们可以先查看数据库的整体结构,了解其中有哪些表。

# 3. 查看数据库中的所有表(这是一个应记住的固定查询)
query_overview = """
SELECT *
FROM sqlite_master
"""
df_overview = pd.read_sql(query_overview, conn)
print(df_overview)

执行上述代码会返回一个表格,其中 type='table' 的行就是数据表,name 列就是表名(如 boardingroutes)。sql 列则显示了创建这些表的原始SQL命令,从中我们可以清楚地看到每个表的列名和数据类型定义,例如:

CREATE TABLE boarding (
    "index" INTEGER,
    stop_id INTEGER,
    route INTEGER,
    latitude REAL,
    longitude REAL,
    daily_boardings REAL
)

这表明 boarding 表有6列,INTEGER 表示整数类型,REAL 表示实数(浮点数)类型。

使用SQL查询提取数据

数据库的核心操作是查询。SQL查询的基本结构是:选择列 -> 从某个表 -> 筛选行 -> 排序 -> 限制数量

其语法顺序如下:

SELECT 列名1, 列名2
FROM 表名
WHERE 条件
ORDER BY 排序列名 ASC|DESC
LIMIT 数量;
  • SELECT:指定要获取哪些列。使用 * 表示选择所有列。
  • FROM:指定从哪个表获取数据。
  • WHERE:可选,用于筛选满足特定条件的行(例如 route = 80)。
  • ORDER BY:可选,用于按指定列排序。ASC 为升序(默认),DESC 为降序。
  • LIMIT:可选,限制返回的行数。

注意:关键字(如 SELECT, FROM)不区分大小写,但通常大写以示区分。查询语句的顺序是固定的。

示例1:查看所有数据

让我们先从 routes 表中查看所有数据,但只显示前几行。

query_all_routes = """
SELECT *
FROM routes
"""
df_routes = pd.read_sql(query_all_routes, conn)
print(df_routes.head())  # 使用Pandas的head()方法查看前5行

示例2:计算总上车人数

现在,我们来回答一个具体问题:麦迪逊每天有多少人乘坐公交车?
我们需要对 boarding 表中的 daily_boardings 列求和。

query_total_riders = """
SELECT daily_boardings
FROM boarding
"""
df_boardings = pd.read_sql(query_total_riders, conn)
# 将DataFrame中的列转换为Series,然后求和
total_riders = df_boardings['daily_boardings'].sum()
print(f"日均公交上车总人数: {total_riders:.0f}")

示例3:寻找最西边的公交站点

假设我们想前往最西边,应该乘坐哪条线路的公交车?
这需要找到 longitude(经度)最小的站点(西经为负,数值越小越靠西)。

query_westernmost = """
SELECT route, longitude, latitude
FROM boarding
ORDER BY longitude ASC
LIMIT 1
"""
df_westernmost = pd.read_sql(query_westernmost, conn)
print(df_westernmost)

这条查询语句的含义是:从 boarding 表中选择 routelongitudelatitude 列,按照 longitude 升序(从小到大)排列,然后只取第一行(即经度最小的那一行)。

总结

本节课中我们一起学习了数据库入门知识。我们了解了数据库相比CSV和JSON文件的优势,包括严格的数据类型、并发处理能力以及高效的查询性能。我们认识了SQLite这一轻量级数据库,并学会了如何使用 sqlite3 模块连接数据库。最重要的是,我们掌握了SQL基本查询语句的结构:使用 SELECTFROM 提取数据,用 WHERE 进行筛选,用 ORDER BY 排序,并用 LIMIT 限制结果数量。通过麦迪逊公交数据的实践,我们计算了总乘车人数并找到了最西边的公交站点。

不要忘记在程序结束时关闭数据库连接:

conn.close()

在接下来的课程中,我们将学习更强大的数据汇总和分组聚合操作。

028:数据库(二)

在本节课中,我们将继续学习SQL数据库查询,重点介绍如何使用聚合函数(如COUNTSUMAVG)以及GROUP BYHAVING子句来对数据进行分组和筛选。我们将通过一系列实践问题来巩固这些概念。

概述与准备

首先,请确保你已下载课程所需的文件,包括模板、数据库和图像文件。本讲座设计为引导式学习活动,建议你在观看视频的同时,暂停并尝试自己解决问题。

导入必要的库并连接到数据库。

import sqlite3
import pandas as pd

# 连接到数据库
conn = sqlite3.connect('movies.db')

查看数据库中的表。

pd.read_sql("SELECT * FROM sqlite_master;", conn)

定义一个辅助函数来执行查询并限制返回的行数,便于查看结果。

def query(sql, n=10):
    df = pd.read_sql(sql, conn)
    return df.head(n)

现在,让我们开始回顾一些基础查询。

基础查询回顾

上一节我们介绍了SELECTFROMWHEREORDER BYLIMIT的基本用法。本节中,我们将通过几个问题来快速回顾。

评分最高的电影

要找出评分最高的电影,我们需要选择电影标题并按评分降序排列。

SELECT title
FROM movies
ORDER BY rating DESC
LIMIT 1;

执导电影时长最短的导演

这个问题要求我们找出执导电影时长最短的导演,并按运行时长短升序排列。

SELECT director
FROM movies
ORDER BY runtime ASC
LIMIT 1;

执导电影收入最高的导演

类似地,我们可以通过按收入降序排列来找到执导电影收入最高的导演。

SELECT director
FROM movies
ORDER BY revenue DESC
LIMIT 1;

2016年收入最高的电影

要找到特定年份(如2016年)收入最高的电影,我们需要使用WHERE子句来筛选年份。

SELECT title
FROM movies
WHERE year = 2016
ORDER BY revenue DESC
LIMIT 1;

评分与收入比率最高的三部电影

有时我们需要基于计算出的新列进行排序。例如,计算评分与收入的比率。

SELECT title, rating / revenue AS ratio
FROM movies
ORDER BY ratio DESC
LIMIT 3;

聚合函数

聚合函数用于对一列数据进行汇总,例如计算总数、平均值、最大值或最小值。

电影总数

使用COUNT函数可以计算表中的行数。

SELECT COUNT(*)
FROM movies;

导演总数(去重)

要计算不重复的导演数量,需要使用COUNT(DISTINCT ...)

SELECT COUNT(DISTINCT director)
FROM movies;

所有电影的总收入

SUM函数用于计算某一列的总和。

SELECT SUM(revenue)
FROM movies;

平均评分(两种方法)

计算平均值可以使用AVG函数,或者手动计算总和除以数量。

-- 方法一:使用AVG函数
SELECT AVG(rating)
FROM movies;

-- 方法二:手动计算
SELECT SUM(rating) / COUNT(*)
FROM movies;

平均收入和平均运行时

可以在一个查询中计算多个聚合值。

SELECT AVG(revenue), AVG(runtime)
FROM movies;

特定导演的平均运行时

结合WHERE子句,可以计算特定导演的电影平均运行时。

SELECT AVG(runtime)
FROM movies
WHERE director = 'James Gunn';

2016年的电影数量

使用COUNTWHERE子句可以统计特定年份的电影数量。

SELECT COUNT(*)
FROM movies
WHERE year = 2016;

最高收入电影占总收入的百分比

这个问题需要计算最大值与总和的比率。

SELECT (MAX(revenue) * 100.0 / SUM(revenue)) AS percentage
FROM movies;

2016年最高收入电影占总收入的百分比

同样,我们可以添加WHERE子句来限制计算范围。

SELECT (MAX(revenue) * 100.0 / SUM(revenue)) AS percentage
FROM movies
WHERE year = 2016;

数据分组(GROUP BY)

GROUP BY子句用于将数据行分组,以便对每个组进行聚合计算。这类似于Python中的“分桶”操作。

每年的总收入

要计算每年的总收入,我们需要按年份分组并对收入求和。

SELECT year, SUM(revenue) AS total_revenue
FROM movies
GROUP BY year;

每位导演执导的电影数量

按导演分组并计算每位导演的电影数量。

SELECT director, COUNT(*) AS movie_count
FROM movies
GROUP BY director
ORDER BY movie_count DESC;

每位导演的平均评分

分组后,可以计算每个组的平均值。

SELECT director, AVG(rating) AS avg_rating
FROM movies
GROUP BY director;

每年的不重复导演数量

结合COUNT(DISTINCT ...)GROUP BY,可以计算每年有多少位不同的导演执导了电影。

SELECT year, COUNT(DISTINCT director) AS unique_directors
FROM movies
GROUP BY year;

筛选分组(HAVING)

HAVING子句用于在分组后对组进行筛选,类似于WHERE子句对行的筛选。

执导超过100万美元收入电影数量最多的导演

首先筛选收入超过100万美元的电影,然后按导演分组,计算数量,最后筛选出数量最多的导演。

SELECT director, COUNT(*) AS count
FROM movies
WHERE revenue > 100
GROUP BY director
ORDER BY count DESC;

平均评分最高的三位导演

按导演分组计算平均评分,然后按评分降序排列并限制结果为前三名。

SELECT director, AVG(rating) AS avg_rating, COUNT(*) AS movie_count
FROM movies
GROUP BY director
ORDER BY avg_rating DESC
LIMIT 3;

自2010年以来执导超过3部电影的导演

这个问题结合了WHERE(筛选年份)和HAVING(筛选分组数量)。

SELECT director, COUNT(*) AS movie_count
FROM movies
WHERE year >= 2010
GROUP BY director
HAVING movie_count > 3;

执导超过3部运行时少于100分钟电影的导演

类似地,我们可以筛选运行时并分组计数。

SELECT director, COUNT(*) AS movie_count
FROM movies
WHERE runtime < 100
GROUP BY director
HAVING movie_count > 3;

SQL查询执行顺序

理解SQL子句的执行顺序对于编写复杂查询至关重要。内部执行顺序如下:

  1. FROM: 选择数据表。
  2. WHERE: 过滤行。
  3. GROUP BY: 对行进行分组。
  4. 聚合函数: 计算每个组的聚合值。
  5. HAVING: 过滤组。
  6. ORDER BY: 对结果排序。
  7. LIMIT: 限制返回的行数。
  8. SELECT: 选择要输出的列(实际上在最后阶段处理)。

总结

本节课中我们一起学习了SQL中强大的数据汇总和分组功能。我们回顾了如何使用COUNTSUMAVGMAX等聚合函数来总结数据。接着,我们深入探讨了GROUP BY子句,它允许我们根据特定列对数据进行分组,并对每个组进行独立的聚合计算。最后,我们介绍了HAVING子句,它用于在分组后对组进行筛选,类似于WHERE子句对行的筛选。

通过结合WHEREGROUP BYHAVING,你可以提出并回答关于数据集的复杂问题,例如“哪位导演在特定年份后执导了多部短片?”。请务必尝试课程提供的额外练习(巴士数据库问题)来巩固这些技能。记住,在调试代码时,使用print语句、添加注释并简化代码是提高效率的关键策略。

029:数据库3

概述

在本节课中,我们将学习SQL与pandas之间的连接,以及如何利用两者之间的知识相互促进学习。我们将通过一个工作表练习,掌握将pandas操作转换为SQL查询,以及将SQL查询转换为pandas操作的方法。


课程结构与数据准备

首先,我们需要导入必要的库并连接到数据库。以下是初始设置步骤。

import pandas as pd
import sqlite3

# 创建数据库连接
conn = sqlite3.connect('worksheet.db')

# 定义一个辅助函数,简化SQL查询的执行
def query(sql):
    return pd.read_sql(sql, conn)

运行上述代码后,我们可以开始探索数据库中的表。首先,查看数据库中的所有表。

query("SELECT * FROM sqlite_master WHERE type='table';")

数据库包含三个表:hydrants(消防栓信息)、trees(树木信息)和species(树种编码信息)。让我们逐一查看这些表的结构。

# 查看trees表
query("SELECT * FROM trees;")
# 查看species表
query("SELECT * FROM species;")

问题1:从pandas到SQL

上一节我们介绍了数据库的基本结构和数据准备。本节中,我们来看看如何将一个pandas操作转换为等价的SQL查询。

问题1A:预测pandas语句的输出

以下是第一个pandas语句,我们需要预测其输出。

trees[trees['priority'] > 90][['x', 'y']]

步骤解析:

  1. trees['priority'] 提取priority列,得到一个Series。
  2. trees['priority'] > 90 对Series进行逐元素比较,生成一个布尔Series。
  3. trees[trees['priority'] > 90] 使用布尔Series选择符合条件的行。
  4. [['x', 'y']] 从结果中提取xy列。

预测输出:

  • 保留priority大于90的行(索引1和4)。
  • 仅显示这些行的xy列。

运行代码验证预测:

trees[trees['priority'] > 90][['x', 'y']]

问题1B:转换为SQL查询

现在,将上述pandas语句转换为SQL查询。

以下是转换后的SQL查询:

SELECT x, y
FROM trees
WHERE priority > 90;

使用query函数执行该SQL语句:

query("SELECT x, y FROM trees WHERE priority > 90;")

问题2:从SQL到pandas

上一节我们练习了从pandas到SQL的转换。本节中,我们来看看如何将一个SQL查询转换为等价的pandas操作。

问题2A:预测SQL查询的输出

以下是SQL查询,我们需要预测其输出。

SELECT x + y
FROM trees
WHERE species = 'M';

步骤解析:

  1. WHERE species = 'M' 选择species列为'M'的行。
  2. SELECT x + y 计算选中行的xy列之和。

预测输出:

  • 对于species为'M'的行(索引0、1、4),计算x + y的值。

运行代码验证预测:

query("SELECT x + y FROM trees WHERE species = 'M';")

问题2B:转换为pandas操作

现在,将上述SQL查询转换为pandas操作。

以下是转换后的pandas代码:

trees[trees['species'] == 'M']['x'] + trees[trees['species'] == 'M']['y']

或者,可以分步操作:

# 选择species为'M'的行
filtered_trees = trees[trees['species'] == 'M']
# 计算x和y列之和
result = filtered_trees['x'] + filtered_trees['y']
result

问题3:复杂pandas操作与SQL转换

上一节我们处理了简单的SQL到pandas的转换。本节中,我们来看看一个更复杂的pandas操作及其SQL转换。

问题3A:预测复杂pandas语句的输出

以下是复杂的pandas语句,我们需要预测其输出。

cd = species[species['species'] == 'maple']['code'].iloc[0]
trees[trees['species'] == cd]['tree']

步骤解析:

  1. species[species['species'] == 'maple']['code']species表中选择species为'maple'的行,并提取code列。
  2. .iloc[0] 获取该列的第一个值(即'M'),并存储在变量cd中。
  3. trees[trees['species'] == cd] 选择trees表中species列等于cd(即'M')的行。
  4. ['tree'] 从结果中提取tree列。

预测输出:

  • 变量cd的值为'M'。
  • 最终输出为species为'M'的行的tree列值(即'A'、'B'、'E')。

运行代码验证预测:

cd = species[species['species'] == 'maple']['code'].iloc[0]
trees[trees['species'] == cd]['tree']

问题3B:转换为SQL查询

现在,将上述pandas操作转换为SQL查询。

以下是转换后的SQL查询:

SELECT tree
FROM trees
WHERE species = 'M';

使用query函数执行该SQL语句:

query("SELECT tree FROM trees WHERE species = 'M';")

问题4:排序操作

上一节我们处理了复杂的数据筛选。本节中,我们来看看如何在SQL和pandas中进行排序操作。

问题4A:预测排序SQL查询的输出

以下是SQL查询,我们需要预测其输出。

SELECT species
FROM trees
ORDER BY priority DESC;

步骤解析:

  1. ORDER BY priority DESCpriority列降序排序。
  2. SELECT species 选择species列。

预测输出:

  • priority降序排列的species列值。

运行代码验证预测:

query("SELECT species FROM trees ORDER BY priority DESC;")

问题4B:转换为pandas操作

现在,将上述SQL查询转换为pandas操作。

以下是转换后的pandas代码:

trees.sort_values(by='priority', ascending=False)['species']

问题5:限制结果数量

上一节我们介绍了排序操作。本节中,我们来看看如何在SQL和pandas中限制结果数量。

问题5A:预测带限制的SQL查询的输出

以下是SQL查询,我们需要预测其输出。

SELECT tree, priority
FROM trees
ORDER BY priority DESC
LIMIT 1;

步骤解析:

  1. ORDER BY priority DESCpriority降序排序。
  2. LIMIT 1 仅返回第一行。
  3. SELECT tree, priority 选择treepriority列。

预测输出:

  • 返回priority最高的行的treepriority值。

运行代码验证预测:

query("SELECT tree, priority FROM trees ORDER BY priority DESC LIMIT 1;")

问题5B:转换为pandas操作

现在,将上述SQL查询转换为pandas操作。

以下是转换后的pandas代码:

trees.sort_values(by='priority', ascending=False)[['tree', 'priority']].iloc[0]

问题6:聚合函数

上一节我们学习了如何限制结果数量。本节中,我们来看看如何在SQL和pandas中使用聚合函数。

问题6A:预测聚合SQL查询的输出

以下是SQL查询,我们需要预测其输出。

SELECT COUNT(species) AS c1, COUNT(DISTINCT species) AS c2
FROM trees;

步骤解析:

  1. COUNT(species) 计算species列的总行数。
  2. COUNT(DISTINCT species) 计算species列中不同值的数量。

预测输出:

  • c1为总行数(5)。
  • c2为不同species的数量(2)。

运行代码验证预测:

query("SELECT COUNT(species) AS c1, COUNT(DISTINCT species) AS c2 FROM trees;")

问题6B:转换为pandas操作

现在,将上述SQL查询转换为pandas操作。

以下是转换后的pandas代码:

c1 = len(trees['species'])
c2 = len(trees['species'].value_counts())
pd.DataFrame({'c1': [c1], 'c2': [c2]})

问题7:分组聚合

上一节我们介绍了基本的聚合函数。本节中,我们来看看如何在SQL和pandas中进行分组聚合操作。

问题7A:预测分组聚合SQL查询的输出

以下是SQL查询,我们需要预测其输出。

SELECT species, COUNT(species) AS count, AVG(diameter) AS size
FROM trees
GROUP BY species
ORDER BY species;

步骤解析:

  1. GROUP BY speciesspecies列分组。
  2. COUNT(species) 计算每组的行数。
  3. AVG(diameter) 计算每组diameter列的平均值。
  4. ORDER BY speciesspecies列排序。

预测输出:

  • 对于每个species,显示其行数和平均diameter

运行代码验证预测:

query("SELECT species, COUNT(species) AS count, AVG(diameter) AS size FROM trees GROUP BY species ORDER BY species;")

问题7B:转换为pandas操作

现在,将上述SQL查询转换为pandas操作。

以下是转换后的pandas代码:

trees.groupby('species').agg(count=('species', 'size'), size=('diameter', 'mean')).reset_index()

总结

本节课中,我们一起学习了SQL与pandas之间的转换方法。通过练习,我们掌握了如何将pandas操作转换为SQL查询,以及如何将SQL查询转换为pandas操作。这些技能将帮助我们在数据处理中灵活选择适合的工具,提高工作效率。

最后,记得关闭数据库连接:

conn.close()

030:绘图1-1 🎨

在本节课中,我们将要学习使用Python进行数据可视化的基础知识。我们将重点介绍如何利用Pandas和Matplotlib库来创建饼图、条形图和散点图。通过本教程,你将掌握生成和美化基本图表的核心技能。


导入必要的库

在开始绘图之前,我们需要导入一些必要的Python库。这些库将帮助我们处理数据和创建图表。

import pandas as pd
import sqlite3
import matplotlib.pyplot as plt
import os

创建饼图 🥧

上一节我们介绍了必要的库,本节中我们来看看如何创建一个简单的饼图。我们将从一个Pandas Series开始。

首先,我们创建一个包含数据的Series。

s = pd.Series([5000000, 3000000, 2000000])
print(s)

接下来,我们使用这个Series来创建一个饼图。

s.plot.pie()

生成的饼图可能看起来不太理想,例如标签太小且不明确。我们可以通过调整Matplotlib的全局参数来改善。

以下是调整字体大小的方法:

import matplotlib
matplotlib.rcParams['font.size'] = 18
s.plot.pie()

为了使标签更有意义,我们可以使用字典来创建Series,从而指定每个扇区的标签。

s = pd.Series({'Police': 5000000, 'Fire': 2000000, 'Schools': 3000000})
print(s)
s.plot.pie()

我们还可以在绘图时设置图表的标题。

s.plot.pie(ylabel='City Spending')

创建条形图 📊

虽然饼图可以展示部分与整体的关系,但条形图通常能更清晰地比较不同类别的数值。本节我们将学习如何创建和美化条形图。

使用之前创建的Series,我们可以轻松地生成一个垂直条形图。

s.plot.bar()

为了改善可读性,我们可以将数值单位转换为“百万”,并相应地调整Y轴标签。

s_in_millions = s / 1000000
ax = s_in_millions.plot.bar()
ax.set_ylabel('Dollars (in millions)')
ax.set_title('City Spending')

有时,水平条形图可能更适合展示数据。以下是创建水平条形图的方法:

ax = s_in_millions.plot.barh()
ax.set_xlabel('Dollars (in millions)')
ax.set_ylabel('Department')
ax.set_title('City Spending by Department')

我们可以通过多种方式自定义图表的外观,例如更改颜色、添加网格线或移除不必要的边框。

以下是更改条形颜色的示例:

ax = s_in_millions.plot.barh(color='k')  # 'k' 代表黑色

以下是移除图表顶部和右侧边框的示例:

ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

使用函数标准化图表创建

为了在多个图表中保持一致的样式,我们可以创建一个函数来生成具有预设格式的坐标轴。这在准备用于出版物的图表时特别有用。

以下是一个创建标准化子图的函数示例:

def get_ax(height=3):
    fig, ax = plt.subplots(figsize=(4, height))
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    return ax

然后,我们可以在创建图表时使用这个函数返回的坐标轴对象。

ax = get_ax(height=4.5)
s_in_millions.plot.bar(ax=ax, color='0.4') # 使用灰度
ax.set_ylabel('Dollars (in millions)')
ax.set_title('Standardized City Spending Chart')

从数据库创建图表 🗃️

数据可视化常常需要从数据库等外部源获取数据。本节我们将学习如何连接SQLite数据库,查询数据,并根据结果创建图表。

首先,我们连接到数据库并查看其中的表。

db_path = 'bus.db'
if not os.path.exists(db_path):
    raise FileNotFoundError(f"Database file not found at {db_path}")
conn = sqlite3.connect(db_path)

我们查询数据库,找出每日乘车人数最多的公交线路。

query = """
SELECT route, SUM(daily_boardings) AS total_boardings
FROM boardings
GROUP BY route
ORDER BY total_boardings DESC
"""
df = pd.read_sql(query, conn)
print(df.head())

将查询结果转换为Series并绘制前5条线路的条形图。

# 设置'route'列为索引
df.set_index('route', inplace=True)
s_top_routes = df['total_boardings'].head(5)
ax = s_top_routes.plot.bar()
ax.set_ylabel('Total Daily Boardings')
ax.set_title('Top 5 Bus Routes by Ridership')

为了更全面地展示数据,我们可以添加一个“其他”类别,汇总前5名之后的所有线路。

# 获取所有线路的数据
df_all = pd.read_sql(query, conn)
df_all.set_index('route', inplace=True)
s_all = df_all['total_boardings']

# 分离前5名和其余线路
s_top5 = s_all.head(5)
s_other = pd.Series({'Other': s_all.iloc[5:].sum()})

# 合并并绘图
s_to_plot = s_top5.append(s_other)
ax = s_to_plot.plot.bar()
ax.set_ylabel('Riders per day (in thousands)')
ax.set_title('Bus Top Routes vs. All Others')

最后,记得关闭数据库连接。

conn.close()

创建散点图 🔵

散点图用于展示两个连续变量之间的关系。本节我们将使用一个关于树木的模拟数据集来创建散点图。

首先,我们创建并查看数据。

trees = [
    {'age': 5, 'height': 20, 'diameter': 8},
    {'age': 10, 'height': 35, 'diameter': 15},
    {'age': 15, 'height': 45, 'diameter': 22},
    {'age': 20, 'height': 55, 'diameter': 30},
    {'age': 25, 'height': 65, 'diameter': 35}
]
df_trees = pd.DataFrame(trees)
print(df_trees)

现在,我们来创建散点图,探索树木年龄与直径之间的关系。

df_trees.plot.scatter(x='age', y='diameter', title='Tree Age vs. Diameter')

我们也可以查看年龄与高度的关系。

df_trees.plot.scatter(x='age', y='height', title='Tree Age vs. Height')

本节课中我们一起学习了使用Pandas和Matplotlib进行数据可视化的基础。我们涵盖了饼图、条形图和散点图的创建与基本美化,并演示了如何从数据库查询数据并可视化。掌握这些技能是进行有效数据分析和沟通的关键一步。

031:绘图进阶

在本节课中,我们将学习如何为散点图添加更多细节,例如通过颜色、大小和形状来编码额外数据维度。我们还将回顾线图的绘制方法,并学习如何美化图表,如设置坐标轴标签和刻度。


快速回顾:绘制简单条形图

上一节我们介绍了绘图的基本概念,本节中我们来看看如何绘制一个简单的条形图。这非常简单,我们通过Pandas连接到Matplotlib来创建图表。

要创建一个基本的条形图,你需要一个Series,然后调用其.plot方法并指定图表类型。

my_series.plot(kind='bar')

条形图的坐标轴

以下是条形图中坐标轴的含义:

  • X轴:对应Series的索引。
  • Y轴:对应Series的数值。

回顾:树木数据的散点图

上一节我们使用了一个关于树木的DataFrame来创建散点图。现在,我们想在这个散点图中加入更多信息,例如树木的直径。

我们首先加载数据并创建基础散点图。

import pandas as pd
import matplotlib.pyplot as plt

# 创建示例数据
trees_data = [
    {'age': 1, 'height': 2, 'diameter': 0.8},
    {'age': 2, 'height': 3, 'diameter': 1.0},
    {'age': 3, 'height': 4, 'diameter': 1.2},
    {'age': 4, 'height': 5, 'diameter': 1.4},
    {'age': 5, 'height': 6, 'diameter': 1.6}
]
trees_df = pd.DataFrame(trees_data)

# 基础散点图:年龄 vs. 高度
trees_df.plot(kind='scatter', x='age', y='height')

在散点图中,X轴和Y轴是必须的,它们直接显示数值。除此之外,我们还可以通过点的颜色大小形状来传达额外信息。


控制散点图外观:颜色

我们可以通过color(或缩写c)参数来改变点的颜色。颜色可以是名称(如'red')或缩写(如'r'代表红色,'b'代表蓝色,'k'代表黑色)。

更强大的是,我们可以将一个数值序列(如直径)传递给color参数,Matplotlib会自动根据数值大小使用灰度表示。

# 使用灰度表示直径
trees_df.plot(kind='scatter', x='age', y='height', c=trees_df['diameter'])

使用颜色(尤其是灰度)表示连续数据时需要注意:

  1. 在白色背景上,接近白色的点可能看不见。可以通过设置vmin参数来调整灰度范围的下限。
  2. 人类很难精确区分3-4种以上的灰度 shades。
  3. 需要考虑色盲用户的体验。
  4. 通常需要图例来解释颜色含义。
# 调整灰度范围,确保所有点可见
min_diameter = trees_df['diameter'].min()
trees_df.plot(kind='scatter', x='age', y='height', c=trees_df['diameter'], vmin=min_diameter-0.5)

控制散点图外观:大小

我们可以通过s参数来改变点的大小。可以给一个固定值,也可以传递一个数值序列,让点的大小对应数据值。

# 点的大小对应直径(乘以一个系数使其更明显)
trees_df.plot(kind='scatter', x='age', y='height', s=trees_df['diameter']*25)

使用大小编码数据时需谨慎,因为人类对面积变化的感知不是线性的(例如,数值翻倍会使点的面积变为四倍),这可能导致误解。


控制散点图外观:形状

我们可以通过marker参数来改变点的形状。Matplotlib提供了多种标记样式,例如'.'(点),'s'(方形),'*'(星形),'^'(三角形)等。

# 改变点的形状为三角形
trees_df.plot(kind='scatter', x='age', y='height', marker='^')

使用不同形状通常更适合表示分类数据(如不同的物种)。对于像直径这样的连续数据,用形状编码可能不直观,除非将其分成几个类别。

注意:应避免同时使用颜色和大小来编码完全相同的数据维度,这会让图表显得冗余,并可能误导观众。


实战:鸢尾花数据集分析

现在让我们分析一个真实数据集:经典的鸢尾花数据集。它包含三种鸢尾花(Setosa, Versicolor, Virginica)的花萼和花瓣测量数据。

我们首先加载数据并查看其类别。

# 加载鸢尾花数据集(假设文件名为iris.data)
column_names = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'class']
iris_df = pd.read_csv('iris.data', names=column_names)

# 查看数据集中有哪些类别
flower_classes = set(iris_df['class'])
print(flower_classes)

我们的目标是在同一张图上绘制所有类别的数据,并用颜色和形状区分它们。

# 准备颜色和形状列表
colors = ['red', 'green', 'blue']
markers = ['^', '*', 'o']  # 三角形,星形,圆形

plot_area = None  # 初始化绘图区域

# 循环每个类别,将数据添加到同一张图中
for flower_class, color, marker in zip(flower_classes, colors, markers):
    # 筛选当前类别的数据
    class_data = iris_df[iris_df['class'] == flower_class]
    # 绘制散点图,并指定颜色和形状
    plot_area = class_data.plot(kind='scatter', x='petal_length', y='petal_width',
                                color=color, marker=marker, s=75,
                                ax=plot_area,  # 关键:将点添加到现有绘图区域
                                label=flower_class)

# 显示图例
plt.legend()
plt.show()

关键点:为了将所有数据画在同一张图上,我们在循环中使用了ax=plot_area参数,并将.plot方法返回的新绘图区域赋值回plot_area变量,以便下次迭代时继续添加数据。


绘制线图

线图常用于展示数据随时间或其他连续变量的趋势。绘制方法与条形图、散点图类似。

从Series绘制线图

对于一个Series,.plot(kind='line')会将其值按索引顺序连接起来。

# 创建一个Series
s = pd.Series([100, 300, 200, 400, 50], index=[0, 1, 2, 23, 24])
s.plot(kind='line')

注意,如果索引不是按顺序排列的,线图可能会产生奇怪的折线。可以使用.sort_index()对Series进行排序。

s_sorted = s.sort_index()
s_sorted.plot(kind='line')

从DataFrame绘制线图(以温度数据为例)

我们可以直接绘制整个DataFrame,Pandas会为每一列画一条线。

# 示例:月平均高低温数据
data = {
    'high': [26, 30, 42, 58, 70, 78, 82, 80, 72, 58, 42, 30],
    'low': [13, 16, 26, 38, 49, 58, 63, 61, 53, 41, 29, 18]
}
temp_df = pd.DataFrame(data)

# 绘制DataFrame,每列一条线
ax = temp_df.plot(kind='line')
ax.set_xlabel('Month')
ax.set_ylabel('Temperature (F)')

美化线图:设置刻度和标签

默认的X轴刻度是数字(0-11),我们希望将其改为月份名称。

# 设置X轴刻度位置和标签
month_ticks = range(12)
month_labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

ax.set_xticks(month_ticks)
ax.set_xticklabels(month_labels, rotation=45)  # 旋转标签45度,避免重叠

数据转换示例:华氏度转摄氏度

对DataFrame进行数学运算是逐元素进行的,非常方便。

# 将温度从华氏度转换为摄氏度
temp_df_celsius = (temp_df - 32) * 5 / 9
# 重命名列
temp_df_celsius.columns = ['high_c', 'low_c']
# 绘制摄氏度图表
temp_df_celsius.plot(kind='line')

综合案例:标普500投资回报可视化

最后,我们看一个综合案例:计算并可视化从1970年开始投资标普500的累积财富增长。

# 加载标普500历史回报数据
sp500_df = pd.read_csv('sp500.csv')
print(sp500_df.head())

# 计算累积回报(累积乘积)
sp500_df['cumulative_return'] = (sp500_df['return'] / 100 + 1).cumprod()

# 计算财富增长(假设初始投资1000美元)
initial_investment = 1000
sp500_df['wealth'] = initial_investment * sp500_df['cumulative_return']

# 将‘year’列设置为索引,以便作为X轴
sp500_df.set_index('year', inplace=True)

# 绘制财富增长曲线
ax = sp500_df['wealth'].plot(kind='line')
ax.set_ylabel('Wealth (USD)')
ax.set_title('Growth of $1000 Investment in S&P 500')
plt.show()

这个案例展示了如何从原始数据开始,通过计算衍生列,最终生成一个有意义的线图来讲述数据故事。


本节课中我们一起学习了如何增强散点图的信息量,包括用颜色、大小和形状编码数据。我们还回顾并深化了线图的绘制技巧,学习了如何设置坐标轴、刻度和标签来制作更清晰、更专业的图表。通过鸢尾花数据集和标普500的案例,我们练习了从数据加载、处理到可视化的完整流程。

032:随机性与模拟 🎲

在本节课中,我们将要学习Python中的随机性概念及其在数据编程中的应用。我们将探讨如何生成随机数据,如何通过模拟来分析概率事件,以及如何避免编程中与随机性相关的常见错误。


概述 📋

随机性在计算机科学和数据科学中扮演着重要角色,从游戏开发到安全加密,再到复杂系统的模拟,都离不开随机数的生成与分析。本节课程将介绍Python中生成随机数的基本方法,并通过实际案例展示如何利用随机性进行数据模拟和概率分析。


随机数生成 🎯

上一节我们介绍了课程的整体安排,本节中我们来看看如何在Python中生成随机数。Python内置的random模块和NumPy库中的random子模块都提供了强大的随机数生成功能。

使用choice函数

choice函数允许我们从给定的列表中随机选择一个元素。以下是其基本用法:

from numpy.random import choice

options = ['石头', '布', '剪刀']
selected = choice(options)
print(selected)

运行上述代码,每次都会从['石头', '布', '剪刀']中随机选择一个元素输出。

生成多个随机选择

我们可以通过指定size参数来一次性生成多个随机选择,结果会以NumPy数组的形式返回。

multiple_choices = choice(options, size=5)
print(multiple_choices)

这段代码会生成一个包含5个随机选择的数组,例如['布', '石头', '剪刀', '布', '石头']

创建随机序列

我们可以将生成的随机数组转换为Pandas Series,以便进行更复杂的数据分析。

import pandas as pd

random_series = pd.Series(choice(options, size=5))
print(random_series)

这将创建一个索引为0到4、值为随机选择的Pandas Series。


多维随机数据 📊

除了生成一维序列,我们还可以创建多维的随机数据数组,这在模拟多组实验时非常有用。

生成二维数组

通过向size参数传递一个元组,我们可以指定生成数组的行数和列数。

# 生成一个5行2列的二维数组
rows = 5
cols = 2
array_2d = choice(options, size=(rows, cols))
print(array_2d)

生成的数组结构如下:

  • : 垂直方向(从上到下)
  • : 水平方向(从左到右)

访问多维数组元素

访问多维数组元素的方式与访问列表的列表类似。

# 访问第3行(索引2),第1列(索引0)的元素
element = array_2d[2, 0]
print(element)

创建随机数据框

我们可以直接将生成的二维数组转换为Pandas DataFrame,以便进行结构化数据分析。

df = pd.DataFrame(choice(options, size=(rows, cols)))
print(df)

分析随机性偏差 ⚖️

生成随机数据后,一个自然的问题是:这个过程是否公平?我们如何检验随机性是否存在偏差?

计算频率分布

我们可以使用Pandas的value_counts方法来统计每个选项出现的次数。

# 生成一个较大的随机序列
large_series = pd.Series(choice(options, size=300000))

# 计算各选项出现的次数
value_counts = large_series.value_counts()
print(value_counts)

在理想情况下,如果选择是均匀随机的,每个选项出现的次数应该大致相等。

可视化频率分布

为了更直观地观察分布,我们可以将计数结果绘制成条形图。

import matplotlib.pyplot as plt

# 按指定顺序(石头、布、剪刀)获取计数并绘图
ordered_counts = value_counts[['石头', '布', '剪刀']]
ordered_counts.plot(kind='bar')
plt.show()

通过观察条形图,我们可以判断随机生成过程是否存在明显的偏差。

控制选择概率

choice函数允许我们通过p参数指定每个选项被选中的概率。

# 指定概率:石头70%,布20%,剪刀10%
probabilities = [0.7, 0.2, 0.1]
biased_choice = choice(options, p=probabilities, size=10)
print(biased_choice)

注意:概率列表中的所有值之和必须为1。


随机数种子与可重现性 🌱

在编程和数据分析中,有时我们需要能够重现相同的“随机”结果,这时就需要使用随机数种子。

设置随机数种子

通过设置种子,我们可以让随机数生成器从同一个起点开始,从而每次生成相同的序列。

from numpy.random import seed

seed(220)  # 设置种子为220
print(choice([0, 1, 2, 3, 4, 5], size=3))

无论运行多少次,只要种子相同,生成的随机序列就会相同。

使用时间作为种子

在实际应用中,我们经常使用当前时间作为种子,这样每次运行都能得到不同的序列,但同时也能够记录种子值以便重现。

import time

now = int(time.time())  # 获取当前时间戳
seed(now)
print(f"种子: {now}")
print(choice([0, 1, 2, 3, 4, 5], size=3))

模拟与概率分析 📈

随机性的一个重要应用是进行概率模拟,帮助我们理解某些事件发生的可能性。

抛硬币模拟

假设我们想模拟抛100次硬币,并统计正面朝上的次数。

flips = 100
trials = 10000

# 模拟10000次实验,每次抛100次硬币
results = choice(['正面', '反面'], size=(trials, flips))
df = pd.DataFrame(results)

# 统计每次实验中正面朝上的次数
heads_count = df.apply(lambda row: (row == '正面').sum(), axis=1)

分析极端结果

我们可以计算在100次抛掷中,正面朝上次数少于40次或多于60次的实验比例。

extreme_cases = heads_count[(heads_count <= 40) | (heads_count >= 60)]
extreme_percentage = len(extreme_cases) / trials * 100
print(f"极端结果比例: {extreme_percentage:.2f}%")

连续事件模拟

我们还可以模拟更复杂的情况,例如在16次抛掷中出现连续7次或更多正面的概率。

def has_consecutive_heads(sequence, n=7):
    """检查序列中是否包含连续n个正面"""
    joined = ''.join(sequence)
    return joined.find('H' * n) != -1

consecutive_count = 0
for _ in range(trials):
    trial = choice(['H', 'T'], size=16)
    if has_consecutive_heads(trial, 7):
        consecutive_count += 1

probability = consecutive_count / trials * 100
print(f"连续7次正面概率: {probability:.2f}%")

总结 🎓

本节课中我们一起学习了Python中随机性的核心概念和应用。我们掌握了如何使用choice函数生成随机数据,如何创建和分析多维随机序列,以及如何通过设置种子确保结果的可重现性。我们还通过抛硬币的案例,学习了如何利用随机模拟来分析概率事件,评估结果的统计显著性。

理解随机性对于数据编程至关重要,它不仅帮助我们创建更真实的模拟,还能让我们在分析数据时保持批判性思维,区分真正的模式与随机噪声。

posted @ 2026-03-26 13:19  布客飞龙V  阅读(3)  评论(0)    收藏  举报