Python 数据分析(PYDA)第三版(一)

原文:wesmckinney.com/book/

译者:飞龙

协议:CC BY-NC-SA 4.0

关于开放版本

第 3 版的《Python 数据分析》现在作为“开放获取”HTML 版本在此网站wesmckinney.com/book上提供,除了通常的印刷和电子书格式。该版本最初于 2022 年 8 月出版,将在未来几个月和年份内定期修正勘误。如果您发现任何勘误,请在此处报告

一般来说,本网站的内容不得复制或复制。代码示例采用 MIT 许可证,可在GitHubGitee上找到,以及支持数据集。

如果您发现本书的在线版本有用,请考虑订购纸质版本无 DRM 的电子书(PDF 和 EPUB 格式)以支持作者。

本书的网络版本是使用Quarto 出版系统创建的。

第 3 版的新内容是什么?

本书已更新到 pandas 2.0.0 和 Python 3.10。第 2 版和第 3 版之间的变化主要集中在将内容与自 2017 年以来 pandas 的变化保持最新。

更新历史

本网站将定期更新,以提供新的早期发布内容,并在出版后修复勘误。

  • 2023 年 4 月 12 日:更新到 pandas 2.0.0 并修复一些代码示例。

  • 2022 年 10 月 19 日:修复表格链接并添加eBooks.com 链接

  • 2022 年 9 月 20 日:最终出版后的网站更新,包括修复了一些小错误。

  • 2022 年 7 月 22 日:在今年夏天后期出版之前,将副本编辑和其他改进纳入“QC1”生产阶段。

  • 2022 年 5 月 18 日:更新开放获取版本的所有章节。包括来自技术审查反馈(谢谢!)的编辑,第三版的致谢以及其他准备工作,使本书准备好在 2022 年晚些时候印刷。

  • 2022 年 2 月 13 日:更新开放获取版本,包括第 7 至第十章。

  • 2022 年 1 月 23 日:首次开放获取版本,包括第 1 至第六章。

正文

前言

原文:wesmckinney.com/book/preface

译者:飞龙

协议:CC BY-NC-SA 4.0

此开放访问网络版本的《Python 数据分析第三版》现已作为印刷版和数字版的伴侣提供。如果您发现任何勘误,请在此处报告。请注意,由 Quarto 生成的本站点的某些方面与 O’Reilly 的印刷版和电子书版本的格式不同。

如果您发现本书的在线版本有用,请考虑订购纸质版无 DRM 的电子书以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。

本书的第一版于 2012 年出版,当时 Python 的开源数据分析库,尤其是 pandas,非常新且快速发展。到了 2016 年和 2017 年写第二版时,我不仅需要将书更新为 Python 3.6(第一版使用 Python 2.7),还需要更新 pandas 在过去五年中发生的许多变化。现在是 2022 年,Python 语言的变化较少(我们现在使用 Python 3.10,3.11 将于 2022 年底发布),但 pandas 仍在不断发展。

在这第三版中,我的目标是将内容与当前版本的 Python、NumPy、pandas 和其他项目保持同步,同时对于讨论近几年出现的较新的 Python 项目保持相对保守。由于这本书已成为许多大学课程和职业人士的重要资源,我将尽量避免讨论可能在一两年内过时的主题。这样,纸质副本在 2023 年、2024 年甚至更久以后也不会太难理解。

第三版的一个新特性是托管在我的网站上的开放访问在线版本,网址为wesmckinney.com/book,可作为印刷版和数字版的所有者的资源和便利。我打算保持那里的内容相对及时更新,因此如果您拥有纸质书并遇到某些问题,请在那里查看最新的内容更改。

本书中使用的约定

本书中使用以下排版约定:

斜体

指示新术语、URL、电子邮件地址、文件名和文件扩展名。

等宽

用于程序清单,以及段落内引用程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。

等宽粗体

显示用户应按照字面意思键入的命令或其他文本。

<等宽斜体>

显示应替换为用户提供的值或由上下文确定的值的文本。

提示:

此元素表示提示或建议。

注意:

此元素表示一般说明。

警告:

此元素表示警告或注意事项。

使用代码示例

您可以在本书的 GitHub 存储库中找到每章的数据文件和相关材料,网址为github.com/wesm/pydata-book,该存储库在 Gitee 上有镜像(供无法访问 GitHub 的用户使用),网址为gitee.com/wesmckinn/pydata-book

这本书旨在帮助您完成工作。一般来说,如果本书提供示例代码,您可以在程序和文档中使用它。除非您复制了代码的大部分内容,否则无需征得我们的许可。例如,编写一个使用本书中几个代码块的程序不需要许可。销售或分发 O’Reilly 图书中的示例需要许可。通过引用本书回答问题并引用示例代码不需要许可。将本书中大量示例代码合并到产品文档中需要许可。

我们感谢,但不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Python for Data Analysis by Wes McKinney(O’Reilly)。版权所有 2022 年 Wes McKinney,978-1-098-10403-0。”

如果您觉得您对代码示例的使用超出了合理使用范围或上述许可,请随时通过 permissions@oreilly.com 与我们联系。

致谢

这项工作是多年来与世界各地许多人进行富有成果的讨论和合作的成果。我想感谢其中的一些人。

追悼:约翰·D·亨特(1968-2012)

我们亲爱的朋友和同事约翰·D·亨特在 2012 年 8 月 28 日与结肠癌搏斗后去世。这发生在我完成本书第一版最终手稿后不久。

约翰在 Python 科学和数据社区的影响和遗产难以估量。除了在 21 世纪初开发 matplotlib(当时 Python 并不那么流行)之外,他还帮助塑造了一代关键的开源开发者文化,这些开发者已经成为我们现在经常视为理所当然的 Python 生态系统的支柱。

我很幸运在 2010 年 1 月早期与约翰建立了联系,就在发布 pandas 0.1 后不久。他的启发和指导帮助我在最黑暗的时刻推动前进,实现了我对 pandas 和 Python 作为一流数据分析语言的愿景。

John 与 Fernando Pérez 和 Brian Granger 非常亲近,他们是 IPython、Jupyter 和 Python 社区中许多其他倡议的先驱。我们曾希望一起合作写一本书,但最终我成为了拥有最多空闲时间的人。我相信他会为我们在过去九年中所取得的成就感到自豪,无论是作为个人还是作为一个社区。

致谢第三版(2022 年)

自从我开始写这本书的第一版以来已经有十多年了,自从我最初作为 Python 程序员开始我的旅程以来已经有 15 年了。那时发生了很多变化!Python 已经从一个相对小众的数据分析语言发展成为最受欢迎、最广泛使用的语言,支持着数据科学、机器学习和人工智能工作的多数(如果不是大多数!)。

自 2013 年以来,我并没有积极参与 pandas 开源项目,但其全球开发者社区仍在蓬勃发展,成为以社区为中心的开源软件开发模式的典范。许多处理表格数据的“下一代”Python 项目直接模仿 pandas 的用户界面,因此该项目已经对 Python 数据科学生态系统未来的发展轨迹产生了持久的影响。

希望这本书能继续为想要学习如何在 Python 中处理数据的学生和个人提供宝贵的资源。

我特别感谢 O’Reilly 允许我在我的网站wesmckinney.com/book上发布这本书的“开放获取”版本,希望它能触达更多人,并帮助扩大数据分析领域的机会。J.J. Allaire 在帮助我将这本书从 Docbook XML“移植”到Quarto时是一个救星,Quarto 是一个出色的新科学技术出版系统,适用于印刷和网络。

特别感谢我的技术审阅者 Paul Barry、Jean-Christophe Leyder、Abdullah Karasan 和 William Jamir,他们的详细反馈极大地提高了内容的可读性、清晰度和可理解性。

致谢第二版(2017 年)

距离我在 2012 年 7 月完成这本书第一版手稿已经快五年了。很多事情发生了变化。Python 社区已经大幅增长,围绕它的开源软件生态系统也蓬勃发展。

如果不是 pandas 核心开发者们不懈的努力,这本书的新版将不会存在,他们已经将这个项目及其用户社区发展成为 Python 数据科学生态系统的支柱之一。这些人包括但不限于 Tom Augspurger、Joris van den Bossche、Chris Bartak、Phillip Cloud、gfyoung、Andy Hayden、Masaaki Horikoshi、Stephan Hoyer、Adam Klein、Wouter Overmeire、Jeff Reback、Chang She、Skipper Seabold、Jeff Tratner 和 y-p。

在撰写这本第二版时,我要感谢 O’Reilly 的工作人员在写作过程中耐心地帮助我。其中包括 Marie Beaugureau、Ben Lorica 和 Colleen Toporek。我再次有幸得到 Tom Augspurger、Paul Barry、Hugh Brown、Jonathan Coe 和 Andreas Müller 等杰出的技术审阅者的帮助。谢谢。

这本书的第一版已经被翻译成许多外语,包括中文、法语、德语、日语、韩语和俄语。翻译所有这些内容并让更广泛的受众获得是一项巨大且常常被忽视的工作。感谢您帮助更多世界上的人学习如何编程和使用数据分析工具。

在过去几年里,我很幸运地得到了 Cloudera 和 Two Sigma Investments 对我持续的开源开发工作的支持。随着开源软件项目相对于用户群体规模而言资源更加稀缺,企业为关键开源项目的开发提供支持变得越来越重要。这是正确的做法。

致谢第一版(2012)

如果没有许多人的支持,我很难写出这本书。

在 O’Reilly 的工作人员中,我非常感激我的编辑 Meghan Blanchette 和 Julie Steele,他们在整个过程中指导我。Mike Loukides 也在提案阶段与我合作,帮助使这本书成为现实。

我得到了许多人的技术审查。特别是 Martin Blais 和 Hugh Brown 在改进书中的示例、清晰度和组织方面提供了极大帮助。James Long,Drew Conway,Fernando Pérez,Brian Granger,Thomas Kluyver,Adam Klein,Josh Klein,Chang She 和 Stéfan van der Walt 分别审查了一个或多个章节,从许多不同的角度提供了有针对性的反馈。

我从数据社区的朋友和同事那里得到了许多出色的示例和数据集的创意,其中包括:Mike Dewar,Jeff Hammerbacher,James Johndrow,Kristian Lum,Adam Klein,Hilary Mason,Chang She 和 Ashley Williams。

当然,我要感谢许多开源科学 Python 社区的领导者,他们为我的开发工作奠定了基础,并在我写这本书时给予了鼓励:IPython 核心团队(Fernando Pérez,Brian Granger,Min Ragan-Kelly,Thomas Kluyver 等),John Hunter,Skipper Seabold,Travis Oliphant,Peter Wang,Eric Jones,Robert Kern,Josef Perktold,Francesc Alted,Chris Fonnesbeck 等等。还有许多其他人,无法一一列举。还有一些人在这个过程中提供了大量的支持、想法和鼓励:Drew Conway,Sean Taylor,Giuseppe Paleologo,Jared Lander,David Epstein,John Krowas,Joshua Bloom,Den Pilsworth,John Myles-White 等等。

我还要感谢一些在我成长过程中的人。首先是我的前 AQR 同事,多年来一直在我的 pandas 工作中支持我:Alex Reyfman,Michael Wong,Tim Sargen,Oktay Kurbanov,Matthew Tschantz,Roni Israelov,Michael Katz,Ari Levine,Chris Uga,Prasad Ramanan,Ted Square 和 Hoon Kim。最后,我的学术导师 Haynes Miller(MIT)和 Mike West(Duke)。

2014 年,我得到了 Phillip Cloud 和 Joris van den Bossche 的重要帮助,更新了书中的代码示例,并修复了由于 pandas 变化而导致的一些不准确之处。

在个人方面,Casey 在写作过程中提供了宝贵的日常支持,容忍我在本已过度忙碌的日程表上拼凑出最终草稿时的起起伏伏。最后,我的父母 Bill 和 Kim 教导我始终追随梦想,永不妥协。

一、初步

原文:wesmckinney.com/book/preliminaries

译者:飞龙

协议:CC BY-NC-SA 4.0

此开放访问网络版本的《Python 数据分析第三版》现已作为印刷版和数字版的伴侣提供。如果您发现任何勘误,请在此处报告。请注意,由 Quarto 生成的本站点的某些方面与 O’Reilly 的印刷版和电子书版本的格式不同。

如果您发现本书的在线版本有用,请考虑订购纸质版无 DRM 的电子书以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。

这本书关注的是在 Python 中操纵、处理、清理和处理数据的基本原理。我的目标是为 Python 编程语言及其面向数据的库生态系统和工具提供指南,使您能够成为一名有效的数据分析师。虽然书名中有“数据分析”一词,但重点特别放在 Python 编程、库和工具上,而不是数据分析方法论。这是您进行数据分析所需的 Python 编程。

在我 2012 年首次出版这本书之后不久,人们开始使用“数据科学”这个术语作为从简单的描述性统计到更高级的统计分析和机器学习等各种内容的总称。自那时以来,用于进行数据分析(或数据科学)的 Python 开源生态系统也显著扩展。现在有许多其他专门关注这些更高级方法的书籍。我希望这本书能够作为足够的准备,使您能够转向更具领域特定性的资源。

注意:

有些人可能将本书的大部分内容描述为“数据操纵”,而不是“数据分析”。我们还使用整理整理这些术语来指代数据操纵。*### 什么样的数据?

当我说“数据”时,我确切指的是什么?主要关注的是结构化数据,这是一个故意模糊的术语,包括许多不同形式的常见数据,例如:

  • 表格或类似电子表格的数据,其中每列可能是不同类型(字符串、数字、日期或其他)。这包括通常存储在关系数据库或制表符或逗号分隔文本文件中的各种数据。

  • 多维数组(矩阵)。

  • 由关键列相互关联的多个数据表(对 SQL 用户来说可能是主键或外键)。

  • 均匀或不均匀间隔的时间序列。

这绝不是一个完整的列表。即使可能并不总是明显,大部分数据集都可以转换为更适合分析和建模的结构化形式。如果不行,可能可以从数据集中提取特征到结构化形式。例如,一组新闻文章可以处理成一个词频表,然后用于执行情感分析。

像 Microsoft Excel 这样的电子表格程序的大多数用户,可能是世界上最广泛使用的数据分析工具,对这些数据类型并不陌生。*## 1.2 为什么选择 Python 进行数据分析?

对许多人来说,Python 编程语言具有很强的吸引力。自 1991 年首次亮相以来,Python 已成为最受欢迎的解释性编程语言之一,与 Perl、Ruby 等一起。自 2005 年左右以来,Python 和 Ruby 特别受欢迎,用于构建网站,使用它们众多的 Web 框架,如 Rails(Ruby)和 Django(Python)。这些语言通常被称为“脚本”语言,因为它们可以用于快速编写小程序或脚本来自动化其他任务。我不喜欢“脚本语言”这个术语,因为它带有一种暗示,即它们不能用于构建严肃的软件。出于各种历史和文化原因,在解释性语言中,Python 已经发展成一个庞大而活跃的科学计算和数据分析社区。在过去的 20 年里,Python 已经从一个前沿或“自担风险”的科学计算语言发展成为学术界和工业界数据科学、机器学习和通用软件开发中最重要的语言之一。

对于数据分析、交互式计算和数据可视化,Python 不可避免地会与其他广泛使用的开源和商业编程语言和工具进行比较,如 R、MATLAB、SAS、Stata 等。近年来,Python 改进的开源库(如 pandas 和 scikit-learn)使其成为数据分析任务的热门选择。结合 Python 在通用软件工程方面的整体实力,它是构建数据应用程序的主要语言的绝佳选择。

Python 作为胶水

Python 在科学计算中的成功部分在于轻松集成 C、C++和 FORTRAN 代码。大多数现代计算环境共享一组类似的传统 FORTRAN 和 C 库,用于进行线性代数、优化、积分、快速傅里叶变换等算法。许多公司和国家实验室使用 Python 将几十年的传统软件粘合在一起的故事也是如此。

许多程序由小部分代码组成,其中大部分时间都花在其中,大量“胶水代码”很少运行。在许多情况下,胶水代码的执行时间微不足道;最有价值的努力是在优化计算瓶颈上,有时通过将代码移动到像 C 这样的低级语言来实现。

解决“双语言”问题

在许多组织中,通常使用更专门的计算语言如 SAS 或 R 进行研究、原型设计和测试新想法,然后将这些想法移植为更大的生产系统的一部分,比如 Java、C#或 C++。人们越来越发现 Python 不仅适合用于研究和原型设计,也适合用于构建生产系统。当一个开发环境足够时,为什么要维护两个呢?我相信越来越多的公司会选择这条道路,因为让研究人员和软件工程师使用相同的编程工具集通常会带来重大的组织效益。

在过去的十年里,一些解决“双语言”问题的新方法出现了,比如 Julia 编程语言。在许多情况下,充分利用 Python 将需要使用低级语言如 C 或 C++编程,并创建 Python 绑定到该代码。也就是说,像 Numba 这样的“即时”(JIT)编译器技术提供了一种在 Python 编程环境中实现出色性能的方法,而无需离开 Python 编程环境。

为什么不用 Python?

虽然 Python 是构建许多种分析应用程序和通用系统的优秀环境,但也有一些用途不太适合 Python。

由于 Python 是一种解释性编程语言,通常大多数 Python 代码运行速度会比像 Java 或 C++这样的编译语言编写的代码慢得多。由于程序员时间通常比CPU 时间更有价值,许多人愿意做出这种权衡。然而,在具有非常低延迟或对资源利用要求苛刻的应用程序中(例如高频交易系统),花费时间以低级语言(但也低生产力)如 C++编程,以实现可能的最大性能,可能是值得的。

Python 可能是一个具有挑战性的语言,用于构建高度并发、多线程的应用程序,特别是具有许多 CPU 绑定线程的应用程序。造成这种情况的原因是它具有所谓的全局解释器锁(GIL),这是一种机制,防止解释器一次执行多个 Python 指令。GIL 存在的技术原因超出了本书的范围。虽然在许多大数据处理应用中,可能需要一组计算机集群来在合理的时间内处理数据集,但仍然存在一些情况,其中单进程、多线程系统是可取的。

这并不是说 Python 不能执行真正的多线程、并行代码。使用本地多线程(在 C 或 C++中)的 Python C 扩展可以在不受 GIL 影响的情况下并行运行代码,只要它们不需要经常与 Python 对象交互。

1.3 必要的 Python 库

对于那些对 Python 数据生态系统和本书中使用的库不太熟悉的人,我将简要介绍其中一些。

NumPy

NumPy,简称 Numerical Python,长期以来一直是 Python 中数值计算的基石。它提供了大多数涉及 Python 中数值数据的科学应用所需的数据结构、算法和库粘合剂。NumPy 包含,除其他内容外:

  • 快速高效的多维数组对象ndarray

  • 执行数组元素计算或数组之间的数学运算的函数

  • 用于读取和写入基于数组的数据集到磁盘的工具

  • 线性代数运算、傅里叶变换和随机数生成

  • 成熟的 C API,用于使 Python 扩展和本地 C 或 C++代码能够访问 NumPy 的数据结构和计算功能

除了 NumPy 为 Python 增加的快速数组处理功能外,它在数据分析中的主要用途之一是作为数据容器,在算法和库之间传递数据。对于数值数据,NumPy 数组比其他内置 Python 数据结构更有效地存储和操作数据。此外,使用低级语言(如 C 或 FORTRAN)编写的库可以在 NumPy 数组中存储的数据上操作,而无需将数据复制到其他内存表示中。因此,许多 Python 的数值计算工具要么将 NumPy 数组作为主要数据结构,要么针对与 NumPy 的互操作性。

pandas

pandas提供了高级数据结构和函数,旨在使处理结构化或表格数据变得直观和灵活。自 2010 年出现以来,它已经帮助 Python 成为一个强大和高效的数据分析环境。本书中将使用的 pandas 中的主要对象是 DataFrame,这是一个表格化的、以列为导向的数据结构,具有行和列标签,以及 Series,这是一个一维带标签的数组对象。

pandas 将 NumPy 的数组计算思想与电子表格和关系数据库(如 SQL)中发现的数据操作能力相结合。它提供了方便的索引功能,使您能够重新塑造、切片、执行聚合操作和选择数据子集。由于数据操作、准备和清理在数据分析中是如此重要,pandas 是本书的主要关注点之一。

作为背景,我在 2008 年初在 AQR Capital Management 期间开始构建 pandas,这是一家量化投资管理公司。当时,我有一套明确的要求,任何单一工具都无法很好地满足:

  • 具有带有标签轴的数据结构,支持自动或显式数据对齐——这可以防止由于数据不对齐和来自不同来源的不同索引数据而导致的常见错误

  • 集成的时间序列功能

  • 相同的数据结构处理时间序列数据和非时间序列数据

  • 保留元数据的算术操作和减少

  • 灵活处理缺失数据

  • 在流行数据库(例如基于 SQL 的数据库)中找到的合并和其他关系操作

我希望能够在一个地方完成所有这些事情,最好是在一种适合通用软件开发的语言中。Python 是这方面的一个很好的候选语言,但当时并不存在一个集成了这些功能的数据结构和工具集。由于最初构建是为了解决金融和业务分析问题,pandas 具有特别深入的时间序列功能和适用于处理由业务流程生成的时间索引数据的工具。

我在 2011 年和 2012 年的大部分时间里与我以前的 AQR 同事 Adam Klein 和 Chang She 一起扩展了 pandas 的功能。2013 年,我停止了日常项目开发的参与,pandas 自那时起已成为一个完全由社区拥有和维护的项目,全球范围内有超过两千名独特贡献者。

对于使用 R 语言进行统计计算的用户,DataFrame 这个名字将是熟悉的,因为该对象是根据类似的 R data.frame对象命名的。与 Python 不同,数据框内置于 R 编程语言及其标准库中。因此,pandas 中许多功能通常要么是 R 核心实现的一部分,要么是由附加包提供的。

pandas 这个名字本身来源于panel data,这是一个描述多维结构化数据集的计量经济学术语,也是对Python 数据分析这个短语的一种变换。

matplotlib

matplotlib是用于生成图表和其他二维数据可视化的最流行的 Python 库。最初由 John D. Hunter 创建,现在由一个庞大的开发团队维护。它专为创建适合出版的图表而设计。虽然 Python 程序员可以使用其他可视化库,但 matplotlib 仍然被广泛使用,并且与生态系统的其他部分相当好地集成。我认为它是默认可视化工具的一个安全选择。

IPython 和 Jupyter

IPython 项目始于 2001 年,是 Fernando Pérez 的一个副业项目,旨在打造更好的交互式 Python 解释器。在随后的 20 年里,它已成为现代 Python 数据堆栈中最重要的工具之一。虽然它本身不提供任何计算或数据分析工具,但 IPython 旨在用于交互式计算和软件开发工作。它鼓励执行-探索工作流程,而不是许多其他编程语言的典型编辑-编译-运行工作流程。它还提供了对操作系统的 shell 和文件系统的集成访问;这在许多情况下减少了在终端窗口和 Python 会话之间切换的需求。由于许多数据分析编码涉及探索、试错和迭代,IPython 可以帮助您更快地完成工作。

2014 年,Fernando 和 IPython 团队宣布了Jupyter 项目,这是一个更广泛的倡议,旨在设计与语言无关的交互式计算工具。IPython 网络笔记本变成了 Jupyter 笔记本,现在支持超过 40 种编程语言。IPython 系统现在可以作为使用 Python 与 Jupyter 的内核(编程语言模式)。

IPython 本身已成为更广泛的 Jupyter 开源项目的组成部分,为交互式和探索性计算提供了一个高效的环境。它最古老和最简单的“模式”是作为一个增强的 Python shell,旨在加速 Python 代码的编写、测试和调试。您还可以通过 Jupyter 笔记本使用 IPython 系统。

Jupyter 笔记本系统还允许您在 Markdown 和 HTML 中编写内容,为您提供了一种创建包含代码和文本的丰富文档的方式。

我个人经常在我的 Python 工作中使用 IPython 和 Jupyter,无论是运行、调试还是测试代码。

GitHub 上的附带书籍材料中,您将找到包含每章代码示例的 Jupyter 笔记本。如果您无法访问 GitHub,您可以尝试Gitee 上的镜像

SciPy

SciPy是一个解决科学计算中一些基础问题的包集合。以下是它在各个模块中包含的一些工具:

scipy.integrate

数值积分例程和微分方程求解器

scipy.linalg

线性代数例程和矩阵分解,扩展到numpy.linalg提供的范围之外

scipy.optimize

函数优化器(最小化器)和根查找算法

scipy.signal

信号处理工具

scipy.sparse

稀疏矩阵和稀疏线性系统求解器

scipy.special

SPECFUN 的包装器,一个实现许多常见数学函数(如gamma函数)的 FORTRAN 库

scipy.stats

标准连续和离散概率分布(密度函数、采样器、连续分布函数)、各种统计检验和更多描述性统计

NumPy 和 SciPy 共同构成了许多传统科学计算应用的相当完整和成熟的计算基础。

scikit-learn

自 2007 年项目开始以来,scikit-learn已成为 Python 程序员的首选通用机器学习工具包。截至撰写本文时,超过两千名不同的个人为该项目贡献了代码。它包括用于以下模型的子模块:

  • 分类:SVM、最近邻、随机森林、逻辑回归等

  • 回归:Lasso、岭回归等

  • 聚类:k-means、谱聚类等

  • 降维:PCA、特征选择、矩阵分解等

  • 模型选择:网格搜索、交叉验证、度量

  • 预处理:特征提取、归一化

除了 pandas、statsmodels 和 IPython 之外,scikit-learn 对于使 Python 成为一种高效的数据科学编程语言至关重要。虽然我无法在本书中包含对 scikit-learn 的全面指南,但我将简要介绍一些其模型以及如何将其与本书中提供的其他工具一起使用。

statsmodels

statsmodels是一个统计分析包,由斯坦福大学统计学教授 Jonathan Taylor 的工作启发而来,他实现了 R 编程语言中流行的一些回归分析模型。Skipper Seabold 和 Josef Perktold 于 2010 年正式创建了新的 statsmodels 项目,自那时以来,该项目已经发展成为一群积极参与的用户和贡献者。Nathaniel Smith 开发了 Patsy 项目,该项目提供了一个受 R 公式系统启发的用于 statsmodels 的公式或模型规范框架。

与 scikit-learn 相比,statsmodels 包含用于经典(主要是频率主义)统计和计量经济学的算法。这包括诸如:

  • 回归模型:线性回归、广义线性模型、鲁棒线性模型、线性混合效应模型等

  • 方差分析(ANOVA)

  • 时间序列分析:AR、ARMA、ARIMA、VAR 和其他模型

  • 非参数方法:核密度估计、核回归

  • 统计模型结果的可视化

statsmodels 更专注于统计推断,为参数提供不确定性估计和p-值。相比之下,scikit-learn 更注重预测。

与 scikit-learn 一样,我将简要介绍 statsmodels 以及如何与 NumPy 和 pandas 一起使用它。

其他包

在 2022 年,有许多其他 Python 库可能会在关于数据科学的书中讨论。这包括一些较新的项目,如 TensorFlow 或 PyTorch,这些项目已经成为机器学习或人工智能工作中流行的工具。现在有其他更专注于这些项目的书籍,我建议使用本书来建立通用 Python 数据处理的基础。然后,您应该准备好转向更高级的资源,这些资源可能假定一定水平的专业知识。

1.4 安装和设置

由于每个人都在不同的应用中使用 Python,因此设置 Python 并获取必要的附加包没有单一的解决方案。许多读者可能没有完整的 Python 开发环境,适合跟随本书,因此我将在每个操作系统上提供详细的设置说明。我将使用 Miniconda,这是 conda 软件包管理器的最小安装,以及conda-forge,这是一个基于 conda 的社区维护的软件分发。本书始终使用 Python 3.10,但如果您是在未来阅读,欢迎安装更新版本的 Python。

如果由于某种原因,这些说明在您阅读时已过时,您可以查看我的书籍网站,我将努力保持最新安装说明的更新。

Windows 上的 Miniconda

要在 Windows 上开始,请从https://conda.io下载最新 Python 版本(目前为 3.9)的 Miniconda 安装程序。我建议按照 conda 网站上提供的 Windows 安装说明进行安装,这些说明可能在本书出版时和您阅读时之间发生了变化。大多数人会想要 64 位版本,但如果这在您的 Windows 机器上无法运行,您可以安装 32 位版本。

当提示是否仅为自己安装还是为系统上的所有用户安装时,请选择最适合您的选项。仅为自己安装将足以跟随本书。它还会询问您是否要将 Miniconda 添加到系统 PATH 环境变量中。如果选择此选项(我通常会这样做),则此 Miniconda 安装可能会覆盖您已安装的其他 Python 版本。如果不这样做,那么您将需要使用安装的 Window 开始菜单快捷方式才能使用此 Miniconda。此开始菜单条目可能称为“Anaconda3 (64 位)”。

我假设您还没有将 Miniconda 添加到系统路径中。要验证配置是否正确,请在“开始”菜单下的“Anaconda3 (64 位)”中打开“Anaconda Prompt (Miniconda3)”条目。然后尝试通过输入python来启动 Python 解释器。您应该会看到类似以下的消息:

(base) C:\Users\Wes>python
Python 3.9 [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

要退出 Python shell,请输入exit()并按 Enter 键。

GNU/Linux

Linux 的详细信息会根据您的 Linux 发行版类型有所不同,但在这里我提供了 Debian、Ubuntu、CentOS 和 Fedora 等发行版的详细信息。设置与 macOS 类似,唯一的区别是 Miniconda 的安装方式。大多数读者会想要下载默认的 64 位安装程序文件,这是针对 x86 架构的(但未来可能会有更多用户使用基于 aarch64 的 Linux 机器)。安装程序是一个必须在终端中执行的 shell 脚本。然后您将会得到一个类似Miniconda3-latest-Linux-x86_64.sh的文件。要安装它,请使用bash执行此脚本:

$ bash Miniconda3-latest-Linux-x86_64.sh

注意

一些 Linux 发行版在其软件包管理器中具有所有所需的 Python 软件包(在某些情况下是过时版本),可以使用类似 apt 的工具进行安装。这里描述的设置使用 Miniconda,因为它在各种发行版中都很容易重现,并且更简单地升级软件包到最新版本。

您可以选择将 Miniconda 文件放在哪里。我建议将文件安装在您的主目录中的默认位置;例如,/home/$USER/miniconda(自然包括您的用户名)。

安装程序会询问您是否希望修改您的 shell 脚本以自动激活 Miniconda。我建议这样做(选择“是”)以方便起见。

安装完成后,启动一个新的终端进程并验证您是否已经安装了新的 Miniconda:

(base) $ python
Python 3.9 | (main) [GCC 10.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

要退出 Python shell,请输入exit()并按 Enter 键或按 Ctrl-D。*### macOS 上的 Miniconda

下载 macOS Miniconda 安装程序,应该命名为Miniconda3-latest-MacOSX-arm64.sh,适用于 2020 年以后发布的基于 Apple Silicon 的 macOS 计算机,或者Miniconda3-latest-MacOSX-x86_64.sh,适用于 2020 年之前发布的基于 Intel 的 Mac。在 macOS 中打开终端应用程序,并通过使用bash执行安装程序(很可能在您的Downloads目录中)来安装:

$ bash $HOME/Downloads/Miniconda3-latest-MacOSX-arm64.sh

当安装程序运行时,默认情况下会自动在默认 shell 环境和默认 shell 配置文件中配置 Miniconda。这可能位于*/Users/$USER/.zshrc*。我建议让它这样做;如果您不想让安装程序修改默认的 shell 环境,您需要查阅 Miniconda 文档以便继续。

要验证一切是否正常工作,请尝试在系统 shell 中启动 Python(打开终端应用程序以获取命令提示符):

$ python
Python 3.9 (main) [Clang 12.0.1 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

要退出 shell,请按 Ctrl-D 或输入exit()并按 Enter 键。

安装必要的软件包

现在我们已经在您的系统上设置了 Miniconda,是时候安装本书中将要使用的主要软件包了。第一步是通过在 shell 中运行以下命令将 conda-forge 配置为您的默认软件包渠道:

(base) $ conda config --add channels conda-forge
(base) $ conda config --set channel_priority strict

现在,我们将使用 Python 3.10 使用conda create命令创建一个新的 conda“环境”:

(base) $ conda create -y -n pydata-book python=3.10

安装完成后,请使用conda activate激活环境:

(base) $ conda activate pydata-book
(pydata-book) $

注意

每次打开新终端时,都需要使用conda activate来激活您的环境。您可以随时通过在终端中运行conda info来查看有关活动 conda 环境的信息。

现在,我们将使用conda install安装整本书中使用的基本软件包(以及它们的依赖项):

(pydata-book) $ conda install -y pandas jupyter matplotlib

我们还将使用其他软件包,但这些软件包可以在需要时稍后安装。有两种安装软件包的方法:使用conda installpip install。在使用 Miniconda 时,应始终优先使用conda install,但某些软件包无法通过 conda 获得,因此如果conda install $package_name失败,请尝试pip install $package_name

注意

如果您想安装本书其余部分使用的所有软件包,现在可以通过运行:

conda install lxml beautifulsoup4 html5lib openpyxl \
               requests sqlalchemy seaborn scipy statsmodels \
               patsy scikit-learn pyarrow pytables numba

在 Windows 上,将^替换为 Linux 和 macOS 上使用的行继续符\

您可以使用conda update命令更新软件包:

conda update package_name

pip 还支持使用--upgrade标志进行升级:

pip install --upgrade package_name

您将有机会在整本书中尝试这些命令。

注意

虽然您可以使用 conda 和 pip 来安装软件包,但应避免使用 pip 更新最初使用 conda 安装的软件包(反之亦然),因为这样做可能会导致环境问题。我建议尽可能使用 conda,并仅在无法使用conda install安装软件包时才回退到 pip。

集成开发环境和文本编辑器

当被问及我的标准开发环境时,我几乎总是说“IPython 加上文本编辑器”。我通常会在 IPython 或 Jupyter 笔记本中编写程序,并逐步测试和调试每个部分。交互式地玩弄数据并直观验证特定数据操作是否正确也是很有用的。像 pandas 和 NumPy 这样的库旨在在 shell 中使用时提高生产力。

然而,在构建软件时,一些用户可能更喜欢使用功能更丰富的集成开发环境(IDE),而不是像 Emacs 或 Vim 这样的编辑器,后者在开箱即用时提供了更简洁的环境。以下是一些您可以探索的内容:

  • PyDev(免费),基于 Eclipse 平台构建的 IDE

  • 来自 JetBrains 的 PyCharm(面向商业用户的订阅制,对于开源开发者免费)

  • Visual Studio 的 Python 工具(适用于 Windows 用户)

  • Spyder(免费),目前与 Anaconda 捆绑的 IDE

  • Komodo IDE(商业版)

由于 Python 的流行,大多数文本编辑器,如 VS Code 和 Sublime Text 2,都具有出色的 Python 支持。

1.5 社区和会议

除了通过互联网搜索外,各种科学和数据相关的 Python 邮件列表通常对问题有帮助并且响应迅速。一些可以参考的包括:

  • pydata:一个 Google Group 列表,用于与 Python 数据分析和 pandas 相关的问题

  • pystatsmodels:用于 statsmodels 或与 pandas 相关的问题

  • scikit-learn 邮件列表(scikit-learn@python.org)和 Python 中的机器学习,一般

  • numpy-discussion:用于与 NumPy 相关的问题

  • scipy-user:用于一般 SciPy 或科学 Python 问题

我故意没有发布这些 URL,以防它们发生变化。它们可以通过互联网搜索轻松找到。

每年举办许多全球各地的 Python 程序员会议。如果您想与其他分享您兴趣的 Python 程序员联系,我鼓励您尽可能参加其中一个。许多会议为那些无法支付入场费或旅行费的人提供财政支持。以下是一些可以考虑的会议:

  • PyCon 和 EuroPython:分别是在北美和欧洲举办的两个主要的一般 Python 会议

  • SciPy 和 EuroSciPy:分别是在北美和欧洲举办的面向科学计算的会议

  • PyData:面向数据科学和数据分析用例的全球系列区域会议

  • 国际和地区 PyCon 会议(请参阅 pycon.org 获取完整列表)

1.6 浏览本书

如果您以前从未在 Python 中编程过,您可能需要花一些时间阅读 第二章:Python 语言基础、IPython 和 Jupyter Notebooks 和 第三章:内置数据结构、函数和文件,我在这里放置了有关 Python 语言特性、IPython shell 和 Jupyter notebooks 的简明教程。这些内容是本书其余部分的先决知识。如果您已经有 Python 经验,您可以选择略读或跳过这些章节。

接下来,我简要介绍了 NumPy 的关键特性,将更高级的 NumPy 使用留给 附录 A:高级 NumPy。然后,我介绍了 pandas,并将本书的其余部分专注于应用 pandas、NumPy 和 matplotlib 进行数据分析主题(用于可视化)。我以递增的方式组织了材料,尽管在章节之间偶尔会有一些轻微的交叉,有些概念可能尚未介绍。

尽管读者可能对他们的工作有许多不同的最终目标,但通常所需的任务大致可以分为许多不同的广泛组别:

与外部世界互动

使用各种文件格式和数据存储进行读写

准备

清理、整理、合并、规范化、重塑、切片和切块以及转换数据以进行分析

转换

对数据集组应用数学和统计操作以派生新数据集(例如,通过组变量对大表进行聚合)

建模和计算

将您的数据连接到统计模型、机器学习算法或其他计算工具

演示

创建交互式或静态图形可视化或文本摘要

代码示例

本书中的大多数代码示例都显示了输入和输出,就像在 IPython shell 或 Jupyter notebooks 中执行时一样:

In [5]: CODE EXAMPLE
Out[5]: OUTPUT

当您看到像这样的代码示例时,意图是让您在编码环境中的 In 区块中键入示例代码,并通过按 Enter 键(或在 Jupyter 中按 Shift-Enter)执行它。您应该看到类似于 Out 区块中显示的输出。

我已更改了 NumPy 和 pandas 的默认控制台输出设置,以提高本书的可读性和简洁性。例如,您可能会看到在数字数据中打印更多位数的精度。要完全匹配书中显示的输出,您可以在运行代码示例之前执行以下 Python 代码:

import numpy as np
import pandas as pd
pd.options.display.max_columns = 20
pd.options.display.max_rows = 20
pd.options.display.max_colwidth = 80
np.set_printoptions(precision=4, suppress=True)

示例数据

每一章的示例数据集都托管在 GitHub 仓库 中(如果无法访问 GitHub,则可以在 Gitee 上的镜像)。您可以通过使用 Git 版本控制系统在命令行上下载这些数据,或者通过从网站下载仓库的 zip 文件来获取数据。如果遇到问题,请转到 书籍网站 获取有关获取书籍材料的最新说明。

如果您下载包含示例数据集的 zip 文件,则必须完全提取 zip 文件的内容到一个目录,并在终端中导航到该目录,然后才能继续运行本书的代码示例:

$ pwd
/home/wesm/book-materials

$ ls
appa.ipynb  ch05.ipynb  ch09.ipynb  ch13.ipynb  README.md
ch02.ipynb  ch06.ipynb  ch10.ipynb  COPYING     requirements.txt
ch03.ipynb  ch07.ipynb  ch11.ipynb  datasets
ch04.ipynb  ch08.ipynb  ch12.ipynb  examples

我已尽一切努力确保 GitHub 仓库包含重现示例所需的一切,但可能会出现一些错误或遗漏。如果有的话,请发送邮件至:book@wesmckinney.com。报告书中错误的最佳方式是在 O’Reilly 网站上的勘误页面上。

导入约定

Python 社区已经采用了许多常用模块的命名约定:

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import statsmodels as sm

这意味着当你看到np.arange时,这是对 NumPy 中arange函数的引用。这样做是因为在 Python 软件开发中,从像 NumPy 这样的大型包中导入所有内容(from numpy import *)被认为是不良实践。

二、Python 语言基础,IPython 和 Jupyter 笔记本

原文:wesmckinney.com/book/python-basics

译者:飞龙

协议:CC BY-NC-SA 4.0

此开放访问网络版本的《Python 数据分析第三版》现已作为印刷版和数字版的伴侣提供。如果您发现任何勘误,请在此处报告。请注意,由 Quarto 生成的本站点的某些方面与 O’Reilly 的印刷版和电子书版本的格式不同。

如果您发现本书的在线版本有用,请考虑订购纸质版无 DRM 的电子书以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。

当我在 2011 年和 2012 年编写本书的第一版时,关于在 Python 中进行数据分析的学习资源较少。这在一定程度上是一个鸡生蛋的问题;许多我们现在认为理所当然的库,如 pandas、scikit-learn 和 statsmodels,在那时相对不成熟。现在是 2022 年,现在有越来越多关于数据科学、数据分析和机器学习的文献,补充了关于面向计算科学家、物理学家和其他研究领域专业人士的通用科学计算的先前作品。还有关于学习 Python 编程语言本身并成为有效软件工程师的优秀书籍。

由于本书旨在作为在 Python 中处理数据的入门文本,我认为从数据操作的角度对 Python 的内置数据结构和库的一些最重要特性进行自包含概述是有价值的。因此,我将在本章和第三章:内置数据结构、函数和文件中提供大致足够的信息,以便您能够跟随本书的其余部分。

本书的很大一部分关注于基于表格的分析和数据准备工具,用于处理足够小以适合个人计算机的数据集。要使用这些工具,有时您必须对混乱的数据进行一些整理,将其整理成更整洁的表格(或结构化)形式。幸运的是,Python 是做这些事情的理想语言。您对 Python 语言及其内置数据类型的熟练程度越高,准备新数据集进行分析就会变得更容易。

本书中的一些工具最好在实时的 IPython 或 Jupyter 会话中进行探索。一旦您学会如何启动 IPython 和 Jupyter,我建议您跟着示例进行实验和尝试不同的东西。与任何基于键盘的类似控制台的环境一样,熟悉常用命令也是学习曲线的一部分。

注意:本章未涵盖一些入门级 Python 概念,如类和面向对象编程,这些概念在您进入 Python 数据分析领域时可能会有用。

为了加深您对 Python 语言的了解,我建议您将本章与官方 Python 教程以及可能是许多优秀的通用 Python 编程书籍结合起来阅读。一些建议让您开始包括:

  • 《Python Cookbook》,第三版,作者 David Beazley 和 Brian K. Jones(O’Reilly)

  • 《流畅的 Python》,作者 Luciano Ramalho(O’Reilly)

  • 《Effective Python》,第二版,作者 Brett Slatkin(Addison-Wesley)## 2.1 Python 解释器

Python 是一种解释性语言。Python 解释器通过逐条执行程序来运行程序。标准的交互式 Python 解释器可以通过命令行使用python命令调用:

$ python
Python 3.10.4 | packaged by conda-forge | (main, Mar 24 2022, 17:38:57)
[GCC 10.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 5
>>> print(a)
5

您看到的>>>是您将输入代码表达式的提示。要退出 Python 解释器,您可以输入exit()或按 Ctrl-D(仅适用于 Linux 和 macOS)。

运行 Python 程序就像调用python并将*.py文件作为第一个参数一样简单。假设我们已经创建了包含以下内容的hello_world.py*文件:

print("Hello world")

您可以通过执行以下命令来运行它(hello_world.py文件必须在您当前的工作终端目录中):

$ python hello_world.py
Hello world

虽然一些 Python 程序员以这种方式执行他们的所有 Python 代码,但进行数据分析或科学计算的人使用 IPython,这是一个增强的 Python 解释器,或者使用 Jupyter 笔记本,这是最初在 IPython 项目中创建的基于 Web 的代码笔记本。我在本章中介绍了如何使用 IPython 和 Jupyter,并在附录 A:高级 NumPy 中深入探讨了 IPython 功能。当您使用%run命令时,IPython 会在同一进程中执行指定文件中的代码,使您能够在完成时交互地探索结果:

$ ipython
Python 3.10.4 | packaged by conda-forge | (main, Mar 24 2022, 17:38:57)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.31.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: %run hello_world.py
Hello world

In [2]:

默认的 IPython 提示采用带编号的In [2]:样式,与标准的>>>提示相比。

2.2 IPython 基础知识

在本节中,我将带您快速了解 IPython shell 和 Jupyter 笔记本,并介绍一些基本概念。

运行 IPython Shell

您可以像启动常规 Python 解释器一样在命令行上启动 IPython shell,只是使用ipython命令:

$ ipython
Python 3.10.4 | packaged by conda-forge | (main, Mar 24 2022, 17:38:57)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.31.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: a = 5

In [2]: a
Out[2]: 5

您可以通过键入 Python 语句并按回车键(或 Enter 键)来执行任意 Python 语句。当您只输入一个变量时,IPython 会呈现对象的字符串表示:

In [5]: import numpy as np

In [6]: data = [np.random.standard_normal() for i in range(7)]

In [7]: data
Out[7]: 
[-0.20470765948471295,
 0.47894333805754824,
 -0.5194387150567381,
 -0.55573030434749,
 1.9657805725027142,
 1.3934058329729904,
 0.09290787674371767]

前两行是 Python 代码语句;第二个语句创建了一个名为data的变量,指向一个新创建的列表。最后一行在控制台中打印了data的值。

许多种类的 Python 对象都被格式化为更易读或漂亮打印,这与使用print进行正常打印不同。如果您在标准 Python 解释器中打印上述data变量,它将不太易读:

>>> import numpy as np
>>> data = [np.random.standard_normal() for i in range(7)]
>>> print(data)
>>> data
[-0.5767699931966723, -0.1010317773535111, -1.7841005313329152,
-1.524392126408841, 0.22191374220117385, -1.9835710588082562,
-1.6081963964963528]

IPython 还提供了执行任意代码块(通过一种略微夸张的复制粘贴方法)和整个 Python 脚本的功能。您还可以使用 Jupyter 笔记本来处理更大的代码块,我们很快就会看到。

在终端中运行 Jupyter Notebook

Jupyter 项目的一个主要组件是笔记本,一种用于代码、文本(包括 Markdown)、数据可视化和其他输出的交互式文档。Jupyter 笔记本与内核交互,这些内核是针对不同编程语言的 Jupyter 交互式计算协议的实现。Python Jupyter 内核使用 IPython 系统作为其基础行为。

要启动 Jupyter,请在终端中运行命令jupyter notebook

$ jupyter notebook
[I 15:20:52.739 NotebookApp] Serving notebooks from local directory:
/home/wesm/code/pydata-book
[I 15:20:52.739 NotebookApp] 0 active kernels
[I 15:20:52.739 NotebookApp] The Jupyter Notebook is running at:
http://localhost:8888/?token=0a77b52fefe52ab83e3c35dff8de121e4bb443a63f2d...
[I 15:20:52.740 NotebookApp] Use Control-C to stop this server and shut down
all kernels (twice to skip confirmation).
Created new window in existing browser session.
    To access the notebook, open this file in a browser:
        file:///home/wesm/.local/share/jupyter/runtime/nbserver-185259-open.html
    Or copy and paste one of these URLs:
        http://localhost:8888/?token=0a77b52fefe52ab83e3c35dff8de121e4...
     or http://127.0.0.1:8888/?token=0a77b52fefe52ab83e3c35dff8de121e4...

在许多平台上,Jupyter 会自动在默认的 Web 浏览器中打开(除非您使用--no-browser启动)。否则,您可以导航到启动笔记本时打印的 HTTP 地址,例如http://localhost:8888/?token=0a77b52fefe52ab83e3c35dff8de121e4bb443a63f2d3055。在 Google Chrome 中查看图 2.1。

注意

许多人将 Jupyter 用作本地计算环境,但它也可以部署在服务器上并远程访问。我不会在这里涵盖这些细节,但如果这与您的需求相关,我鼓励您在互联网上探索这个主题。

图 2.1:Jupyter 笔记本首页

要创建一个新笔记本,点击“New”按钮并选择“Python 3”选项。您应该看到类似于图 2.2 的内容。如果这是您第一次尝试,请尝试点击空的代码“单元格”并输入一行 Python 代码。然后按 Shift-Enter 执行它。

图 2.2:Jupyter 新笔记本视图

当您保存笔记本(请参见笔记本文件菜单下的“保存和检查点”)时,它将创建一个扩展名为*.ipynb*的文件。这是一种自包含的文件格式,包含当前笔记本中的所有内容(包括任何已评估的代码输出)。其他 Jupyter 用户可以加载和编辑这些文件。

要重命名打开的笔记本,请单击页面顶部的笔记本标题,然后键入新标题,完成后按 Enter。

要加载现有笔记本,请将文件放在启动笔记本进程的同一目录中(或其中的子文件夹),然后从登录页面点击名称。您可以尝试使用 GitHub 上我的wesm/pydata-book存储库中的笔记本。请参见图 2.3。

当您想要关闭笔记本时,请单击文件菜单,然后选择“关闭并停止”。如果您只是关闭浏览器选项卡,则与笔记本相关联的 Python 进程将继续在后台运行。

虽然 Jupyter 笔记本可能感觉与 IPython shell 有所不同,但本章中的几乎所有命令和工具都可以在任何环境中使用。

图 2.3:现有笔记本的 Jupyter 示例视图

Tab Completion

从表面上看,IPython shell 看起来像标准终端 Python 解释器的外观不同版本(使用python调用)。与标准 Python shell 相比,IPython shell 的一个主要改进是制表完成,在许多 IDE 或其他交互式计算分析环境中都可以找到。在 shell 中输入表达式时,按 Tab 键将搜索命名空间以查找与您迄今为止键入的字符匹配的任何变量(对象、函数等),并在方便的下拉菜单中显示结果:

In [1]: an_apple = 27

In [2]: an_example = 42

In [3]: an<Tab>
an_apple   an_example  any

在此示例中,请注意 IPython 显示了我定义的两个变量以及内置函数any。此外,在键入句点后,您还可以完成任何对象的方法和属性:

In [3]: b = [1, 2, 3]

In [4]: b.<Tab>
append()  count()   insert()  reverse()
clear()   extend()  pop()     sort()
copy()    index()   remove()

模块也是如此:

In [1]: import datetime

In [2]: datetime.<Tab>
date          MAXYEAR       timedelta
datetime      MINYEAR       timezone
datetime_CAPI time          tzinfo

注意

请注意,默认情况下,IPython 隐藏以下划线开头的方法和属性,例如魔术方法和内部“私有”方法和属性,以避免显示混乱(并使初学者感到困惑!)。这些也可以通过制表完成,但您必须首先键入下划线才能看到它们。如果您希望始终在制表完成中看到此类方法,请更改 IPython 配置中的此设置。请参阅IPython 文档以了解如何执行此操作。

制表完成在许多上下文中起作用,不仅限于搜索交互式命名空间并完成对象或模块属性。在键入任何看起来像文件路径的内容(即使在 Python 字符串中),按 Tab 键将完成与您键入的内容匹配的计算机文件系统上的任何内容。

结合%run命令(请参见附录 B.2.1:%run命令),此功能可以为您节省许多按键。

制表完成还可以节省函数关键字参数(包括=符号!)的完成时间。请参见图 2.4。

图 2.4:在 Jupyter 笔记本中自动完成函数关键字

我们稍后将更仔细地查看函数。

内省

在变量前或后使用问号(?)将显示有关对象的一些常规信息:

In [1]: b = [1, 2, 3]

In [2]: b?
Type:        list
String form: [1, 2, 3]
Length:      3
Docstring:
Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list.
The argument must be an iterable if specified.

In [3]: print?
Docstring:
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

Prints the values to a stream, or to sys.stdout by default.
Optional keyword arguments:
file:  a file-like object (stream); defaults to the current sys.stdout.
sep:   string inserted between values, default a space.
end:   string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.
Type:      builtin_function_or_method

这被称为对象内省。如果对象是函数或实例方法,则如果定义了文档字符串,它也将显示出来。假设我们编写了以下函数(您可以在 IPython 或 Jupyter 中重现):

def add_numbers(a, b):
 """
 Add two numbers together

 Returns
 -------
 the_sum : type of arguments
 """
 return a + b

然后使用?显示文档字符串:

In [6]: add_numbers?
Signature: add_numbers(a, b)
Docstring:
Add two numbers together
Returns
-------
the_sum : type of arguments
File:      <ipython-input-9-6a548a216e27>
Type:      function

?还有一个最终的用途,就是在 IPython 命名空间中进行搜索,类似于标准的 Unix 或 Windows 命令行。与通配符(*)结合的一系列字符将显示所有与通配符表达式匹配的名称。例如,我们可以获取包含load的顶级 NumPy 命名空间中的所有函数列表:

In [9]: import numpy as np

In [10]: np.*load*?
np.__loader__
np.load
np.loads
np.loadtxt

2.3 Python 语言基础

在本节中,我将为您概述基本的 Python 编程概念和语言机制。在下一章中,我将更详细地介绍 Python 数据结构、函数和其他内置工具。

语言语义

Python 语言设计的一个重要特点是其对可读性、简单性和明确性的强调。有些人甚至将其比作“可执行的伪代码”。

缩进,而不是大括号

Python 使用空格(制表符或空格)来结构化代码,而不是像 R、C++、Java 和 Perl 等许多其他语言那样使用大括号。考虑一个排序算法中的for循环:

for x in array:
 if x < pivot:
 less.append(x)
 else:
 greater.append(x)

冒号表示缩进代码块的开始,之后所有代码都必须缩进相同的量,直到块的结束。

无论你喜欢还是讨厌,对于 Python 程序员来说,有意义的空白是一个事实。虽然一开始可能会感到陌生,但希望你能逐渐习惯它。

注意

我强烈建议使用四个空格作为默认缩进,并用四个空格替换制表符。许多文本编辑器都有一个设置,可以自动将制表符替换为空格(请这样做!)。IPython 和 Jupyter 笔记本会在冒号后的新行自动插入四个空格,并用四个空格替换制表符。

正如你现在所看到的,Python 语句也不需要以分号结尾。但是,分号可以用来在单行上分隔多个语句:

a = 5; b = 6; c = 7

在一行上放置多个语句通常在 Python 中是不鼓励的,因为这可能会使代码变得不太可读。

一切都是对象

Python 语言的一个重要特点是其对象模型的一致性。每个数字、字符串、数据结构、函数、类、模块等都存在于 Python 解释器中的自己的“盒子”中,这被称为Python 对象。每个对象都有一个关联的类型(例如整数字符串函数)和内部数据。实际上,这使得语言非常灵活,因为即使函数也可以像任何其他对象一样对待。

注释

由井号(井号)#引导的任何文本都会被 Python 解释器忽略。这通常用于向代码添加注释。有时您可能还想排除某些代码块而不删除它们。一种解决方案是注释掉代码:

results = []
for line in file_handle:
 # keep the empty lines for now
 # if len(line) == 0:
 #   continue
 results.append(line.replace("foo", "bar"))

注释也可以出现在执行代码的行之后。虽然一些程序员更喜欢将注释放在特定代码行之前的行中,但有时这样做也是有用的:

print("Reached this line")  # Simple status report
函数和对象方法调用

使用括号调用函数并传递零个或多个参数,可选地将返回的值赋给一个变量:

result = f(x, y, z)
g()

Python 中几乎每个对象都有附加的函数,称为方法,这些函数可以访问对象的内部内容。您可以使用以下语法调用它们:

obj.some_method(x, y, z)

函数可以接受位置关键字参数:

result = f(a, b, c, d=5, e="foo")

我们稍后会更详细地看一下这个。

变量和参数传递

在 Python 中赋值变量(或名称)时,您正在创建对等号右侧显示的对象的引用。在实际操作中,考虑一个整数列表:

In [8]: a = [1, 2, 3]

假设我们将a赋给一个新变量b

In [9]: b = a

In [10]: b
Out[10]: [1, 2, 3]

在一些语言中,对b的赋值将导致数据[1, 2, 3]被复制。在 Python 中,ab实际上现在指向同一个对象,即原始列表[1, 2, 3](请参见图 2.5 的模拟)。您可以通过向a附加一个元素,然后检查b来证明这一点:

In [11]: a.append(4)

In [12]: b
Out[12]: [1, 2, 3, 4]

图 2.5:同一对象的两个引用

了解 Python 中引用的语义以及何时、如何以及为什么数据被复制,在处理 Python 中的大型数据集时尤为重要。

注意

赋值也被称为绑定,因为我们正在将一个名称绑定到一个对象。已经分配的变量名称有时可能被称为绑定变量。

当您将对象作为参数传递给函数时,将创建新的本地变量引用原始对象,而不进行任何复制。如果在函数内部将一个新对象绑定到一个变量,那么它不会覆盖函数外部(“父范围”)具有相同名称的变量。因此,可以更改可变参数的内部。假设我们有以下函数:

In [13]: def append_element(some_list, element):
 ....:     some_list.append(element)

然后我们有:

In [14]: data = [1, 2, 3]

In [15]: append_element(data, 4)

In [16]: data
Out[16]: [1, 2, 3, 4]
动态引用,强类型

Python 中的变量没有与之关联的固有类型;通过赋值,变量可以引用不同类型的对象。以下情况没有问题:

In [17]: a = 5

In [18]: type(a)
Out[18]: int

In [19]: a = "foo"

In [20]: type(a)
Out[20]: str

变量是特定命名空间内对象的名称;类型信息存储在对象本身中。一些观察者可能匆忙得出结论,认为 Python 不是一种“类型化语言”。这是不正确的;考虑这个例子:

In [21]: "5" + 5
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-21-7fe5aa79f268> in <module>
----> 1 "5" + 5
TypeError: can only concatenate str (not "int") to str

在某些语言中,字符串'5'可能会被隐式转换(或转换)为整数,从而得到 10。在其他语言中,整数5可能会被转换为字符串,从而得到连接的字符串'55'。在 Python 中,不允许这种隐式转换。在这方面,我们说 Python 是一种强类型语言,这意味着每个对象都有一个特定的类型(或),隐式转换只会在某些允许的情况下发生,例如:

In [22]: a = 4.5

In [23]: b = 2

# String formatting, to be visited later
In [24]: print(f"a is {type(a)}, b is {type(b)}")
a is <class 'float'>, b is <class 'int'>

In [25]: a / b
Out[25]: 2.25

在这里,即使b是一个整数,它也会被隐式转换为浮点数进行除法运算。

了解对象的类型很重要,能够编写能够处理许多不同类型输入的函数也很有用。您可以使用isinstance函数检查对象是否是特定类型的实例:

In [26]: a = 5

In [27]: isinstance(a, int)
Out[27]: True

如果要检查对象的类型是否在元组中存在,isinstance可以接受一个类型元组:

In [28]: a = 5; b = 4.5

In [29]: isinstance(a, (int, float))
Out[29]: True

In [30]: isinstance(b, (int, float))
Out[30]: True
属性和方法

Python 中的对象通常具有属性(存储在对象“内部”的其他 Python 对象)和方法(与对象关联的函数,可以访问对象的内部数据)。它们都可以通过语法<obj.attribute_name>访问:

In [1]: a = "foo"

In [2]: a.<Press Tab>
capitalize() index()        isspace()      removesuffix()  startswith()
casefold()   isprintable()  istitle()      replace()       strip()
center()     isalnum()      isupper()      rfind()         swapcase()
count()      isalpha()      join()         rindex()        title()
encode()     isascii()      ljust()        rjust()         translate()
endswith()   isdecimal()    lower()        rpartition()
expandtabs() isdigit()      lstrip()       rsplit()
find()       isidentifier() maketrans()    rstrip()
format()     islower()      partition()    split()
format_map() isnumeric()    removeprefix() splitlines()

属性和方法也可以通过getattr函数按名称访问:

In [32]: getattr(a, "split")
Out[32]: <function str.split(sep=None, maxsplit=-1)>

虽然我们在本书中不会广泛使用getattr函数和相关函数hasattrsetattr,但它们可以非常有效地用于编写通用的可重用代码。

鸭子类型

通常,您可能不关心对象的类型,而只关心它是否具有某些方法或行为。这有时被称为鸭子类型,源自谚语“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子。”例如,如果对象实现了迭代器协议,则可以验证该对象是否可迭代。对于许多对象,这意味着它具有一个__iter__“魔术方法”,尽管检查的另一种更好的方法是尝试使用iter函数:

In [33]: def isiterable(obj):
 ....:     try:
 ....:         iter(obj)
 ....:         return True
 ....:     except TypeError: # not iterable
 ....:         return False

对于字符串以及大多数 Python 集合类型,此函数将返回True

In [34]: isiterable("a string")
Out[34]: True

In [35]: isiterable([1, 2, 3])
Out[35]: True

In [36]: isiterable(5)
Out[36]: False
导入

在 Python 中,模块只是一个包含 Python 代码的扩展名为*.py*的文件。假设我们有以下模块:

# some_module.py
PI = 3.14159

def f(x):
 return x + 2

def g(a, b):
 return a + b

如果我们想要从同一目录中的另一个文件中访问some_module.py中定义的变量和函数,我们可以这样做:

import some_module
result = some_module.f(5)
pi = some_module.PI

或者:

from some_module import g, PI
result = g(5, PI)

通过使用as关键字,您可以为导入指定不同的变量名称:

import some_module as sm
from some_module import PI as pi, g as gf

r1 = sm.f(pi)
r2 = gf(6, pi)
二进制运算符和比较

大多数二进制数学运算和比较使用其他编程语言中常用的数学语法:

In [37]: 5 - 7
Out[37]: -2

In [38]: 12 + 21.5
Out[38]: 33.5

In [39]: 5 <= 2
Out[39]: False

查看表 2.1 以获取所有可用的二进制运算符。

表 2.1:二进制运算符

操作描述
a + bab相加
a - ba中减去b
a * ba乘以b
a / ba除以b
a // b通过b进行地板除法,去除任何小数余数
a ** ba提升到b次方
a & b如果ab都为True,则为True;对于整数,取位AND
`ab`
a ^ b对于布尔值,如果abTrue,但不是两者都为True;对于整数,取位异或
a == b如果a等于b,则为True
a != b如果a不等于b,则为True
a < b,a <= b如果a小于(小于或等于)b,则为True
a > b, a >= b如果a大于(大于或等于)b,则为True
a is b如果ab引用相同的 Python 对象,则为True
a is not b如果ab引用不同的 Python 对象,则为True

要检查两个变量是否引用同一对象,请使用is关键字。使用is not来检查两个对象是否不相同:

In [40]: a = [1, 2, 3]

In [41]: b = a

In [42]: c = list(a)

In [43]: a is b
Out[43]: True

In [44]: a is not c
Out[44]: True

由于list函数始终创建一个新的 Python 列表(即一个副本),我们可以确保ca不同。与==运算符不同,使用is不同,因为在这种情况下我们有:

In [45]: a == c
Out[45]: True

isis not的常见用法是检查变量是否为None,因为None只有一个实例:

In [46]: a = None

In [47]: a is None
Out[47]: True
可变和不可变对象

Python 中的许多对象,如列表、字典、NumPy 数组和大多数用户定义的类型(类),都是可变的。这意味着它们包含的对象或值可以被修改:

In [48]: a_list = ["foo", 2, [4, 5]]

In [49]: a_list[2] = (3, 4)

In [50]: a_list
Out[50]: ['foo', 2, (3, 4)]

其他,如字符串和元组,是不可变的,这意味着它们的内部数据不能被更改:

In [51]: a_tuple = (3, 5, (4, 5))

In [52]: a_tuple[1] = "four"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-52-cd2a018a7529> in <module>
----> 1 a_tuple[1] = "four"
TypeError: 'tuple' object does not support item assignment

请记住,仅因为您可以改变对象并不意味着您总是应该这样做。这些操作被称为副作用。例如,在编写函数时,任何副作用都应明确地在函数的文档或注释中向用户传达。如果可能的话,我建议尽量避免副作用并偏爱不可变性,即使可能涉及可变对象。

标量类型

Python 具有一小组内置类型,用于处理数字数据、字符串、布尔(TrueFalse)值以及日期和时间。这些“单值”类型有时被称为标量类型,我们在本书中将它们称为标量。请参阅表 2.2 以获取主要标量类型的列表。日期和时间处理将单独讨论,因为这些由标准库中的datetime模块提供。

表 2.2:标准 Python 标量类型

类型描述
NonePython 的“null”值(只存在一个None对象的实例)
str字符串类型;保存 Unicode 字符串
bytes原始二进制数据
float双精度浮点数(请注意没有单独的double类型)
bool布尔值TrueFalse
int任意精度整数
数字类型

数字的主要 Python 类型是intfloatint可以存储任意大的数字:

In [53]: ival = 17239871

In [54]: ival ** 6
Out[54]: 26254519291092456596965462913230729701102721

浮点数用 Python 的float类型表示。在底层,每个都是双精度值。它们也可以用科学计数法表示:

In [55]: fval = 7.243

In [56]: fval2 = 6.78e-5

整数除法如果结果不是整数,将始终产生一个浮点数:

In [57]: 3 / 2
Out[57]: 1.5

要获得 C 风格的整数除法(如果结果不是整数,则丢弃小数部分),请使用地板除法运算符//

In [58]: 3 // 2
Out[58]: 1
字符串

许多人使用 Python 是因为其内置的字符串处理功能。您可以使用单引号'或双引号"(通常更喜欢双引号)编写字符串字面值

a = 'one way of writing a string'
b = "another way"

Python 字符串类型是str

对于带有换行符的多行字符串,可以使用三引号,即'''"""

c = """
This is a longer string that
spans multiple lines
"""

这个字符串c实际上包含四行文本可能会让您感到惊讶;在"""之后和lines之后的换行符包含在字符串中。我们可以使用c上的count方法来计算换行符的数量:

In [60]: c.count("\n")
Out[60]: 3

Python 字符串是不可变的;您不能修改一个字符串:

In [61]: a = "this is a string"

In [62]: a[10] = "f"
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-62-3b2d95f10db4> in <module>
----> 1 a[10] = "f"
TypeError: 'str' object does not support item assignment

要解释此错误消息,请从下往上阅读。我们尝试用字母"f"替换位置 10 处的字符(“项”),但对于字符串对象来说,这是不允许的。如果我们需要修改一个字符串,我们必须使用一个创建新字符串的函数或方法,比如字符串replace方法:

In [63]: b = a.replace("string", "longer string")

In [64]: b
Out[64]: 'this is a longer string'

此操作后,变量a保持不变:

In [65]: a
Out[65]: 'this is a string'

许多 Python 对象可以使用str函数转换为字符串:

In [66]: a = 5.6

In [67]: s = str(a)

In [68]: print(s)
5.6

字符串是 Unicode 字符序列,因此可以像其他序列(如列表和元组)一样对待:

In [69]: s = "python"

In [70]: list(s)
Out[70]: ['p', 'y', 't', 'h', 'o', 'n']

In [71]: s[:3]
Out[71]: 'pyt'

s[:3]语法称为切片,对于许多种类的 Python 序列都有实现。稍后将更详细地解释这一点,因为它在本书中被广泛使用。

反斜杠字符\是一个转义字符,意味着它用于指定特殊字符,如换行符\n或 Unicode 字符。要编写带有反斜杠的字符串字面值,您需要对其进行转义:

In [72]: s = "12\\34"

In [73]: print(s)
12\34

如果您有一个带有许多反斜杠且没有特殊字符的字符串,您可能会觉得有点烦人。幸运的是,您可以在字符串的前导引号前加上r,这意味着应该按原样解释字符:

In [74]: s = r"this\has\no\special\characters"

In [75]: s
Out[75]: 'this\\has\\no\\special\\characters'

r代表原始

将两个字符串相加会将它们连接在一起并生成一个新字符串:

In [76]: a = "this is the first half "

In [77]: b = "and this is the second half"

In [78]: a + b
Out[78]: 'this is the first half and this is the second half'

字符串模板或格式化是另一个重要主题。随着 Python 3 的出现,进行此操作的方式数量已经扩展,这里我将简要描述其中一个主要接口的机制。字符串对象具有一个format方法,可用于将格式化参数替换为字符串中,生成一个新字符串:

In [79]: template = "{0:.2f}  {1:s} are worth US${2:d}"

在这个字符串中:

  • {0:.2f}表示将第一个参数格式化为带有两位小数的浮点数。

  • {1:s}表示将第二个参数格式化为字符串。

  • {2:d}表示将第三个参数格式化为精确整数。

要为这些格式参数替换参数,我们将一系列参数传递给format方法:

In [80]: template.format(88.46, "Argentine Pesos", 1)
Out[80]: '88.46 Argentine Pesos are worth US$1'

Python 3.6 引入了一个名为f-strings(即格式化字符串字面值)的新功能,可以使创建格式化字符串更加方便。要创建 f-string,只需在字符串字面值之前立即写入字符f。在字符串中,用大括号括起 Python 表达式,以将表达式的值替换为格式化字符串中的值:

In [81]: amount = 10

In [82]: rate = 88.46

In [83]: currency = "Pesos"

In [84]: result = f"{amount}  {currency} is worth US${amount / rate}"

格式说明符可以在每个表达式后添加,使用与上面字符串模板相同的语法:

In [85]: f"{amount}  {currency} is worth US${amount / rate:.2f}"
Out[85]: '10 Pesos is worth US$0.11'

字符串格式化是一个深入的主题;有多种方法和大量选项和调整可用于控制结果字符串中的值的格式。要了解更多,请参阅官方 Python 文档

字节和 Unicode

在现代 Python(即 Python 3.0 及更高版本)中,Unicode 已成为一流的字符串类型,以实现更一致地处理 ASCII 和非 ASCII 文本。在旧版本的 Python 中,字符串都是字节,没有任何明确的 Unicode 编码。您可以假设您知道字符编码来转换为 Unicode。这里是一个带有非 ASCII 字符的示例 Unicode 字符串:

In [86]: val = "español"

In [87]: val
Out[87]: 'español'

我们可以使用encode方法将此 Unicode 字符串转换为其 UTF-8 字节表示:

In [88]: val_utf8 = val.encode("utf-8")

In [89]: val_utf8
Out[89]: b'espa\xc3\xb1ol'

In [90]: type(val_utf8)
Out[90]: bytes

假设您知道bytes对象的 Unicode 编码,您可以使用decode方法返回:

In [91]: val_utf8.decode("utf-8")
Out[91]: 'español'

现在最好使用 UTF-8 进行任何编码,但出于历史原因,您可能会遇到各种不同编码的数据:

In [92]: val.encode("latin1")
Out[92]: b'espa\xf1ol'

In [93]: val.encode("utf-16")
Out[93]: b'\xff\xfee\x00s\x00p\x00a\x00\xf1\x00o\x00l\x00'

In [94]: val.encode("utf-16le")
Out[94]: b'e\x00s\x00p\x00a\x00\xf1\x00o\x00l\x00'

在处理文件时,最常见的是遇到bytes对象,其中不希望将所有数据隐式解码为 Unicode 字符串。

布尔值

Python 中的两个布尔值分别写为TrueFalse。比较和其他条件表达式的结果要么为True,要么为False。布尔值可以使用andor关键字组合:

In [95]: True and True
Out[95]: True

In [96]: False or True
Out[96]: True

当转换为数字时,False变为0True变为1

In [97]: int(False)
Out[97]: 0

In [98]: int(True)
Out[98]: 1

关键字not可以将布尔值从True翻转为False,反之亦然:

In [99]: a = True

In [100]: b = False

In [101]: not a
Out[101]: False

In [102]: not b
Out[102]: True
类型转换

strboolintfloat类型也是可以用来将值转换为这些类型的函数:

In [103]: s = "3.14159"

In [104]: fval = float(s)

In [105]: type(fval)
Out[105]: float

In [106]: int(fval)
Out[106]: 3

In [107]: bool(fval)
Out[107]: True

In [108]: bool(0)
Out[108]: False

请注意,大多数非零值在转换为bool时会变为True

None

None是 Python 的空值类型:

In [109]: a = None

In [110]: a is None
Out[110]: True

In [111]: b = 5

In [112]: b is not None
Out[112]: True

None也是函数参数的常见默认值:

def add_and_maybe_multiply(a, b, c=None):
 result = a + b

 if c is not None:
 result = result * c

 return result
日期和时间

内置的 Python datetime模块提供了datetimedatetime类型。datetime类型结合了datetime中存储的信息,是最常用的类型:

In [113]: from datetime import datetime, date, time

In [114]: dt = datetime(2011, 10, 29, 20, 30, 21)

In [115]: dt.day
Out[115]: 29

In [116]: dt.minute
Out[116]: 30

给定一个datetime实例,您可以通过在具有相同名称的datetime上调用方法来提取等效的datetime对象:

In [117]: dt.date()
Out[117]: datetime.date(2011, 10, 29)

In [118]: dt.time()
Out[118]: datetime.time(20, 30, 21)

strftime方法将datetime格式化为字符串:

In [119]: dt.strftime("%Y-%m-%d %H:%M")
Out[119]: '2011-10-29 20:30'

字符串可以使用strptime函数转换(解析)为datetime对象:

In [120]: datetime.strptime("20091031", "%Y%m%d")
Out[120]: datetime.datetime(2009, 10, 31, 0, 0)

查看表 11.2 以获取完整的格式规范列表。

当您聚合或以其他方式对时间序列数据进行分组时,偶尔会有必要替换一系列datetime的时间字段,例如,将minutesecond字段替换为零:

In [121]: dt_hour = dt.replace(minute=0, second=0)

In [122]: dt_hour
Out[122]: datetime.datetime(2011, 10, 29, 20, 0)

由于datetime.datetime是不可变类型,这些方法总是会产生新对象。因此,在前面的例子中,dt不会被replace修改:

In [123]: dt
Out[123]: datetime.datetime(2011, 10, 29, 20, 30, 21)

两个datetime对象的差产生一个datetime.timedelta类型:

In [124]: dt2 = datetime(2011, 11, 15, 22, 30)

In [125]: delta = dt2 - dt

In [126]: delta
Out[126]: datetime.timedelta(days=17, seconds=7179)

In [127]: type(delta)
Out[127]: datetime.timedelta

输出timedelta(17, 7179)表示timedelta编码了 17 天和 7179 秒的偏移量。

timedelta添加到datetime会产生一个新的偏移datetime

In [128]: dt
Out[128]: datetime.datetime(2011, 10, 29, 20, 30, 21)

In [129]: dt + delta
Out[129]: datetime.datetime(2011, 11, 15, 22, 30)

控制流

Python 有几个内置关键字用于条件逻辑、循环和其他标准控制流概念,这些概念在其他编程语言中也可以找到。

if、elif 和 else

if语句是最为人熟知的控制流语句类型之一。它检查一个条件,如果为True,则评估后面的代码块:

x = -5
if x < 0:
 print("It's negative")

if语句后面可以选择跟随一个或多个elif代码块和一个全捕获的else代码块,如果所有条件都为False

if x < 0:
 print("It's negative")
elif x == 0:
 print("Equal to zero")
elif 0 < x < 5:
 print("Positive but smaller than 5")
else:
 print("Positive and larger than or equal to 5")

如果任何条件为True,则不会继续执行任何elifelse代码块。使用andor的复合条件,条件从左到右进行评估并会短路:

In [130]: a = 5; b = 7

In [131]: c = 8; d = 4

In [132]: if a < b or c > d:
 .....:     print("Made it")
Made it

在这个例子中,比较c > d永远不会被评估,因为第一个比较是True

也可以链接比较:

In [133]: 4 > 3 > 2 > 1
Out[133]: True
for 循环

for循环用于遍历集合(如列表或元组)或迭代器。for循环的标准语法是:

for value in collection:
 # do something with value

您可以使用continue关键字将for循环推进到下一个迭代,跳过代码块的其余部分。考虑这段代码,它对列表中的整数求和并跳过None值:

sequence = [1, 2, None, 4, None, 5]
total = 0
for value in sequence:
 if value is None:
 continue
 total += value

可以使用break关键字完全退出for循环。这段代码将列表元素求和,直到达到 5 为止:

sequence = [1, 2, 0, 4, 6, 5, 2, 1]
total_until_5 = 0
for value in sequence:
 if value == 5:
 break
 total_until_5 += value

break关键字仅终止最内层的for循环;任何外部的for循环将继续运行:

In [134]: for i in range(4):
 .....:     for j in range(4):
 .....:         if j > i:
 .....:             break
 .....:         print((i, j))
 .....:
(0, 0)
(1, 0)
(1, 1)
(2, 0)
(2, 1)
(2, 2)
(3, 0)
(3, 1)
(3, 2)
(3, 3)

正如我们将在更详细地看到的,如果集合或迭代器中的元素是序列(例如元组或列表),它们可以方便地在for循环语句中解包为变量:

for a, b, c in iterator:
 # do something
while 循环

while循环指定一个条件和一个要执行的代码块,直到条件评估为False或循环被显式地使用break结束为止:

x = 256
total = 0
while x > 0:
 if total > 500:
 break
 total += x
 x = x // 2
pass

pass是 Python 中的“空操作”(或“什么也不做”)语句。它可以在不需要执行任何操作的代码块中使用(或作为尚未实现的代码的占位符);它仅仅是因为 Python 使用空格来分隔代码块:

if x < 0:
 print("negative!")
elif x == 0:
 # TODO: put something smart here
 pass
else:
 print("positive!")
范围

range函数生成一系列均匀间隔的整数:

In [135]: range(10)
Out[135]: range(0, 10)

In [136]: list(range(10))
Out[136]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

可以给定起始点、终点和步长(可以是负数):

In [137]: list(range(0, 20, 2))
Out[137]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

In [138]: list(range(5, 0, -1))
Out[138]: [5, 4, 3, 2, 1]

正如您所看到的,range 生成的整数是直到但不包括终点的。range 的一个常见用途是通过索引迭代序列:

In [139]: seq = [1, 2, 3, 4]

In [140]: for i in range(len(seq)):
 .....:     print(f"element {i}: {seq[i]}")
element 0: 1
element 1: 2
element 2: 3
element 3: 4

虽然您可以使用list等函数将range生成的所有整数存储在其他数据结构中,但通常默认的迭代器形式会是您想要的。这段代码将从 0 到 99,999 之间是 3 或 5 的倍数的所有数字相加:

In [141]: total = 0

In [142]: for i in range(100_000):
 .....:     # % is the modulo operator
 .....:     if i % 3 == 0 or i % 5 == 0:
 .....:         total += i

In [143]: print(total)
2333316668

虽然生成的范围可以任意大,但在任何给定时间内的内存使用可能非常小。

2.4 结论

本章简要介绍了一些基本的 Python 语言概念以及 IPython 和 Jupyter 编程环境。在下一章中,我将讨论许多内置数据类型、函数和输入输出工具,这些内容将在本书的其余部分中持续使用。

三、内置数据结构、函数和文件

原文:wesmckinney.com/book/python-builtin

译者:飞龙

协议:CC BY-NC-SA 4.0

此开放访问网络版本的《Python 数据分析第三版》现已作为印刷版和数字版的伴侣提供。如果您发现任何勘误,请在此处报告。请注意,由 Quarto 生成的本站点的某些方面与 O’Reilly 的印刷版和电子书版本的格式不同。

如果您发现本书的在线版本有用,请考虑订购纸质版无 DRM 的电子书以支持作者。本网站的内容不得复制或再生产。代码示例采用 MIT 许可,可在 GitHub 或 Gitee 上找到。

本章讨论了内置到 Python 语言中的功能,这些功能将在整本书中被广泛使用。虽然像 pandas 和 NumPy 这样的附加库为更大的数据集添加了高级计算功能,但它们旨在与 Python 的内置数据操作工具一起使用。

我们将从 Python 的主要数据结构开始:元组、列表、字典和集合。然后,我们将讨论如何创建自己可重用的 Python 函数。最后,我们将看看 Python 文件对象的机制以及如何与本地硬盘交互。

3.1 数据结构和序列

Python 的数据结构简单而强大。掌握它们的使用是成为熟练的 Python 程序员的关键部分。我们从元组、列表和字典开始,它们是一些最常用的序列类型。

元组

元组是 Python 对象的固定长度、不可变序列,一旦分配,就无法更改。创建元组的最简单方法是使用括号括起的逗号分隔的值序列:

In [2]: tup = (4, 5, 6)

In [3]: tup
Out[3]: (4, 5, 6)

在许多情况下,括号可以省略,所以这里我们也可以这样写:

In [4]: tup = 4, 5, 6

In [5]: tup
Out[5]: (4, 5, 6)

您可以通过调用tuple将任何序列或迭代器转换为元组:

In [6]: tuple([4, 0, 2])
Out[6]: (4, 0, 2)

In [7]: tup = tuple('string')

In [8]: tup
Out[8]: ('s', 't', 'r', 'i', 'n', 'g')

元素可以使用方括号[]访问,就像大多数其他序列类型一样。与 C、C++、Java 和许多其他语言一样,在 Python 中,序列是从 0 开始索引的:

In [9]: tup[0]
Out[9]: 's'

当您在更复杂的表达式中定义元组时,通常需要将值括在括号中,就像在创建元组的示例中一样:

In [10]: nested_tup = (4, 5, 6), (7, 8)

In [11]: nested_tup
Out[11]: ((4, 5, 6), (7, 8))

In [12]: nested_tup[0]
Out[12]: (4, 5, 6)

In [13]: nested_tup[1]
Out[13]: (7, 8)

虽然存储在元组中的对象本身可能是可变的,但一旦创建了元组,就无法修改存储在每个槽中的对象:

In [14]: tup = tuple(['foo', [1, 2], True])

In [15]: tup[2] = False
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-b89d0c4ae599> in <module>
----> 1 tup[2] = False
TypeError: 'tuple' object does not support item assignment

如果元组中的对象是可变的,比如列表,您可以就地修改它:

In [16]: tup[1].append(3)

In [17]: tup
Out[17]: ('foo', [1, 2, 3], True)

您可以使用+运算符连接元组以生成更长的元组:

In [18]: (4, None, 'foo') + (6, 0) + ('bar',)
Out[18]: (4, None, 'foo', 6, 0, 'bar')

将元组乘以一个整数,与列表一样,会产生该元组的多个副本的效果:

In [19]: ('foo', 'bar') * 4
Out[19]: ('foo', 'bar', 'foo', 'bar', 'foo', 'bar', 'foo', 'bar')

请注意,对象本身并没有被复制,只有对它们的引用。

解包元组

如果您尝试对类似元组的变量表达式进行赋值,Python 将尝试在等号右侧解包值:

In [20]: tup = (4, 5, 6)

In [21]: a, b, c = tup

In [22]: b
Out[22]: 5

即使包含嵌套元组的序列也可以解包:

In [23]: tup = 4, 5, (6, 7)

In [24]: a, b, (c, d) = tup

In [25]: d
Out[25]: 7

使用这个功能,您可以轻松交换变量名,这在许多语言中可能看起来像:

tmp = a
a = b
b = tmp

但是,在 Python 中,交换可以这样做:

In [26]: a, b = 1, 2

In [27]: a
Out[27]: 1

In [28]: b
Out[28]: 2

In [29]: b, a = a, b

In [30]: a
Out[30]: 2

In [31]: b
Out[31]: 1

变量解包的常见用途是迭代元组或列表的序列:

In [32]: seq = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]

In [33]: for a, b, c in seq:
 ....:     print(f'a={a}, b={b}, c={c}')
a=1, b=2, c=3
a=4, b=5, c=6
a=7, b=8, c=9

另一个常见用途是从函数返回多个值。我稍后会更详细地介绍这个问题。

有一些情况下,您可能希望从元组的开头“摘取”一些元素。有一种特殊的语法可以做到这一点,*rest,这也用于函数签名中捕获任意长的位置参数:

In [34]: values = 1, 2, 3, 4, 5

In [35]: a, b, *rest = values

In [36]: a
Out[36]: 1

In [37]: b
Out[37]: 2

In [38]: rest
Out[38]: [3, 4, 5]

这个rest位有时是您想要丢弃的内容;rest名称没有特殊之处。作为一种惯例,许多 Python 程序员会使用下划线(_)表示不需要的变量:

In [39]: a, b, *_ = values
元组方法

由于元组的大小和内容不能被修改,因此实例方法非常少。一个特别有用的方法(也适用于列表)是count,它计算值的出现次数:

In [40]: a = (1, 2, 2, 2, 3, 4, 2)

In [41]: a.count(2)
Out[41]: 4

列表

与元组相反,列表是可变长度的,其内容可以就地修改。列表是可变的。您可以使用方括号[]定义它们,也可以使用list类型函数:

In [42]: a_list = [2, 3, 7, None]

In [43]: tup = ("foo", "bar", "baz")

In [44]: b_list = list(tup)

In [45]: b_list
Out[45]: ['foo', 'bar', 'baz']

In [46]: b_list[1] = "peekaboo"

In [47]: b_list
Out[47]: ['foo', 'peekaboo', 'baz']

列表和元组在语义上是相似的(尽管元组不能被修改),并且可以在许多函数中互换使用。

list内置函数在数据处理中经常用作实例化迭代器或生成器表达式的方法:

In [48]: gen = range(10)

In [49]: gen
Out[49]: range(0, 10)

In [50]: list(gen)
Out[50]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
添加和删除元素

元素可以使用append方法附加到列表的末尾:

In [51]: b_list.append("dwarf")

In [52]: b_list
Out[52]: ['foo', 'peekaboo', 'baz', 'dwarf']

使用insert可以在列表中的特定位置插入元素:

In [53]: b_list.insert(1, "red")

In [54]: b_list
Out[54]: ['foo', 'red', 'peekaboo', 'baz', 'dwarf']

插入索引必须在列表的长度之间,包括 0 和长度。

警告:

append相比,insert的计算成本较高,因为必须在内部移动后续元素的引用以为新元素腾出空间。如果需要在序列的开头和结尾插入元素,您可能希望探索collections.deque,这是一个双端队列,专为此目的进行了优化,并且包含在 Python 标准库中。

insert的反向操作是pop,它会删除并返回特定索引处的元素:

In [55]: b_list.pop(2)
Out[55]: 'peekaboo'

In [56]: b_list
Out[56]: ['foo', 'red', 'baz', 'dwarf']

可以使用remove按值删除元素,它会定位第一个这样的值并将其从列表中删除:

In [57]: b_list.append("foo")

In [58]: b_list
Out[58]: ['foo', 'red', 'baz', 'dwarf', 'foo']

In [59]: b_list.remove("foo")

In [60]: b_list
Out[60]: ['red', 'baz', 'dwarf', 'foo']

如果不关心性能,通过使用appendremove,可以使用 Python 列表作为类似集合的数据结构(尽管 Python 有实际的集合对象,稍后讨论)。

使用in关键字检查列表是否包含一个值:

In [61]: "dwarf" in b_list
Out[61]: True

关键字not可以用来否定in

In [62]: "dwarf" not in b_list
Out[62]: False

检查列表是否包含一个值比使用字典和集合慢得多(即将介绍),因为 Python 会在线性扫描列表的值,而可以在常量时间内检查其他值(基于哈希表)。

连接和组合列表

与元组类似,使用+将两个列表相加会将它们连接起来:

In [63]: [4, None, "foo"] + [7, 8, (2, 3)]
Out[63]: [4, None, 'foo', 7, 8, (2, 3)]

如果已经定义了一个列表,可以使用extend方法将多个元素附加到其中:

In [64]: x = [4, None, "foo"]

In [65]: x.extend([7, 8, (2, 3)])

In [66]: x
Out[66]: [4, None, 'foo', 7, 8, (2, 3)]

请注意,通过加法进行列表连接是一种相对昂贵的操作,因为必须创建一个新列表并复制对象。通常最好使用extend将元素附加到现有列表中,特别是如果您正在构建一个大列表。因此:

everything = []
for chunk in list_of_lists:
 everything.extend(chunk)

比连接替代方案更快:

everything = []
for chunk in list_of_lists:
 everything = everything + chunk
排序

您可以通过调用其sort函数就地对列表进行排序(而不创建新对象):

In [67]: a = [7, 2, 5, 1, 3]

In [68]: a.sort()

In [69]: a
Out[69]: [1, 2, 3, 5, 7]

sort有一些选项,偶尔会派上用场。其中之一是能够传递一个次要排序键——即生成用于对对象进行排序的值的函数。例如,我们可以按字符串的长度对字符串集合进行排序:

In [70]: b = ["saw", "small", "He", "foxes", "six"]

In [71]: b.sort(key=len)

In [72]: b
Out[72]: ['He', 'saw', 'six', 'small', 'foxes']

很快,我们将看一下sorted函数,它可以生成一份排序后的一般序列的副本。

切片

您可以使用切片表示法选择大多数序列类型的部分,其基本形式是将start:stop传递给索引运算符[]

In [73]: seq = [7, 2, 3, 7, 5, 6, 0, 1]

In [74]: seq[1:5]
Out[74]: [2, 3, 7, 5]

切片也可以用序列赋值:

In [75]: seq[3:5] = [6, 3]

In [76]: seq
Out[76]: [7, 2, 3, 6, 3, 6, 0, 1]

虽然start索引处的元素被包括在内,但stop索引不包括在内,因此结果中的元素数量为stop - start

startstop可以省略,此时它们分别默认为序列的开头和序列的结尾:

In [77]: seq[:5]
Out[77]: [7, 2, 3, 6, 3]

In [78]: seq[3:]
Out[78]: [6, 3, 6, 0, 1]

负索引相对于末尾切片序列:

In [79]: seq[-4:]
Out[79]: [3, 6, 0, 1]

In [80]: seq[-6:-2]
Out[80]: [3, 6, 3, 6]

切片语义需要一点时间来适应,特别是如果你是从 R 或 MATLAB 过来的。参见图 3.1 以了解使用正整数和负整数进行切片的有用示例。在图中,索引显示在“箱边缘”,以帮助显示使用正整数或负整数索引开始和停止的切片选择。

图 3.1:Python 切片约定的示例

第二个冒号后也可以使用step,比如,每隔一个元素取一个:

In [81]: seq[::2]
Out[81]: [7, 3, 3, 0]

这种方法的一个巧妙用法是传递-1,这样可以有效地反转列表或元组:

In [82]: seq[::-1]
Out[82]: [1, 0, 6, 3, 6, 3, 2, 7]

字典

字典或dict可能是 Python 中最重要的内置数据结构。在其他编程语言中,字典有时被称为哈希映射关联数组。字典存储一组键-值对,其中是 Python 对象。每个键都与一个值关联,以便可以方便地检索、插入、修改或删除给定特定键的值。创建字典的一种方法是使用大括号{}和冒号来分隔键和值:

In [83]: empty_dict = {}

In [84]: d1 = {"a": "some value", "b": [1, 2, 3, 4]}

In [85]: d1
Out[85]: {'a': 'some value', 'b': [1, 2, 3, 4]}

可以使用与访问列表或元组元素相同的语法来访问、插入或设置元素:

In [86]: d1[7] = "an integer"

In [87]: d1
Out[87]: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}

In [88]: d1["b"]
Out[88]: [1, 2, 3, 4]

你可以使用与检查列表或元组是否包含值相同的语法来检查字典是否包含键:

In [89]: "b" in d1
Out[89]: True

可以使用del关键字或pop方法(同时返回值并删除键)来删除值:

In [90]: d1[5] = "some value"

In [91]: d1
Out[91]: 
{'a': 'some value',
 'b': [1, 2, 3, 4],
 7: 'an integer',
 5: 'some value'}

In [92]: d1["dummy"] = "another value"

In [93]: d1
Out[93]: 
{'a': 'some value',
 'b': [1, 2, 3, 4],
 7: 'an integer',
 5: 'some value',
 'dummy': 'another value'}

In [94]: del d1[5]

In [95]: d1
Out[95]: 
{'a': 'some value',
 'b': [1, 2, 3, 4],
 7: 'an integer',
 'dummy': 'another value'}

In [96]: ret = d1.pop("dummy")

In [97]: ret
Out[97]: 'another value'

In [98]: d1
Out[98]: {'a': 'some value', 'b': [1, 2, 3, 4], 7: 'an integer'}

keysvalues方法分别为你提供字典的键和值的迭代器。键的顺序取决于它们插入的顺序,这些函数以相同的顺序输出键和值:

In [99]: list(d1.keys())
Out[99]: ['a', 'b', 7]

In [100]: list(d1.values())
Out[100]: ['some value', [1, 2, 3, 4], 'an integer']

如果需要同时迭代键和值,可以使用items方法以 2 元组的形式迭代键和值:

In [101]: list(d1.items())
Out[101]: [('a', 'some value'), ('b', [1, 2, 3, 4]), (7, 'an integer')]

可以使用update方法将一个字典合并到另一个字典中:

In [102]: d1.update({"b": "foo", "c": 12})

In [103]: d1
Out[103]: {'a': 'some value', 'b': 'foo', 7: 'an integer', 'c': 12}

update方法会直接更改字典,因此传递给update的数据中的任何现有键都将丢弃其旧值。

从序列创建字典

通常会偶尔出现两个你想要逐个元素配对的序列。作为第一步,你可能会编写这样的代码:

mapping = {}
for key, value in zip(key_list, value_list):
 mapping[key] = value

由于字典本质上是 2 元组的集合,dict函数接受一个 2 元组的列表:

In [104]: tuples = zip(range(5), reversed(range(5)))

In [105]: tuples
Out[105]: <zip at 0x17d604d00>

In [106]: mapping = dict(tuples)

In [107]: mapping
Out[107]: {0: 4, 1: 3, 2: 2, 3: 1, 4: 0}

稍后我们将讨论字典推导,这是构建字典的另一种方法。

默认值

通常会有类似以下逻辑:

if key in some_dict:
 value = some_dict[key]
else:
 value = default_value

因此,字典方法getpop可以接受要返回的默认值,因此上述if-else块可以简单地写为:

value = some_dict.get(key, default_value)

get默认情况下会返回None,如果键不存在,而pop会引发异常。在设置值时,可能字典中的值是另一种集合,比如列表。例如,你可以想象将单词列表按照它们的首字母分类为列表的字典:

In [108]: words = ["apple", "bat", "bar", "atom", "book"]

In [109]: by_letter = {}

In [110]: for word in words:
 .....:     letter = word[0]
 .....:     if letter not in by_letter:
 .....:         by_letter[letter] = [word]
 .....:     else:
 .....:         by_letter[letter].append(word)
 .....:

In [111]: by_letter
Out[111]: {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}

setdefault字典方法可用于简化此工作流程。前面的for循环可以重写为:

In [112]: by_letter = {}

In [113]: for word in words:
 .....:     letter = word[0]
 .....:     by_letter.setdefault(letter, []).append(word)
 .....:

In [114]: by_letter
Out[114]: {'a': ['apple', 'atom'], 'b': ['bat', 'bar', 'book']}

内置的collections模块有一个有用的类defaultdict,使这更加容易。要创建一个,你需要传递一个类型或函数,用于为字典中的每个槽生成默认值:

In [115]: from collections import defaultdict

In [116]: by_letter = defaultdict(list)

In [117]: for word in words:
 .....:     by_letter[word[0]].append(word)
有效的字典键类型

虽然字典的值可以是任何 Python 对象,但键通常必须是不可变对象,如标量类型(int、float、string)或元组(元组中的所有对象也必须是不可变的)。这里的技术术语是可哈希性。你可以使用hash函数检查对象是否可哈希(可以用作字典中的键):

In [118]: hash("string")
Out[118]: 4022908869268713487

In [119]: hash((1, 2, (2, 3)))
Out[119]: -9209053662355515447

In [120]: hash((1, 2, [2, 3])) # fails because lists are mutable
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-120-473c35a62c0b> in <module>
----> 1 hash((1, 2, [2, 3])) # fails because lists are mutable
TypeError: unhashable type: 'list'

通常情况下,使用hash函数时看到的哈希值将取决于你使用的 Python 版本。

要将列表用作键,一种选择是将其转换为元组,只要其元素也可以被散列:

In [121]: d = {}

In [122]: d[tuple([1, 2, 3])] = 5

In [123]: d
Out[123]: {(1, 2, 3): 5}

集合

集合是一个无序的唯一元素集合。可以通过set函数或使用花括号的集合字面值来创建集合:

In [124]: set([2, 2, 2, 1, 3, 3])
Out[124]: {1, 2, 3}

In [125]: {2, 2, 2, 1, 3, 3}
Out[125]: {1, 2, 3}

集合支持数学集合操作,如并集、交集、差集和对称差集。考虑这两个示例集合:

In [126]: a = {1, 2, 3, 4, 5}

In [127]: b = {3, 4, 5, 6, 7, 8}

这两个集合的并集是两个集合中出现的不同元素的集合。可以使用union方法或|二进制运算符来计算:

In [128]: a.union(b)
Out[128]: {1, 2, 3, 4, 5, 6, 7, 8}

In [129]: a | b
Out[129]: {1, 2, 3, 4, 5, 6, 7, 8}

交集包含两个集合中都出现的元素。可以使用&运算符或intersection方法:

In [130]: a.intersection(b)
Out[130]: {3, 4, 5}

In [131]: a & b
Out[131]: {3, 4, 5}

请参见表 3.1 以获取常用集合方法的列表。

表 3.1:Python 集合操作

函数替代语法描述
a.add(x)N/A将元素x添加到集合a
a.clear()N/A将集合a重置为空状态,丢弃所有元素
a.remove(x)N/A从集合a中删除元素x
a.pop()N/A从集合a中删除一个任意元素,如果集合为空则引发KeyError
a.union(b)a &#124; bab中所有唯一的元素
a.update(b)a &#124;= ba的内容设置为ab中元素的并集
a.intersection(b)a & bab存在的所有元素
a.intersection_update(b)a &= ba的内容设置为ab中元素的交集
a.difference(b)a - ba中不在b中的元素
a.difference_update(b)a -= ba设置为a中不在b中的元素
a.symmetric_difference(b)a ^ bab中的所有元素,但不是两者都有的
a.symmetric_difference_update(b)a ^= ba设置为ab中的元素,但不是两者都有的
a.issubset(b)<=如果a的元素都包含在b中,则为True
a.issuperset(b)>=如果b的元素都包含在a中,则为True
a.isdisjoint(b)N/A如果ab没有共同元素,则为True

注意

如果将不是集合的输入传递给unionintersection等方法,Python 将在执行操作之前将输入转换为集合。在使用二进制运算符时,两个对象必须已经是集合。

所有逻辑集合操作都有原地对应物,这使您可以用结果替换操作左侧集合的内容。对于非常大的集合,这可能更有效率:*

In [132]: c = a.copy()

In [133]: c |= b

In [134]: c
Out[134]: {1, 2, 3, 4, 5, 6, 7, 8}

In [135]: d = a.copy()

In [136]: d &= b

In [137]: d
Out[137]: {3, 4, 5}

与字典键类似,集合元素通常必须是不可变的,并且它们必须是可散列的(这意味着对值调用hash不会引发异常)。为了将类似列表的元素(或其他可变序列)存储在集合中,可以将它们转换为元组:

In [138]: my_data = [1, 2, 3, 4]

In [139]: my_set = {tuple(my_data)}

In [140]: my_set
Out[140]: {(1, 2, 3, 4)}

您还可以检查一个集合是否是另一个集合的子集(包含在内)或超集(包含所有元素):

In [141]: a_set = {1, 2, 3, 4, 5}

In [142]: {1, 2, 3}.issubset(a_set)
Out[142]: True

In [143]: a_set.issuperset({1, 2, 3})
Out[143]: True

只有当集合的内容相等时,集合才相等:

In [144]: {1, 2, 3} == {3, 2, 1}
Out[144]: True

内置序列函数

Python 有一些有用的序列函数,您应该熟悉并在任何机会使用。

enumerate

在迭代序列时,通常希望跟踪当前项目的索引。自己动手的方法如下:

index = 0
for value in collection:
 # do something with value
 index += 1

由于这种情况很常见,Python 有一个内置函数enumerate,它返回一个(i, value)元组序列:

for index, value in enumerate(collection):
 # do something with value
sorted

sorted函数从任何序列的元素返回一个新的排序列表:

In [145]: sorted([7, 1, 2, 6, 0, 3, 2])
Out[145]: [0, 1, 2, 2, 3, 6, 7]

In [146]: sorted("horse race")
Out[146]: [' ', 'a', 'c', 'e', 'e', 'h', 'o', 'r', 'r', 's']

sorted函数接受与列表的sort方法相同的参数。

zip

zip将多个列表、元组或其他序列的元素“配对”起来,以创建一个元组列表:

In [147]: seq1 = ["foo", "bar", "baz"]

In [148]: seq2 = ["one", "two", "three"]

In [149]: zipped = zip(seq1, seq2)

In [150]: list(zipped)
Out[150]: [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]

zip可以接受任意数量的序列,并且它生成的元素数量由最短的序列决定:

In [151]: seq3 = [False, True]

In [152]: list(zip(seq1, seq2, seq3))
Out[152]: [('foo', 'one', False), ('bar', 'two', True)]

zip的一个常见用法是同时迭代多个序列,可能还与enumerate结合使用:

In [153]: for index, (a, b) in enumerate(zip(seq1, seq2)):
 .....:     print(f"{index}: {a}, {b}")
 .....:
0: foo, one
1: bar, two
2: baz, three
反转

reversed以相反的顺序迭代序列的元素:

In [154]: list(reversed(range(10)))
Out[154]: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

请记住,reversed是一个生成器(稍后将更详细讨论),因此它不会创建反转的序列,直到实现(例如,使用listfor循环)。

列表、集合和字典推导

列表推导是 Python 语言中一个方便且广泛使用的特性。它们允许您通过过滤集合的元素,将通过过滤的元素转换为一个简洁的表达式来简洁地形成一个新列表。它们的基本形式如下:

[expr for value in collection if condition]

这等同于以下的for循环:

result = []
for value in collection:
 if condition:
 result.append(expr)

过滤条件可以被省略,只留下表达式。例如,给定一个字符串列表,我们可以过滤出长度为2或更少的字符串,并将它们转换为大写:

In [155]: strings = ["a", "as", "bat", "car", "dove", "python"]

In [156]: [x.upper() for x in strings if len(x) > 2]
Out[156]: ['BAT', 'CAR', 'DOVE', 'PYTHON']

集合和字典推导是一个自然的扩展,以一种类似的方式产生集合和字典,而不是列表。

字典推导看起来像这样:

dict_comp = {key-expr: value-expr for value in collection
 if condition}

集合推导看起来与等效的列表推导相同,只是用花括号代替方括号:

set_comp = {expr for value in collection if condition}

与列表推导类似,集合和字典推导大多是便利性的,但它们同样可以使代码更易于编写和阅读。考虑之前的字符串列表。假设我们想要一个集合,其中只包含集合中包含的字符串的长度;我们可以很容易地使用集合推导来计算:

In [157]: unique_lengths = {len(x) for x in strings}

In [158]: unique_lengths
Out[158]: {1, 2, 3, 4, 6}

我们也可以更加功能化地使用map函数,稍后介绍:

In [159]: set(map(len, strings))
Out[159]: {1, 2, 3, 4, 6}

作为一个简单的字典推导示例,我们可以创建一个查找这些字符串在列表中位置的查找映射:

In [160]: loc_mapping = {value: index for index, value in enumerate(strings)}

In [161]: loc_mapping
Out[161]: {'a': 0, 'as': 1, 'bat': 2, 'car': 3, 'dove': 4, 'python': 5}
嵌套列表推导

假设我们有一个包含一些英文和西班牙名字的列表列表:

In [162]: all_data = [["John", "Emily", "Michael", "Mary", "Steven"],
 .....:             ["Maria", "Juan", "Javier", "Natalia", "Pilar"]]

假设我们想要获得一个包含所有包含两个或更多个a的名称的单个列表。我们可以通过一个简单的for循环来实现:

In [163]: names_of_interest = []

In [164]: for names in all_data:
 .....:     enough_as = [name for name in names if name.count("a") >= 2]
 .....:     names_of_interest.extend(enough_as)
 .....:

In [165]: names_of_interest
Out[165]: ['Maria', 'Natalia']

实际上,您可以将整个操作封装在一个单独的嵌套列表推导中,看起来像:

In [166]: result = [name for names in all_data for name in names
 .....:           if name.count("a") >= 2]

In [167]: result
Out[167]: ['Maria', 'Natalia']

起初,嵌套列表推导可能有点难以理解。列表推导的for部分按照嵌套的顺序排列,任何过滤条件都放在最后。这里是另一个示例,我们将整数元组的列表“展平”为一个简单的整数列表:

In [168]: some_tuples = [(1, 2, 3), (4, 5, 6), (7, 8, 9)]

In [169]: flattened = [x for tup in some_tuples for x in tup]

In [170]: flattened
Out[170]: [1, 2, 3, 4, 5, 6, 7, 8, 9]

请记住,如果您写一个嵌套的for循环而不是列表推导,for表达式的顺序将是相同的:

flattened = []

for tup in some_tuples:
 for x in tup:
 flattened.append(x)

您可以有任意多层的嵌套,尽管如果您有超过两三层的嵌套,您可能应该开始质疑这是否在代码可读性方面是有意义的。重要的是要区分刚刚显示的语法与列表推导内部的列表推导,后者也是完全有效的:

In [172]: [[x for x in tup] for tup in some_tuples]
Out[172]: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

这将产生一个列表的列表,而不是所有内部元素的扁平化列表。

3.2 函数

函数是 Python 中代码组织和重用的主要和最重要的方法。作为一个经验法则,如果您预计需要重复相同或非常相似的代码超过一次,编写可重用的函数可能是值得的。函数还可以通过给一组 Python 语句命名来使您的代码更易读。

函数使用def关键字声明。函数包含一个代码块,可选使用return关键字:

In [173]: def my_function(x, y):
 .....:     return x + y

当到达带有return的行时,return后的值或表达式将发送到调用函数的上下文,例如:

In [174]: my_function(1, 2)
Out[174]: 3

In [175]: result = my_function(1, 2)

In [176]: result
Out[176]: 3

有多个return语句是没有问题的。如果 Python 在函数结尾处没有遇到return语句,将自动返回None。例如:

In [177]: def function_without_return(x):
 .....:     print(x)

In [178]: result = function_without_return("hello!")
hello!

In [179]: print(result)
None

每个函数可以有 位置 参数和 关键字 参数。关键字参数最常用于指定默认值或可选参数。在这里,我们将定义一个带有默认值 1.5 的可选 z 参数的函数:

def my_function2(x, y, z=1.5):
 if z > 1:
 return z * (x + y)
 else:
 return z / (x + y)

虽然关键字参数是可选的,但在调用函数时必须指定所有位置参数。

您可以向 z 参数传递值,可以使用关键字也可以不使用关键字,但建议使用关键字:

In [181]: my_function2(5, 6, z=0.7)
Out[181]: 0.06363636363636363

In [182]: my_function2(3.14, 7, 3.5)
Out[182]: 35.49

In [183]: my_function2(10, 20)
Out[183]: 45.0

对函数参数的主要限制是关键字参数 必须 跟在位置参数(如果有的话)后面。您可以以任何顺序指定关键字参数。这使您不必记住函数参数的指定顺序。您只需要记住它们的名称。

命名空间、作用域和本地函数

函数可以访问函数内部创建的变量以及函数外部在更高(甚至 全局)作用域中的变量。在 Python 中描述变量作用域的另一种更具描述性的名称是 命名空间。在函数内部分配的任何变量默认分配给本地命名空间。本地命名空间在函数调用时创建,并立即由函数的参数填充。函数完成后,本地命名空间将被销毁(有一些例外情况超出了本章的范围)。考虑以下函数:

def func():
 a = []
 for i in range(5):
 a.append(i)

当调用 func() 时,将创建空列表 a,附加五个元素,然后在函数退出时销毁 a。假设我们改为这样声明 a

In [184]: a = []

In [185]: def func():
 .....:     for i in range(5):
 .....:         a.append(i)

每次调用 func 都会修改列表 a

In [186]: func()

In [187]: a
Out[187]: [0, 1, 2, 3, 4]

In [188]: func()

In [189]: a
Out[189]: [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]

在函数范围之外分配变量是可能的,但这些变量必须使用 globalnonlocal 关键字显式声明:

In [190]: a = None

In [191]: def bind_a_variable():
 .....:     global a
 .....:     a = []
 .....: bind_a_variable()
 .....:

In [192]: print(a)
[]

nonlocal 允许函数修改在非全局高级作用域中定义的变量。由于它的使用有些神秘(我在这本书中从未使用过它),我建议您查阅 Python 文档以了解更多信息。

注意

我通常不鼓励使用 global 关键字。通常,全局变量用于在系统中存储某种状态。如果您发现自己使用了很多全局变量,这可能表明需要使用面向对象编程(使用类)

返回多个值

当我在 Java 和 C++ 中编程后第一次在 Python 中编程时,我最喜欢的功能之一是能够以简单的语法从函数中返回多个值。这里有一个例子:

def f():
 a = 5
 b = 6
 c = 7
 return a, b, c

a, b, c = f()

在数据分析和其他科学应用中,您可能经常这样做。这里发生的是函数实际上只返回一个对象,一个元组,然后将其解包为结果变量。在前面的例子中,我们可以这样做:

return_value = f()

在这种情况下,return_value 将是一个包含三个返回变量的 3 元组。与之前返回多个值的一个潜在有吸引力的替代方法可能是返回一个字典:

def f():
 a = 5
 b = 6
 c = 7
 return {"a" : a, "b" : b, "c" : c}

这种替代技术可以根据您尝试做什么而有用。

函数是对象

由于 Python 函数是对象,许多构造可以很容易地表达,而在其他语言中很难做到。假设我们正在进行一些数据清理,并需要对以下字符串列表应用一系列转换:

In [193]: states = ["   Alabama ", "Georgia!", "Georgia", "georgia", "FlOrIda",
 .....:           "south   carolina##", "West virginia?"]

任何曾经处理过用户提交的调查数据的人都会看到这样混乱的结果。需要做很多事情才能使这个字符串列表统一并准备好进行分析:去除空格、删除标点符号,并标准化适当的大写。其中一种方法是使用内置的字符串方法以及 re 标准库模块进行正则表达式:

import re

def clean_strings(strings):
 result = []
 for value in strings:
 value = value.strip()
 value = re.sub("[!#?]", "", value)
 value = value.title()
 result.append(value)
 return result

结果如下:

In [195]: clean_strings(states)
Out[195]: 
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']

您可能会发现有用的另一种方法是制作一个要应用于特定字符串集的操作列表:

def remove_punctuation(value):
 return re.sub("[!#?]", "", value)

clean_ops = [str.strip, remove_punctuation, str.title]

def clean_strings(strings, ops):
 result = []
 for value in strings:
 for func in ops:
 value = func(value)
 result.append(value)
 return result

然后我们有以下内容:

In [197]: clean_strings(states, clean_ops)
Out[197]: 
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']

像这样的更函数式模式使您能够轻松修改字符串在非常高级别上的转换方式。clean_strings函数现在也更具可重用性和通用性。

您可以将函数用作其他函数的参数,比如内置的map函数,它将一个函数应用于某种序列:

In [198]: for x in map(remove_punctuation, states):
 .....:     print(x)
Alabama 
Georgia
Georgia
georgia
FlOrIda
south   carolina
West virginia

map可以作为替代方案用于列表推导而不需要任何过滤器。

匿名(Lambda)函数

Python 支持所谓的匿名lambda函数,这是一种编写由单个语句组成的函数的方式,其结果是返回值。它们使用lambda关键字定义,该关键字除了“我们正在声明一个匿名函数”之外没有其他含义:

In [199]: def short_function(x):
 .....:     return x * 2

In [200]: equiv_anon = lambda x: x * 2

我通常在本书的其余部分中将这些称为 lambda 函数。它们在数据分析中特别方便,因为正如您将看到的,有许多情况下,数据转换函数将接受函数作为参数。与编写完整函数声明或甚至将 lambda 函数分配给本地变量相比,传递 lambda 函数通常更少输入(更清晰)。考虑这个例子:

In [201]: def apply_to_list(some_list, f):
 .....:     return [f(x) for x in some_list]

In [202]: ints = [4, 0, 1, 5, 6]

In [203]: apply_to_list(ints, lambda x: x * 2)
Out[203]: [8, 0, 2, 10, 12]

您也可以写成[x * 2 for x in ints],但在这里我们能够简洁地将自定义运算符传递给apply_to_list函数。

举个例子,假设你想按每个字符串中不同字母的数量对字符串集合进行排序:

In [204]: strings = ["foo", "card", "bar", "aaaa", "abab"]

在这里,我们可以将一个 lambda 函数传递给列表的sort方法:

In [205]: strings.sort(key=lambda x: len(set(x)))

In [206]: strings
Out[206]: ['aaaa', 'foo', 'abab', 'bar', 'card']

生成器

Python 中的许多对象支持迭代,例如列表中的对象或文件中的行。这是通过迭代器协议实现的,这是一种使对象可迭代的通用方法。例如,对字典进行迭代会产生字典键:

In [207]: some_dict = {"a": 1, "b": 2, "c": 3}

In [208]: for key in some_dict:
 .....:     print(key)
a
b
c

当您写for key in some_dict时,Python 解释器首先尝试从some_dict创建一个迭代器:

In [209]: dict_iterator = iter(some_dict)

In [210]: dict_iterator
Out[210]: <dict_keyiterator at 0x17d60e020>

迭代器是任何对象,在上下文中像for循环中使用时,将向 Python 解释器产生对象。大多数期望列表或类似列表的对象的方法也将接受任何可迭代对象。这包括内置方法如minmaxsum,以及类构造函数如listtuple

In [211]: list(dict_iterator)
Out[211]: ['a', 'b', 'c']

生成器是一种方便的方式,类似于编写普通函数,来构造一个新的可迭代对象。普通函数一次执行并返回一个结果,而生成器可以通过暂停和恢复执行每次使用生成器时返回多个值的序列。要创建一个生成器,请在函数中使用yield关键字而不是return

def squares(n=10):
 print(f"Generating squares from 1 to {n ** 2}")
 for i in range(1, n + 1):
 yield i ** 2

当您实际调用生成器时,不会立即执行任何代码:

In [213]: gen = squares()

In [214]: gen
Out[214]: <generator object squares at 0x17d5fea40>

直到您请求生成器的元素时,它才开始执行其代码:

In [215]: for x in gen:
 .....:     print(x, end=" ")
Generating squares from 1 to 100
1 4 9 16 25 36 49 64 81 100

注意

由于生成器一次产生一个元素的输出,而不是一次产生整个列表,这可以帮助您的程序使用更少的内存。

生成器表达式

另一种生成器的方法是使用生成器表达式。这是列表、字典和集合推导的生成器类比。要创建一个,将否则是列表推导的内容括在括号中而不是方括号中:

In [216]: gen = (x ** 2 for x in range(100))

In [217]: gen
Out[217]: <generator object <genexpr> at 0x17d5feff0>

这等同于以下更冗长的生成器:

def _make_gen():
 for x in range(100):
 yield x ** 2
gen = _make_gen()

生成器表达式可以在某些情况下用作函数参数,而不是列表推导:

In [218]: sum(x ** 2 for x in range(100))
Out[218]: 328350

In [219]: dict((i, i ** 2) for i in range(5))
Out[219]: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

根据推导表达式产生的元素数量,生成器版本有时可以更有意义地更快。

itertools 模块

标准库itertools模块具有许多常见数据算法的生成器集合。例如,groupby接受任何序列和一个函数,通过函数的返回值对序列中的连续元素进行分组。这里是一个例子:

In [220]: import itertools

In [221]: def first_letter(x):
 .....:     return x[0]

In [222]: names = ["Alan", "Adam", "Wes", "Will", "Albert", "Steven"]

In [223]: for letter, names in itertools.groupby(names, first_letter):
 .....:     print(letter, list(names)) # names is a generator
A ['Alan', 'Adam']
W ['Wes', 'Will']
A ['Albert']
S ['Steven']

查看表 3.2 以获取我经常发现有用的其他一些itertools函数列表。您可能想查看官方 Python 文档以获取有关这个有用的内置实用程序模块的更多信息。

表 3.2:一些有用的itertools函数

函数描述
chain(*iterables)通过将迭代器链接在一起生成序列。一旦第一个迭代器的元素用尽,将返回下一个迭代器的元素,依此类推。
combinations(iterable, k)生成可迭代对象中所有可能的k元素元组的序列,忽略顺序且不重复(另请参阅伴随函数combinations_with_replacement)。
permutations(iterable, k)生成可迭代对象中所有可能的k元素元组的序列,保持顺序。
groupby(iterable[, keyfunc])为每个唯一键生成(key, sub-iterator)

| product(*iterables, repeat=1) | 生成输入可迭代对象的笛卡尔积作为元组,类似于嵌套的for循环。 |

错误和异常处理

处理 Python 错误或异常的优雅是构建健壮程序的重要部分。在数据分析应用中,许多函数只对特定类型的输入有效。例如,Python 的float函数能够将字符串转换为浮点数,但在不当输入时会引发ValueError异常:

In [224]: float("1.2345")
Out[224]: 1.2345

In [225]: float("something")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-225-5ccfe07933f4> in <module>
----> 1 float("something")
ValueError: could not convert string to float: 'something'

假设我们想要一个版本的float,它能够优雅地失败,返回输入参数。我们可以通过编写一个函数,在其中将对float的调用封装在try/except块中来实现这一点(在 IPython 中执行此代码):

def attempt_float(x):
 try:
 return float(x)
 except:
 return x

块中的except部分的代码只有在float(x)引发异常时才会执行:

In [227]: attempt_float("1.2345")
Out[227]: 1.2345

In [228]: attempt_float("something")
Out[228]: 'something'

您可能会注意到float可能引发除ValueError之外的异常:

In [229]: float((1, 2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-229-82f777b0e564> in <module>
----> 1 float((1, 2))
TypeError: float() argument must be a string or a real number, not 'tuple'

您可能只想抑制ValueError,因为TypeError(输入不是字符串或数值)可能表明程序中存在合法错误。要做到这一点,请在except后面写上异常类型:

def attempt_float(x):
 try:
 return float(x)
 except ValueError:
 return x

然后我们有:

In [231]: attempt_float((1, 2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-231-8b0026e9e6b7> in <module>
----> 1 attempt_float((1, 2))
<ipython-input-230-6209ddecd2b5> in attempt_float(x)
 1 def attempt_float(x):
 2     try:
----> 3         return float(x)
 4     except ValueError:
 5         return x
TypeError: float() argument must be a string or a real number, not 'tuple'

您可以通过编写异常类型的元组来捕获多个异常类型(括号是必需的):

def attempt_float(x):
 try:
 return float(x)
 except (TypeError, ValueError):
 return x

在某些情况下,您可能不想抑制异常,但您希望无论try块中的代码是否成功,都执行一些代码。要做到这一点,请使用finally

f = open(path, mode="w")

try:
 write_to_file(f)
finally:
 f.close()

在这里,文件对象f始终被关闭。同样,您可以使用else来执行仅在try:块成功时执行的代码:

f = open(path, mode="w")

try:
 write_to_file(f)
except:
 print("Failed")
else:
 print("Succeeded")
finally:
 f.close()
在 IPython 中的异常

如果在%run脚本或执行任何语句时引发异常,默认情况下 IPython 将打印完整的调用堆栈跟踪(traceback),并在堆栈中的每个位置周围显示几行上下文:

In [10]: %run examples/ipython_bug.py
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/home/wesm/code/pydata-book/examples/ipython_bug.py in <module>()
 13     throws_an_exception()
 14
---> 15 calling_things()

/home/wesm/code/pydata-book/examples/ipython_bug.py in calling_things()
 11 def calling_things():
 12     works_fine()
---> 13     throws_an_exception()
 14
 15 calling_things()

/home/wesm/code/pydata-book/examples/ipython_bug.py in throws_an_exception()
 7     a = 5
 8     b = 6
----> 9     assert(a + b == 10)
 10
 11 def calling_things():

AssertionError:

仅仅通过附加上下文本身就是与标准 Python 解释器相比的一个巨大优势(标准 Python 解释器不提供任何额外上下文)。您可以使用%xmode魔术命令来控制显示的上下文量,从Plain(与标准 Python 解释器相同)到Verbose(内联函数参数值等)。正如您将在附录 B:更多关于 IPython 系统中看到的,您可以在错误发生后进行交互式事后调试,进入堆栈(使用%debug%pdb魔术)。

本书的大部分内容使用高级工具如pandas.read_csv从磁盘读取数据文件到 Python 数据结构中。然而,了解如何在 Python 中处理文件的基础知识是很重要的。幸运的是,这相对简单,这也是 Python 在文本和文件处理方面如此受欢迎的原因。

要打开一个文件进行读取或写入,请使用内置的open函数,使用相对或绝对文件路径以及可选的文件编码:

In [233]: path = "examples/segismundo.txt"

In [234]: f = open(path, encoding="utf-8")

在这里,我传递 encoding="utf-8" 作为最佳实践,因为默认的 Unicode 编码读取文件在不同平台上有所不同。

默认情况下,文件以只读模式 "r" 打开。然后我们可以像处理列表一样处理文件对象 f 并迭代文件行:

for line in f:
 print(line)

行从文件中出来时保留了行尾(EOL)标记,因此您经常会看到代码以获取文件中无行尾的行列表,如下所示:

In [235]: lines = [x.rstrip() for x in open(path, encoding="utf-8")]

In [236]: lines
Out[236]: 
['Sueña el rico en su riqueza,',
 'que más cuidados le ofrece;',
 '',
 'sueña el pobre que padece',
 'su miseria y su pobreza;',
 '',
 'sueña el que a medrar empieza,',
 'sueña el que afana y pretende,',
 'sueña el que agravia y ofende,',
 '',
 'y en el mundo, en conclusión,',
 'todos sueñan lo que son,',
 'aunque ninguno lo entiende.',
 '']

当使用 open 创建文件对象时,建议在完成后关闭文件。关闭文件会将其资源释放回操作系统:

In [237]: f.close()

使得清理打开文件更容易的一种方法是使用 with 语句:

In [238]: with open(path, encoding="utf-8") as f:
 .....:     lines = [x.rstrip() for x in f]

当退出 with 块时,这将自动关闭文件 f。确保关闭文件在许多小程序或脚本中不会导致问题,但在需要与大量文件交互的程序中可能会出现问题。

如果我们输入 f = open(path, "w")examples/segismundo.txt 将会创建一个新文件(小心!),覆盖原来的任何文件。还有 "x" 文件模式,它创建一个可写文件,但如果文件路径已经存在则失败。查看 Table 3.3 获取所有有效的文件读写模式列表。

Table 3.3: Python 文件模式

模式描述
r只读模式
w只写模式;创建一个新文件(擦除同名文件的数据)
x只写模式;创建一个新文件,但如果文件路径已经存在则失败
a追加到现有文件(如果文件不存在则创建文件)
r+读取和写入
b用于二进制文件的附加模式(即 "rb""wb"
t文件的文本模式(自动将字节解码为 Unicode);如果未指定,则为默认模式

对于可读文件,一些最常用的方法是 readseektellread 从文件返回一定数量的字符。什么构成一个“字符”取决于文件编码,或者如果文件以二进制模式打开,则是原始字节:

In [239]: f1 = open(path)

In [240]: f1.read(10)
Out[240]: 'Sueña el r'

In [241]: f2 = open(path, mode="rb")  # Binary mode

In [242]: f2.read(10)
Out[242]: b'Sue\xc3\xb1a el '

read 方法通过读取的字节数推进文件对象位置。tell 给出当前位置:

In [243]: f1.tell()
Out[243]: 11

In [244]: f2.tell()
Out[244]: 10

即使我们从以文本模式打开的文件 f1 中读取了 10 个字符,位置也是 11,因为使用默认编码解码 10 个字符需要这么多字节。您可以在 sys 模块中检查默认编码:

In [245]: import sys

In [246]: sys.getdefaultencoding()
Out[246]: 'utf-8'

为了在各个平台上获得一致的行为,最好在打开文件时传递一个编码(例如 encoding="utf-8",这是广泛使用的)。

seek 将文件位置更改为文件中指定的字节:

In [247]: f1.seek(3)
Out[247]: 3

In [248]: f1.read(1)
Out[248]: 'ñ'

In [249]: f1.tell()
Out[249]: 5

最后,我们记得关闭文件:

In [250]: f1.close()

In [251]: f2.close()

要将文本写入文件,可以使用文件的 writewritelines 方法。例如,我们可以创建一个没有空行的 examples/segismundo.txt 版本如下:

In [252]: path
Out[252]: 'examples/segismundo.txt'

In [253]: with open("tmp.txt", mode="w") as handle:
 .....:     handle.writelines(x for x in open(path) if len(x) > 1)

In [254]: with open("tmp.txt") as f:
 .....:     lines = f.readlines()

In [255]: lines
Out[255]: 
['Sueña el rico en su riqueza,\n',
 'que más cuidados le ofrece;\n',
 'sueña el pobre que padece\n',
 'su miseria y su pobreza;\n',
 'sueña el que a medrar empieza,\n',
 'sueña el que afana y pretende,\n',
 'sueña el que agravia y ofende,\n',
 'y en el mundo, en conclusión,\n',
 'todos sueñan lo que son,\n',
 'aunque ninguno lo entiende.\n']

查看 Table 3.4 获取许多最常用的文件方法。

Table 3.4: 重要的 Python 文件方法或属性

方法/属性描述
read([size])根据文件模式返回文件数据作为字节或字符串,可选的 size 参数指示要读取的字节数或字符串字符数
readable()如果文件支持 read 操作则返回 True
readlines([size])返回文件中行的列表,带有可选的 size 参数
write(string)将传递的字符串写入文件
writable()如果文件支持 write 操作则返回 True
writelines(strings)将传递的字符串序列写入文件
close()关闭文件对象
flush()刷新内部 I/O 缓冲区到磁盘
seek(pos)移动到指定的文件位置(整数)
seekable()如果文件对象支持寻找并且随机访问则返回 True(某些类似文件的对象不支持)
tell()返回当前文件位置作为整数
closed如果文件已关闭则为True
encoding用于将文件中的字节解释为 Unicode 的编码(通常为 UTF-8)

字节和 Unicode 与文件

Python 文件的默认行为(无论是可读还是可写)是文本模式,这意味着您打算使用 Python 字符串(即 Unicode)。这与二进制模式相反,您可以通过在文件模式后附加b来获得。重新访问上一节中包含 UTF-8 编码的非 ASCII 字符的文件,我们有:

In [258]: with open(path) as f:
 .....:     chars = f.read(10)

In [259]: chars
Out[259]: 'Sueña el r'

In [260]: len(chars)
Out[260]: 10

UTF-8 是一种可变长度的 Unicode 编码,因此当我从文件请求一些字符时,Python 会读取足够的字节(可能少至 10 个或多至 40 个字节)来解码相应数量的字符。如果我以"rb"模式打开文件,read请求确切数量的字节:

In [261]: with open(path, mode="rb") as f:
 .....:     data = f.read(10)

In [262]: data
Out[262]: b'Sue\xc3\xb1a el '

根据文本编码,您可能可以自己将字节解码为str对象,但前提是每个编码的 Unicode 字符都是完整形式的:

In [263]: data.decode("utf-8")
Out[263]: 'Sueña el '

In [264]: data[:4].decode("utf-8")
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
<ipython-input-264-846a5c2fed34> in <module>
----> 1 data[:4].decode("utf-8")
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 in position 3: unexpecte
d end of data

文本模式,结合openencoding选项,提供了一种方便的方法来将一个 Unicode 编码转换为另一个:

In [265]: sink_path = "sink.txt"

In [266]: with open(path) as source:
 .....:     with open(sink_path, "x", encoding="iso-8859-1") as sink:
 .....:         sink.write(source.read())

In [267]: with open(sink_path, encoding="iso-8859-1") as f:
 .....:     print(f.read(10))
Sueña el r

在除了二进制模式之外的任何模式下打开文件时要小心使用seek。如果文件位置落在定义 Unicode 字符的字节中间,那么后续的读取将导致错误:

In [269]: f = open(path, encoding='utf-8')

In [270]: f.read(5)
Out[270]: 'Sueña'

In [271]: f.seek(4)
Out[271]: 4

In [272]: f.read(1)
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
<ipython-input-272-5a354f952aa4> in <module>
----> 1 f.read(1)
~/miniforge-x86/envs/book-env/lib/python3.10/codecs.py in decode(self, input, fin
al)
 320         # decode input (taking the buffer into account)
 321         data = self.buffer + input
--> 322         (result, consumed) = self._buffer_decode(data, self.errors, final
)
 323         # keep undecoded input until the next call
 324         self.buffer = data[consumed:]
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb1 in position 0: invalid s
tart byte

In [273]: f.close()

如果您经常在非 ASCII 文本数据上进行数据分析,掌握 Python 的 Unicode 功能将会很有价值。查看Python 的在线文档获取更多信息。

3.4 结论

随着 Python 环境和语言的一些基础知识现在掌握,是时候继续学习 Python 中的 NumPy 和面向数组的计算了。

  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值