章节 2

Python 基础

本节阐述使用 Python 编程语言创建机器学习模型的基本原则。Python 是一种功能强大的高级编程语言,不需要编译;同时,在一定程度上也能提供接近底层机器代码语言的运行效率。它已经成为机器学习领域开发工具和应用程序时最常用、最受欢迎的工具之一。借助大量开放且持续更新的库,Python 具有很强的功能扩展能力。它非常适合高效地进行程序设计和代码编写。Python 还具有较快的执行速度,并且其结构非常适合处理大规模数据,而这正是机器学习中的关键环节。 本章并不是正式的 Python 教程,而是帮助读者更容易开始学习并实践后续章节中的材料。我们将重点介绍一些有用的小技巧,以及一些经常影响机器学习代码编写的行为细节。本教材中的全部代码,尤其是本章代码,...

75分钟 38,801词数 10资料

关键思想

  • 2.1 必要的软件
  • 2.2 Python 模块
  • 2.3 Python 简介
  • 2.4 Python 中的变量类型:基础与示例
  • 2.5 算术运算符
  • 2.6 布尔值和运算符

实践任务

选取一个与“Python 基础”相关的小例子,说明机器学习、向量在其中如何发挥作用,并用一句话解释结果。

打开实验室

Python 基础

打开实验室

2.1 必要的软件

本节阐述使用 Python 编程语言创建机器学习模型的基本原则。Python 是一种功能强大的高级编程语言,不需要编译;同时,在一定程度上也能提供接近底层机器代码语言的运行效率。它已经成为机器学习领域开发工具和应用程序时最常用、最受欢迎的工具之一。借助大量开放且持续更新的库,Python 具有很强的功能扩展能力。它非常适合高效地进行程序设计和代码编写。Python 还具有较快的执行速度,并且其结构非常适合处理大规模数据,而这正是机器学习中的关键环节。

本章并不是正式的 Python 教程,而是帮助读者更容易开始学习并实践后续章节中的材料。我们将重点介绍一些有用的小技巧,以及一些经常影响机器学习代码编写的行为细节。本教材中的全部代码,尤其是本章代码,可以在 https://sohoware.ru/SohoBook/ 找到。熟悉 Python 的读者可以直接跳过本章。

安装 Python 时,请访问官方网站 https://www.python.org/downloads/,选择 3.10.X 版本,并下载适用于您操作系统的安装文件。在安装过程中,务必勾选 “Add Python to PATH” 选项,以便能够从命令行访问 Python。安装完成后,在命令行输入 python --version(对于 macOS 或 Linux,可输入 python3 --version)进行检查;命令应输出 Python 的版本号。

在 macOS 终端中检查已安装的 Python 版本
在 macOS 终端中检查已安装的 Python 版本 — 2.1 必要的软件

安装 Visual Studio Code(VS Code)时,请访问 https://code.visualstudio.com/ 并下载适用于您操作系统的版本。安装完成后,打开 VS Code,通过左侧扩展面板中的 Marketplace 安装 Python 扩展。这样可以为 Python 开发优化环境,并提供便于编写和测试代码的工具与功能。

教材插图:Python 基础
教材插图:Python 基础 — 2.1 必要的软件

2.2 Python 模块

让我们采用一种不太传统的开场方式。不同于其他计算机语言教材,本书首先讨论如何使用我们自己的代码——也就是在学习过程中可以逐步开发出的代码。

首先,需要熟悉 Python 的模块系统。它允许通过使用预先开发好的库来扩展代码的功能。例如,sys 模块提供了对某些变量和函数的访问,这些变量和函数可以与 Python 解释器进行交互。

Python
import sys  # 导入 sys 模块,用于处理系统参数

sys.path.append('grbin')  # 将用户模块所在目录添加到搜索路径
# 可以取消下一行注释,以查看当前路径列表
# print(sys.path)  # 输出 Python 查找模块时使用的路径列表

代码中的注释以 # 符号开头,用于解释代码的运行方式,使其他开发者更容易理解代码。

对于较长的说明或代码文档,可以使用由三引号括起来的多行字符串作为多行注释。这样可以插入较长的说明,而不必在每一行前都写 #。

Python
'''
这是一个多行注释,可以在这里描述更复杂的逻辑,或给出详细说明。
使用多行注释可以提高代码可读性,并使代码更容易维护。
'''

为了演示,我们创建一个简单模块 grcodes.py,其中包含函数 printx。该函数用于演示如何输出变量名及其变量值。

Python
import sys  # 导入 sys 模块,用于处理系统参数

# grcodes.py 模块中的函数定义
def printx(name):
    """
    函数输出变量名及其值。
    使用示例:name = 88.0; printx('name')
    """
    frame = sys._getframe(1)
    print(name, '=', repr(eval(name, frame.f_globals, frame.f_locals)))

使用 printx 函数可以简化信息输出,减少重复代码,并降低修改代码时产生错误的概率。

Python
import grcodes as gr  # 导入用户模块,并为方便起见设置别名

x = 1.0  # 给变量赋值
print(x)  # 标准方式输出变量值

gr.printx('x')  # 使用 gr 模块中的 printx 函数输出变量名和值

# 获取 printx 函数的帮助信息
help(gr.printx)

请注意,如果使用 from grcodes import printx,则调用 printx 时前面的 gr. 就不再是必需的。这会使代码更加简洁、易读。

教材插图:Python 基础
教材插图:Python 基础 — 2.2 Python 模块

2.3 Python 简介

Python 是一种高级解释型编程语言,由 Guido van Rossum 于 20 世纪 80 年代末开发,并在 1991 年首次公开发布。创建 Python 的主要目标是通过简化代码阅读和编写来降低编程难度。Python 支持多种编程范式,包括过程式编程、面向对象编程和函数式编程,因此适用于各种类型的项目。

Python 的设计哲学追求简洁与优雅,并强调代码的高可读性。这一点通过具有意义的缩进和简洁的语法来实现。“Python 之禅”(PEP 20)阐述了 Python 设计背后的关键原则,例如“简单优于复杂”和“可读性很重要”。这些原则旨在鼓励程序员编写简洁、清晰的代码。

Python 由全球开发者社区积极发展和维护。Python Software Foundation(PSF)是一个非营利组织,致力于促进 Python 语言发展,并维护其开放生态系统。

自诞生以来,Python 经历了显著变化。2008 年发布的 Python 3 是一次重大更新,引入了许多改进和与 Python 2 不兼容的变化。这导致了较长的过渡时期,在此期间两个版本并存;但最终,Python 2 的支持在 2020 年停止,社区也将主要精力集中到 Python 3 的发展上。

在现代 Python 开发中,集成开发环境(IDE)发挥着重要作用。IDE 为开发者提供编写、测试和调试代码所需的大量工具。在众多可用 IDE 中,Microsoft 开发的 Visual Studio Code(VS Code)因其灵活性、强大功能和可扩展性而占据重要位置。VS Code 为 Python 开发提供了广泛功能,包括 Git 集成、调试、依赖管理、代码自动补全等。

VS Code 通过扩展支持 Python,例如 Python Extension for Visual Studio Code。该扩展提供智能代码提示、调试、测试、代码格式化以及虚拟环境管理等功能。因此,无论是初学者还是专业人员,VS Code 都是 Python 开发的理想选择。

为了改善与代码的交互并提高在 VS Code 中的工作效率,建议安装一组基础扩展,首先是 Python Extension。它能简化许多开发任务,包括代码编写、测试和调试。VS Code 中的扩展可通过内置扩展商店安装,扩展商店可以直接从 IDE 中访问。

教材插图:Python 基础
教材插图:Python 基础 — 2.3 Python 简介

要启用 Python 扩展并开始在 VS Code 中使用 Python,请执行以下步骤:

1. 打开 VS Code,点击侧边栏中的扩展图标,或使用快捷键 Ctrl+Shift+X 进入扩展商店。 2. 在搜索框中输入 “Python”,并选择 Microsoft 提供的 Python 扩展。 3. 点击“安装”按钮,将扩展添加到开发环境中。

安装扩展后,VS Code 会自动识别系统中已安装的 Python 解释器,并建议在项目中使用它们。您还可以根据自己的偏好配置开发环境,包括选择解释器、设置可执行文件路径、配置调试器等。

2.4 Python 中的变量类型:基础与示例

Python 被认为是一种面向对象语言。Python 中的每个变量都是对象。Python 是“动态类型”的:变量的类型在声明时确定。您不需要事先声明变量,也不需要预先指定它们的类型。Python 中有几种基本变量类型:数字和字符串。这些变量既可以独立存在,也可以组成元组(Tuple)、列表(List)、字典(Dictionary)、集合(Set)、NumPy 数组等。代码中可以对所有这些类型执行各种操作,例如算术运算、逻辑运算、格式化等。需要注意的是,Python 中变量的定义相当自由:变量可以是元组、列表、字典等。例如,列表可以嵌套在列表中,元组可以放在列表中,列表也可以放在元组中。

Python 支持三种数值类型:整数、浮点数和复数。

要定义一个整数变量,只需给它赋一个整数值即可。

Python
my_int = 48  # 通过这个赋值,my_int 成为整数
print(my_int)
printx('my_int')
Text
48
my_int = 48
Python
type(my_int)  # 检查变量类型。如果这是单元格中的最后一行,不需要 print()
Text
int
Python
my_int = 5.0  # 现在 my_int 变成浮点数

type(my_int)
Text
float
Python
my_complex = 8.0 + 5.0j  # 这样 my_complex 就是复数
print(my_complex)
printx('my_complex')
Text
(8+5j)
my_complex = (8+5j)
Python
my_int, my_float, my_string = 20, 10.0, "Hello!"

if my_string == "Hello!":  # 比较运算符:==, !=, <, <=, >, >=
    print("字符串:%s" % my_string)  # 缩进 4 个空格

if isinstance(my_float, float) and my_float == 10.0:  # isinstance(): 判断对象是否为某个类的实例
    print("这是一个浮点数,它等于 %f" % my_float)

if isinstance(my_int, int) and my_int == 20:
    print("这是一个整数,它等于:%d" % my_int)
Text
字符串:Hello!
这是一个浮点数,它等于 10.000000
这是一个整数,它等于:20

变量类型可以进行转换:

Python
my_float = 5.0  # 给 my_float 赋值 5.0,使其成为 float 类型
print(my_float)  # 输出变量 my_float 的值
my_float = float(6)  # 使用 float() 将整数转换为浮点数
print(my_float)  # 输出变量 my_float 的值
print(int(7.0))  # 将 float 转换为 int 并输出
print('int(7.0)')  # 输出字符串 'int(7.0)'
Text
5.0
6.0
7
int(7.0)

要检查变量在内存中的地址,可以使用:

Python
a = 1.0
print('a =', a, '内存地址:', id(a))

b = a
print('b =', b, '内存地址:', id(b))
Text
a = 1.0 内存地址:140703208811472
b = 1.0 内存地址:140703208811472

请注意,b 与 a 的地址相同。

Python
a, b = 2.0, 3.0

print('a =', a, '内存地址:', id(a))
print('b =', b, '内存地址:', id(b))
Text
a = 2.0 内存地址:4365829648
b = 3.0 内存地址:4365829264

请注意,当变量值改变时,地址也发生了变化。

下划线 _ 在 Python 中具有若干特殊作用。下面来看其中一些例子。

Python
n1 = 100000000000
n_1 = 100_000_000_000  # 为了便于阅读

# 三元条件表达式
print('是的,n1 与 n_1 相同') if n1 == n_1 else print('不是')

n2 = 1_000_000_000
print('总计=', n1 + n2)

# f 字符串(Python 3.6 或更高版本)
print('总计=', f'{n1 + n2:,}')

# 下一行若写成 total 会出错,因为变量 total 未定义
# 为了修正,将 total 改为 n1 + n2
print('总计=', f'{n1 + n2:_}')
Text
是的,n1 与 n_1 相同
总计= 101000000000
总计= 101,000,000,000
总计= 101_000_000_000

2.4.1 字符串

字符串是文本片段,可用于创建标签以及程序运行结果文件的名称。字符串可以定义为任何用引号括起来的字符序列。可以使用一对单引号,也可以使用一对双引号。

Python
my_string = "Как дела? Как твое настроение?"
# 字符串已定义,可以对其中的字符进行索引
print(my_string, my_string[14], my_string[15], my_string[16], my_string[17], my_string[18:])

my_string = 'Привет,' + " привет!" + " Я здесь."
# 注意:字符串中的 “+” 运算符执行拼接
print(my_string)
Text
Как дела? Как твое настроение? т в о е  настроение?
Привет, привет! Я здесь.

虽然既可以使用单引号,也可以使用双引号,但如果字符串中包含撇号,就应使用双引号;否则,撇号会在使用单引号时提前结束字符串。反之亦然。例如:

Python
my_string = "Не волнуйтесь, просто используйте двойные кавычки для 'экранирования'."
print(my_string)
Text
Не волнуйтесь, просто используйте двойные кавычки для 'экранирования'.

也可以交换使用这两种引号:

Python
my_string = 'Не волнуйтесь о "двойных кавычках".'
print(my_string)
Text
Не волнуйтесь о "двойных кавычках".

当需要在字符串中包含反斜杠、Unicode 字符等内容时,请查阅 Python 文档。下面列出一些可用于数字和字符串的便捷、简洁的操作。可以亲自尝试,以获得经验。

Python
one, two, three = 1, 2, 3  # 给变量赋值

summation = one + two + three
print('和=', summation)  # 输出“和”
Text
和= 6
Python
one, two, three = 1, 2, 3.0  # 变量类型可以不同!

summation = one + two + three
print('求和=', summation)  # 输出“求和”
Text
求和= 6.0
Python
one3 = two3 = three3 = 3  # 给多个变量赋同一个值
print(one3, two3, three3)
Text
3 3 3

更多便捷操作:

Python
hello, world = "Hello,", "world!"

# 拼接字符串
helloworld = hello + " " + world + "!!"
print(helloworld, ' ', hello + " " + world)

# 获取字符串长度,包括空格和标点符号
lhw = len(helloworld)
print('字符串 "helloworld" 的长度为', lhw)
Text
Hello, world!!!   Hello, world!
字符串 "helloworld" 的长度为 17

可以把一个字符串拆分成由若干字符串组成的列表,其中每个字符串都是一个词。

Python
hello, world = "Hello,", "world!"

helloworld = hello + " " + world + "!!"
the_words = helloworld.split(" ")  # 创建字符串列表
# 后面还会介绍与列表相关的类似操作
print("将字符串拆分成单词:%s" % the_words)
print('使用空格作为分隔符重新连接:', ' '.join(the_words))
Text
将字符串拆分成单词:['Hello,', 'world!!!']
使用空格作为分隔符重新连接: Hello, world!!!

要在字符串中查找字母(字符),可以尝试如下操作:

Python
my_string = "Helloworld!"
print('"o" 位于第', my_string.index("o"), '个字符之后。')
print('第一个字母 "l" 位于第', my_string.index("l"), '个字符之后。')
Text
"o" 位于第 4 个字符之后。
第一个字母 "l" 位于第 2 个字符之后。

在下面的例子中,我们使用 + 运算符拼接字符串,以避免数字和后缀之间出现多余空格:

Python
my_string = "Helloworld!"
# “+” 执行拼接
print('字母 "o" 的位置在第 ' + str(my_string.index("o")) + ' 个字符之后。')
print('第 1 个字母 "l" 位于第 ' + str(my_string.index("l")) + ' 个字符之后。')
Text
字母 "o" 的位置在第 4 个字符之后。
第 1 个字母 "l" 位于第 2 个字符之后。

有时需要找出列表中每个元素出现的频率。

Python
from collections import Counter  # 导入 Counter 模块

my_list = ['а', 'а', 'б', 'б', 'б', 'в', 'г', 'г', 'г', 'г', 'г']
count = Counter(my_list)  # Counter 对象是一种字典

print(count)  # 输出列表中每个元素的出现频率
# 后面还会介绍字典
Text
Counter({'г': 5, 'б': 3, 'а': 2, 'в': 1})

在这个例子中,字母 а 在列表中出现 2 次,字母 б 出现 3 次,字母 в 出现 1 次,字母 г 出现 5 次。

Python
print('“б” 的出现频率为', count['б'])
# 通过键索引元素频率
Text
“б” 的出现频率为 3

因此,字母 б 在给定列表中出现 3 次。

请注意,Python(以及许多其他编程语言)从 0 开始计数,而不是从 1 开始。

下面列出一些可能有用的其他操作。

字符串中字母大小写之间的转换:

Python
my_string = "Привет мир!"  # 定义字符串
print(my_string.upper(), my_string.lower(), my_string.title())  # 分别转换为大写、小写和标题形式
Text
ПРИВЕТ МИР! привет мир! Привет Мир!

通过切片返回反向字符串:

Python
my_string = "АБВГДЕЖ"
reversed_string = my_string[::-1]
print(reversed_string)
Text
ЖЕДГВБА

字符串 АБВГДЕЖ 被反转,结果为字符串 ЖЕДГВБА。

字符串类的 title() 函数:

Python
my_string = "Россия - страна возможностей"
new_string = my_string.title()
print(new_string)
Text
Россия - Страна Возможностей

对字符串 Россия - страна возможностей 应用 title() 方法后,每个单词的首字母变为大写,其余字母变为小写。

使用重复操作:

Python
n = 8
my_list = [1] * n
print(my_list)
Text
[1, 1, 1, 1, 1, 1, 1, 1]

列表 my_list 包含 8 个元素,每个元素都是数字 1。

Python
# 声明变量 my_string,并赋值为 "абвгдеж"
my_string = "абвгдеж"

# 输出重复两次的字符串 my_string
print(my_string * 2)
Text
абвгдежабвгдеж

Python 中的 * 运算符也可以用于字符串重复。在本例中,my_string * 2 表示字符串 my_string 将重复两次。

因此,这段代码会输出两次字符串 абвгдеж。

Python
# 声明变量 lotsofhellos,并将其赋值为重复 5 次的 "Привет "
lotsofhellos = "Привет " * 5

# 输出变量 lotsofhellos 的值
print(lotsofhellos)
Text
Привет Привет Привет Привет Привет 

这个程序的运行方式如下。第一行声明变量 lotsofhellos,并将其赋值为重复 5 次的字符串 Привет 。Python 中的乘法运算符 * 可用于字符串拼接;在这种情况下,它会重复字符串 Привет 五次。

使用 len() 函数获得给定参数的长度:

Python
# 字符串 "АБВГД" 的长度
print("字符串 «АБВГД» 的长度为", len("АБВГД"))
Text
字符串 «АБВГД» 的长度为 5

在这段代码中,我们使用 len() 函数获取字符串长度。len() 函数以字符串作为参数,并返回一个整数,该整数等于字符串中的字符数。

Python
# 输出字符串长度的程序
# 初始化变量 my_string
my_string = "Hello, world!"
# 输出 my_string 的长度
print("字符串", my_string, "的长度为", len(my_string))
Text
字符串 "Hello, world!" 的长度为 13
Python
# 声明两个包含列表的变量
even_numbers, odd_numbers = [2, 4, 6, 8], [1, 3, 5, 7]

# 使用 len() 函数获取 even_numbers 列表的长度
length = len(even_numbers)
# 使用 + 运算符合并两个列表
all_numbers = odd_numbers + even_numbers
# 输出合并后的列表,以及原始长度和新长度
print(all_numbers, ' 原始长度为', length, '。新长度为', len(all_numbers))
Text
[1, 3, 5, 7, 2, 4, 6, 8] 原始长度为 4。新长度为 8

使用 sorted() 函数对元素排序:

Python
# 创建列表
all_numbers = [5, 3, 9, 1, 4]
# 输出按字母顺序排序后的俄文字母 'ДГВБА',以及 'АБВГД' 的逆序排序
print(sorted('ДГВБА'), sorted('АБВГД', reverse=True))
# 输出 all_numbers 列表的升序和降序排序结果
print(sorted(all_numbers), sorted(all_numbers, reverse=True))
Text
['А', 'Б', 'В', 'Г', 'Д'] ['Д', 'Г', 'В', 'Б', 'А']
[1, 3, 4, 5, 9] [9, 5, 4, 3, 1]

将列表中的每个元素乘以同一个数字:

Python
# 原始列表和变量
original_list, n = [1, 2, 3, 4], 2

# 使用列表推导式创建新列表,
# 其中原始列表的每个元素都乘以 n
new_list = [n * x for x in original_list]
# 输出新列表
print(new_list)
Text
[2, 4, 6, 8]

使用 enumerate() 函数为列表创建索引:

Python
my_list = ['a', 'b', 'c']  # 创建包含元素 'a'、'b'、'c' 的列表
# 遍历列表,同时获得每个元素的索引和值
for index, value in enumerate(my_list):
    # 输出索引(加 1 后)和每个列表元素的值
    print('{0}: {1}'.format(index + 1, value))
Text
1: a
2: b
3: c
Python
my_list = ['a', 'b', 'c']  # 创建包含元素 'a'、'b'、'c' 的列表

for index, value in enumerate(my_list):  # 生成索引和值
    print(f'{index + 1}: {value}')  # 使用 f 字符串输出索引(加 1 后)和值
Text
1: a
2: b
3: c

try-except-finally 错误处理结构可以监控代码并避免程序执行中断:

Python
a, b = 1, 2  # 给变量 a 和 b 赋值

try:
    print(a / b)  # 尝试执行除法
except ZeroDivisionError:
    print("除以零")  # 处理除零错误
else:
    # 如果 try 块中没有异常,则执行 else 块
    print("没有发生异常")
finally:
    # finally 块无论如何都会执行
    print("无论发生什么,这一段总会执行")
Text
0.5
没有发生异常
无论发生什么,这一段总会执行

获取以字节为单位的内存大小:

Python
import sys  # 导入 sys 模块

num = "AAA"
print('内存大小为 %d' % sys.getsizeof(num), '字节')
# 字符串的内存大小
Text
内存大小为 52 字节

这个结果很有意思,因为它表明 Python 中字符串占用的内存不仅取决于字符串中的字符数,还包括额外的元数据,例如字符串长度和数据类型。一个由三个字符组成的字符串占用的内存远比直觉上想象的更多。在 Python 中处理大量文本数据时,这一点非常重要。

Python
num = 12345
print('内存大小为 %d' % sys.getsizeof(num), '字节')
# 整数的内存大小
Text
内存大小为 28 字节

这个结果表明,在 Python 中,整数占用的内存也不仅仅是存储数值本身所需的字节数。额外字节还会用于存储变量相关的元数据,例如其类型。

检查字符串是否以某个内容开头,或以某个内容结尾:

Python
# 定义俄语字符串
astring = "Привет мир!"

# 检查字符串是否以 "Привет" 开头,以及是否以 "абвгд" 或 "!" 结尾
starts_with_privet = astring.startswith("Привет")
ends_with_abvgd = astring.endswith("абвгд")
ends_with_exclamation = astring.endswith("!")

print(starts_with_privet, ends_with_abvgd, ends_with_exclamation)
Text
True False True

我们检查了包含俄语文本 Привет мир! 的字符串 astring 是否以子字符串 Привет 开头,以及是否以子字符串 абвгд 或 ! 结尾。

代码执行结果如下:

字符串 astring 以子字符串 Привет 开头:True。

字符串 astring 以子字符串 абвгд 结尾:False。

字符串 astring 以字符 ! 结尾:True。

更多类似函数:

Python
# 检查所有字符是否都由数字组成
# 检查所有字符是否都由字母组成
# 检查字符串是否全部为大写

my_string = "Приветмир!"
my_string1 = "ПриветМир"
my_string2 = "ПРИВЕТ МИР!"

print(my_string.isalnum())  # 是否所有字符都是字母或数字
print(my_string1.isalpha())  # 是否所有字符都是字母
print(my_string2.isupper())  # 是否全部由大写字母组成
Text
False
True
True

检查变量类型以及其他属性:

Python
n, x, s = 1234, 1234.0, 'это строка'

print(type(n), type(x), type(s))  # 检查对象类型
print(len(s), len(str(n)), len(str(x)))  # 确定字符串和数字字符串表示的长度
Text
<class 'int'> <class 'float'> <class 'str'>
10 4 6

代码执行结果如下。

变量类型:n 的类型是 int,表示整数;x 的类型是 float,表示浮点数(实数);s 的类型是 str,表示字符串。

字符串长度:字符串 s(это строка)长度为 10 个字符;数字 n 的字符串表示(1234)长度为 4 个字符;数字 x 的字符串表示(1234.0)长度为 6 个字符,因为它包含数字、小数点和小数点后的零。

这些结果正确反映了给定变量的数据类型和字符串长度。

2.4.2 Python 中的数据类型转换

当某个运算中的一个变量是浮点数,而其他变量是整数时,运算结果会变成浮点数。

Python
a = 2  # 将变量 a 初始化为 2
print('a=', a, ' type of a:', type(a))  # 输出变量 a 的值和类型
b = 3.0  # 将变量 b 初始化为 3.0
a = a + b  # 对变量 a 和 b 求和,结果保存到 a 中
# 输出变量 a 和 b 的值与类型
print('a=', a, ' type of a:', type(a))
print('b=', b, ' type of b:', type(b))
Text
a= 2  type of a: <class 'int'>
a= 5.0  type of a: <class 'float'>
b= 3.0  type of b: <class 'float'>

变量类型可以转换为其他类型。

Python
n, x, s = 1234, 1234.5, 'это строка'  # 初始化变量 n、x 和 s

sfn = str(n)  # 将整数 n 转换为字符串
print(sfn, type(sfn))  # 输出变量 sfn 的值和类型
sfx = str(x)  # 将浮点数 x 转换为字符串
print(sfx, type(sfx))  # 输出变量 sfx 的值和类型
Text
1234 <class 'str'>
1234.5 <class 'str'>
Python
n, x, s = 1234, 1234.5, 'это строка'  # 初始化变量 n、x 和 s

xfn = float(n)  # 将整数 n 转换为浮点数
print(xfn, type(xfn))  # 输出变量 xfn 的值和类型
nfx = int(x)  # 将浮点数 x 转换为整数
print(nfx, type(nfx))  # 输出变量 nfx 的值和类型
Text
1234.0 <class 'float'>
1234 <class 'int'>
Python
# a = int('Привет')  # 将字符串 'Привет' 转换为整数会引发错误 ValueError
# a = int('2.5')     # 将字符串 '2.5' 转换为整数也会引发错误 ValueError

a = int('25')  # 将字符串 '25' 转换为整数,可以正确工作
print(a, type(a))  # 输出转换后变量 a 的值和类型
Text
25 <class 'int'>

代码中的注释还说明,尝试把字符串 Привет 或 2.5 转换为整数会导致 ValueError,因为这些字符串不能被正确解释为整数。

Python
# 执行加法运算
result = 8.0 + float("8.0")  # 将字符串 "8.0" 转换为浮点数后,与浮点数 8.0 相加
print(result)  # 输出运算结果
Text
16.0

这里执行了数字 8.0 与字符串 8.0 转换成的浮点数之间的加法运算。运算结果为 16.0。

Python
a = int(False)  # 将布尔值 False 转换为整数
print(a, type(a))  # 输出转换后变量 a 的值和类型
Text
0 <class 'int'>

然而,数字与字符串混合使用的运算是不允许的,这会引发 TypeError:

Python
# 定义浮点数和字符串
my_float = 3.14
my_string = "Этострока-пример"

# 尝试将它们相加
try:
    my_mix = my_float + my_string  # 尝试把浮点数和字符串相加
except TypeError as e:
    print(f"错误:{e}")  # 输出错误
Text
错误:unsupported operand type(s) for +: 'float' and 'str'

2.4.3 Python 数据格式化的技巧

在输出变量时,格式化非常有用。Python 使用类似 C 语言风格的字符串格式化来创建新的格式化字符串。运算符 % 用于格式化一组包含在“元组”中的变量,而元组是固定大小的列表(后面会讨论)。格式化结果会出现在某个“参数说明符”指定的位置,例如 %s、%d、%f 等。理解这一点的最好方式是通过例子:

Python
name = "Иван"
print("Привет, %s! Как ты?" % name)  # %s 用于字符串
# 对于两个或更多参数,请使用元组:

name, age = "Алексей", 23
print("%s %d лет." % (name, age))  # %d 用于数字
Text
Привет, Иван! Как ты?
Алексей 23 лет.

下面的例子演示了 Python 中 f 字符串的使用,它从 Python 3.6 开始可用。f 字符串允许直接在字符串中插入变量值,使代码更加可读和方便。

Python
# 定义变量
name = "Иван"  # 姓名
age = 23  # 年龄
# 输出问候语和年龄信息
# 使用 f 字符串把变量直接插入字符串
print(f"{name} {age} лет.")  # f 字符串可用于 Python 3.6 及更高版本
Text
Иван 23 лет.

任何不是字符串的对象(例如列表)也可以使用 %s 运算符进行格式化。%s 运算符使用 repr 方法把对象格式化为字符串并返回它。例如:

Python
list1, list2, x = [1, 2, 3], ['Аня', 'Маша'], 12.5  # 多重赋值

print("Список1:%s; Список2:%s\nx=%s, x=%f, x=%.3f, x=%e" % (list1, list2, x, x, x, x))
Text
Список1:[1, 2, 3]; Список2:['Аня', 'Маша']
x=12.5, x=12.500000, x=12.500, x=1.250000e+01

第二行输出显示了变量 x 的不同格式:

Text
x=%s   - 字符串格式。
x=%f   - 浮点格式。
x=%.3f - 保留三位小数的浮点格式。
x=%e   - 科学计数法格式。
Python
list1, list2, x = [1, 2, 3], ['Аня', 'Маша'], 12.5  # 多重赋值

print(f"List1:{list1};List2:{list2};x={x},x={x:.2f}, x={x:.3e}")  # 使用 f 字符串格式化输出变量 list1、list2 和 x
Text
List1:[1, 2, 3];List2:['Аня', 'Маша'];x=12.5,x=12.50, x=1.250e+01

常用的格式化参数说明符(在不使用 f 字符串时):

Python 有一种特殊方式可以把变量值插入字符串,使字符串更加动态、信息量更高。这种方式称为字符串格式化。

格式化符号如下:

教材插图:Python 基础
教材插图:Python 基础 — 2.4.3 Python 数据格式化的技巧

%s —— 字符串,或可以转换为字符串的对象(例如数字)。

%d —— 整数。

%f —— 浮点数。

%.f —— 按指定小数位数显示的浮点数。例如,%.2f 会输出保留两位小数的数字。

%e —— 科学计数法:以指定的 10 的幂表示浮点数。

%x/%X —— 十六进制整数(小写/大写字母)。

使用示例:

Python
имя = "Иван"
возраст = 30
рост = 1.82

print(f"Привет, {имя}! Твой возраст - {возраст} лет, а рост - {рост:.2f} метра.")
Text
Привет, Иван! Твой возраст - 30 лет, а рост - 1.82 метра.

在这个例子中:

字符串前面的 f 表示使用字符串格式化。

变量 имя 和 возраст 的值被插入字符串中。

{рост:.2f} 用于把变量 рост 格式化为保留两位小数的值。

附加功能包括:可以指定输出字段宽度,例如 %10d 会在宽度为 10 个字符的字段中输出整数;可以指定空位填充符号,例如 %05d 会用前导零填充到 5 个字符宽;也可以指定对齐方式,例如左对齐输出整数。

2.5 算术运算符

加、减、乘、除运算符可以用于数字。

2.4.1 加法、减法、乘法、除法和幂运算

+ —— 加法

- —— 减法

* —— 乘法

/ —— 除法

// —— 整除(舍去小数部分)

% —— 整除后的余数(模运算)

** —— 幂运算

下面是这些运算符的一些使用示例:

Python
# 加法
print(1 + 2)  # 3
# 减法
print(5 - 3)  # 2
# 乘法
print(2 * 3)  # 6
# 除法
print(10 / 2)  # 5
# 整除
print(10 // 2)  # 5
# 整除余数
print(10 % 2)  # 0
# 幂运算
print(2 ** 3)  # 8
Text
3
2
6
5.0
5
0
8
Python
# 声明变量 number,并将其赋值为 1 + 2 * 3 / 4.0
number = 1 + 2 * 3 / 4.0
# 输出变量 number 的值
print(number)
Text
2.5

在 Python 中,模运算符 % 返回整数除法的余数:被除数 % 除数 = 余数。

Python
# 声明两个变量:
# numerator - 分子
# denominator - 分母
numerator, denominator = 11, 2

# 计算整除商
floor = numerator // denominator
# 输出结果
print(str(numerator) + '//' + str(denominator) + '=', floor)

# 计算除法余数
remainder = numerator % denominator
# 输出结果
print(str(numerator) + '%' + str(denominator) + '=', remainder)

# 计算“商 × 除数 + 余数”
print(floor * denominator + remainder)
Text
11//2= 5
11%2= 1
11

在 Python 中,两个乘号表示幂运算。

Python
# 声明两个变量:
# squared - 数字 7 的平方
# cubed - 数字 2 的立方
squared, cubed = 7 ** 2, 2 ** 3

# 在控制台输出变量值
print('7 ** 2 =', squared, ', and 2 ** 3 =', cubed)
Text
7 ** 2 = 49 , and 2 ** 3 = 8
Python
# 数字 7 和 2 之间的按位异或(XOR)
test72 = 7 ^ 2
# 在控制台输出结果
print(test72)
Text
5

运算符 ^ 对操作数执行按位 XOR 运算。它比较两个数的对应二进制位:如果两位不同,则返回 1;如果相同,则返回 0。

XOR 运算的结果(本例中为 5)被赋给变量 test72。

请注意,Python 中的 ^ 运算符用于按位 XOR,而不是幂运算。

Python 允许在两个变量之间进行简单的交换操作。

Python
# 声明变量 a 和 b,并分别赋值为 100 和 200
a, b = 100, 200

# 输出变量 a 和 b 的值
print('a=', a, 'b=', b)

# 使用赋值运算符交换变量 a 和 b 的值
a, b = b, a

# 输出变量 a 和 b 的值
print('a=', a, 'b=', b)
Text
a= 100 b= 200
a= 200 b= 100

从输出可以看出,代码执行后变量 a 和 b 的值互换了。

Python 提供了一系列始终可用的内置函数和类型。要快速了解它们,可以参考相关表格,或访问 https://docs.python.org/3/library/functions.html 获取更详细信息。

2.6 布尔值和运算符

布尔值是两个常量对象:True 和 False。当它们作为算术运算符的参数使用时,分别表现为整数 1 和 0。内置函数 bool() 可用于把任意值转换为布尔值。

定义如下:

Python
# 对除 0 和空值之外的所有值输出 True
print(bool(5), bool(-5), bool(0.2), bool(-0.1), bool(str('a')), bool(str('0')))
# True True True True True True

# 对 0 和 -0.0 输出 False
print(bool(0), bool(-0.0))
# False False

# 对空值输出 False
print(bool(''), bool([]), bool({}), bool(()))
# False False False False

bool() 函数只有在值为零或容器为空时才返回 False。其他情况下返回 True。请注意,str('0') 既不是数值零,也不是空值。

布尔运算符包括“与”和“或”。

Python
# 第一行代码输出三个值:
# - True and True:两个真值的逻辑与,结果为 True
# - False or True:两个值的逻辑或,其中一个为真,结果为 True
# - True or True:两个真值的逻辑或,结果为 True
print(True and True, False or True, True or True)

# 第二行代码输出两个值:
# - False and False:两个假值的逻辑与,结果为 False
# - False and True:两个值的逻辑与,其中一个为假,结果为 False
print(False and False, False and True)
Text
True True True
False False

2.7 列表:可变类型的容器

前面我们已经多次接触到列表。本节将更详细地讨论它们。

列表是一组变量的集合,与数组非常相似。列表可以包含任意类型的变量,也可以包含任意数量的变量。这些变量存放在一对方括号 [] 中。

必要时,可以对列表进行迭代,以便执行相应操作。列表属于“可迭代”对象之一。

2.7.1 列表的创建、添加、连接和更新

Python
# 声明一个 list 类型变量 x_list
x_list = []

# 将变量 x_list 的内容输出到屏幕
print(f"x_list={x_list}")

# 以十六进制格式输出变量 x_list 的地址
print(hex(id(x_list)))
Text
x_list=[]
0x104983300
Python
x_list = []
x_list.append(1)    # 将数字 1 添加到 x_list 的末尾
x_list.append(2)    # 将数字 2 添加到 x_list 的末尾
x_list.append(3.)   # 将浮点数 3.0 添加到 x_list 的末尾

print(x_list[0])    # 输出列表的第一个元素(索引 0),即 1
print(x_list[1])    # 输出列表的第二个元素(索引 1),即 2
print(x_list[2])    # 输出列表的第三个元素(索引 2),即 3.0
print(x_list)       # 输出整个列表
Text
1
2
3.0
[1, 2, 3.0]
Python
for x in x_list:  # 使用 for 循环遍历列表 x_list 中的元素
    print(x, end=',')  # 不换行输出每个元素
print('\n')            # 循环结束后输出换行

x_list2 = x_list * 2   # 通过将 x_list 与自身连接来创建新列表 x_list2
print(x_list2)         # 输出新列表
Text
1,2,3.0,

[1, 2, 3.0, 1, 2, 3.0]
Python
print(id(x_list), id(x_list2))
Text
4367086336 4367678592

说明:

Python
print(id(x_list), id(x_list2))

这一行代码会输出两个列表 x_list 和 x_list2 的唯一标识符,也就是它们在内存中的地址。

不同的地址说明,x_list 和 x_list2 是内存中的两个不同对象,尽管它们包含相同的元素。这是因为操作 x_list2 = x_list * 2 创建了一个新列表,而不是修改原来的列表 x_list。

重要要点:

• 在 Python 中,每个变量都引用内存中的一个对象。

• 对象标识符,也就是对象在内存中的地址,可以通过 id() 函数获得。

• 当把一个列表赋值给另一个变量时,创建的是指向同一内存对象的新引用。

• 当对列表执行会创建新列表的操作时,例如列表乘法或列表连接,会在内存中创建具有不同标识符的新对象。

Python
# 返回列表 x_list 第二个元素的标识符(内存地址)
id(x_list[1])
Text
1594536160

id(x_list[1]) 这一行代码返回列表 x_list 第二个元素的标识符,也就是其内存地址。

Python
x_list3 = x_list  # 将列表 x_list 赋值给变量 x_list3
print(x_list, ' ', x_list3)  # 输出两个变量的值
Text
[1, 2, 3.0]  [1, 2, 3.0]

说明:

Python
x_list3 = x_list

这一行并不会创建新列表。相反,变量 x_list3 只是成为 x_list 所指向的同一列表的另一个名称。这意味着两个变量都引用内存中的同一个对象。

Python
print(x_list, ' ', x_list3)

这一行输出两个变量的值;由于它们引用同一个列表,所以输出结果相同。

重要要点:

• Python 中的列表赋值不会创建列表副本,而是创建指向同一内存列表的另一个引用。

• 因为两个变量引用同一个对象,所以对其中一个列表所做的任何更改都会反映到另一个变量中。

• 要创建独立的列表副本,需要使用复制方法,例如 list.copy(),或使用列表切片。

Python
print(id(x_list), id(x_list3))
Text
1847992120648 1847992120648

说明:

Python
print(id(x_list), id(x_list3))

这一行代码输出变量 x_list 和 x_list3 的唯一标识符,也就是内存地址。

相同的地址说明,这两个变量引用内存中的同一个对象。这是因为赋值 x_list3 = x_list 并没有创建新列表,而只是创建了指向同一个列表的另一个引用。

Python
x_list4 = x_list.copy()  # 使用 copy() 方法创建列表 x_list 的独立副本
print(x_list, ' ', x_list4)  # 输出两个列表的值
Text
[1, 2, 3.0]  [1, 2, 3.0]

说明:

Python
x_list4 = x_list.copy()

copy() 方法创建列表 x_list 的一个新的独立副本,并将其赋给变量 x_list4。这意味着 x_list4 与 x_list 包含相同的元素,但位于不同的内存区域。

Python
print(x_list, ' ', x_list4)
print(id(x_list), id(x_list4))
Text
1847992120648 1847993186760

不同的地址说明,x_list 和 x_list4 是内存中的不同对象,尽管它们包含相同的元素。这是因为 copy() 方法创建了列表 x_list 的新独立副本。

Python
# 将列表 x_list 的第一个元素赋值为 4.0
x_list[0] = 4.0

# 将列表输出到屏幕
print(x_list)
Text
[4.0, 2, 3.0]
Python
# 创建列表 x_list
x_list = [1, 2, 3.0]

# 创建列表 x_list2,它是列表 x_list 的副本
x_list2 = x_list.copy()

# 修改列表 x_list 第一个元素的值
x_list[0] = 4.0

# 将列表 x_list2 输出到屏幕
print(x_list2)
Text
[1, 2, 3.0]

在这个例子中可以看到,修改列表 x_list 中某个元素的值不会影响列表 x_list2 中对应元素的值。这是因为 x_list2 是列表 x_list 的独立副本。

通过展开数字字符串来创建列表:

Python
num = 19345678

# 方法 1:使用 map() 函数并转换为列表
list_of_digits = list(map(int, str(num)))
print(list_of_digits)  # 输出:[1, 9, 3, 4, 5, 6, 7, 8]

# 方法 2:使用列表推导式
list_of_digits = [int(x) for x in str(num)]
print(list_of_digits)  # 输出:[1, 9, 3, 4, 5, 6, 7, 8]

说明:

方法 1:使用 map() 并转换为列表。

map(int, str(num)) 会把 int() 函数应用到字符串 str(num) 中的每一个字符,从而把字符转换为整数。map() 返回一个迭代器,需要用 list() 将其转换为列表。

方法 2:使用列表推导式。

Python
[int(x) for x in str(num)]

它创建一个新列表,其中每个元素都是对字符串 str(num) 中对应字符应用 int() 后得到的结果。这是一种更紧凑、通常也更易读的基于已有迭代对象创建列表的方法。

两种方法得到相同结果:

list_of_digits 是一个列表,其中包含数字 num 的各个数字,并把它们作为独立整数保存:[1, 9, 3, 4, 5, 6, 7, 8]。

选择哪种方法取决于编码风格和可读性偏好:

• map() 对简单转换可能更简洁。

• 列表推导式对于复杂转换和筛选更有表现力,也更灵活。

列表的逐元素相加需要一点技巧。包括使用 NumPy 数组在内的更好方法,将在列表理解相关部分中讨论。

Python
# 声明两个包含列表的变量
list1, list2 = [20, 30, 40], [5, 6, 8]

# 将原始列表输出到屏幕
print(list1, " ", list2, " ", list1 + list2)

# 以字符串形式输出原始列表
print("原始字符串 1:" + str(list1))
print("原始字符串 2:" + str(list2))

# 输出提示:"+" 不是加法,而是连接
print('"+" 不是加法,而是连接:', list1 + list2)
Text
[20, 30, 40]   [5, 6, 8]   [20, 30, 40, 5, 6, 8]
原始字符串 1:[20, 30, 40]
原始字符串 2:[5, 6, 8]
"+" 不是加法,而是连接:[20, 30, 40, 5, 6, 8]
Python
# 声明两个列表
list1 = [1, 2, 3, 4, 5]
list2 = [6, 7, 8, 9, 10]

# 声明一个空列表,用于保存元素之和
add_list = []

# 使用 for 循环对两个列表进行逐元素相加
for i in range(0, len(list1)):
    add_list.append(list1[i] + list2[i])

# 将结果输出到屏幕
print("两个列表逐元素相加:" + str(add_list))
Text
两个列表逐元素相加:[7, 9, 11, 13, 15]
Python
# 输出列表 add_list 第一个元素的地址
print("列表第一个元素的地址:", id(add_list[0]))

# 输出列表 list1 第一个元素的地址
print("列表第一个元素的地址:", id(list1[0]))
Text
列表第一个元素的地址: 4363141552
列表第一个元素的地址: 4377526512
Python
# 声明两个列表
list1 = [1, 2, 3, 4, 5]
list2 = [6, 7, 8, 9, 10]

# 声明空列表,用于保存结果
add_list = []

# for 循环同时遍历两个列表中的元素
for i1, i2 in zip(list1, list2):
    # 将元素 i1 与元素 i2 相加
    add_list.append(i1 + i2)

# 将结果输出到屏幕
print("两个列表逐元素求和:", add_list)
Text
两个列表逐元素求和: [7, 9, 11, 13, 15]

代码说明:

Python
add_list = []

声明一个空列表,用于保存结果。

Python
for i1, i2 in zip(list1, list2):

这是一个 for 循环,它同时遍历两个列表中的元素。

zip() 是一个内置函数,它返回一个迭代器;该迭代器包含由两个列表中相同索引处的元素组成的元组。

i1 是 zip() 返回的元组中的第一个元素。

i2 是 zip() 返回的元组中的第二个元素。

Python
add_list.append(i1 + i2)

将元素 i1 与元素 i2 相加,并把结果加入 add_list。

Python
print("两个列表逐元素求和:", add_list)

将结果输出到屏幕。

2.7.2 字符串和列表的切片

切片是一种有用且高效的操作,可用于处理字符串、列表或数组的一部分;关于数组,我们将在后文讨论。先从字符串切片开始,然后再转向列表。

字符串切片就是根据索引或索引范围取得字符串的一部分。

Python 中的索引从零开始,因此字符串中的第一个字符索引为 0,第二个字符索引为 1,依此类推。

冒号 : 用于在切片中表示索引范围。

负索引用于从字符串末尾开始计数。

步长参数允许指定切片中被选择字符之间的间隔。

当试图访问超出字符串或列表长度范围的索引时,会产生 IndexError 异常。

Python
my_string = "每座小屋都有自己的小响铃"
# 输出原始字符串
print(my_string)
# 输出第五个字符
print('第 5 个=', my_string[4])
# 输出第七到第十一个字符
print('从第 7 个到第 11 个 =', my_string[6:11])
Text
每座小屋都有自己的小响铃
第 5 个= 屋
从第 7 个到第 11 个 = 有自己的小
Python
# 创建字符串变量
my_string = "你好,世界!"  # 可以把该字符串改成任意其他字符串

# 输出带说明的字符串切片:
print('[6:-1]=', my_string[6:-1])  # 从第 7 个元素(含)到最后一个元素之前
print('[:]=', my_string[:])        # 字符串中的全部字符
print('[6:]=', my_string[6:])      # 从第 7 个元素(含)到字符串末尾
print('[:-1]=', my_string[:-1])    # 除最后一个字符之外的所有字符
Text
[6:-1]= 
[:]= 你好,世界!
[6:]= 
[:-1]= 你好,世界
Python
# 声明变量 my_string,并为它赋值 "Helloworld!"
my_string = "Helloworld!"
# 在屏幕上输出文本“第 4 到第 9 个字符,步长为 2”
print("第 4 到第 9 个字符,步长为 2")
# 使用切片从 my_string 中取得子串:从第 4 个字符开始,到第 9 个字符结束,步长为 2
print("[3:9:2]=", my_string[3:9:2])

# 字符串 "Helloworld!" 由 11 个字符组成:
#   H - 第 1 个字符
#   e - 第 2 个字符
#   l - 第 3 个字符
#   l - 第 4 个字符
#   o - 第 5 个字符
#   w - 第 6 个字符
#   o - 第 7 个字符
#   r - 第 8 个字符
#   l - 第 9 个字符
#   d - 第 10 个字符
#   ! - 第 11 个字符
# 切片语法:[start:stop:step]
# start - 子串中第一个字符的索引
# stop  - 子串结束位置的索引(不包含该位置)
# step  - 子串中字符序列的步长
Text
第 4 到第 9 个字符,步长为 2
[3:9:2]= lwr

使用负步长可以很容易地反转字符串,正如前面所看到的那样:

Python
my_string = "暖暖地开始了!(引)"
# 输出原始字符串
print("string:", my_string)
# 逆序输出字符串
print("[::-1]=", my_string[::-1])
Text
string: 暖暖地开始了!(引)
[::-1]= )引(!了始开地暖暖

简言之,如果方括号中只给出一个数字,则切片取位于“该数字对应索引”的字符。由于 Python 从零开始计数,索引 4 表示第 5 个字符。冒号表示“所有可用元素”。如果冒号单独使用,切片会取得整个字符串。如果冒号左侧有数字,则切片从该索引开始直到字符串末尾;反之则从开头取到冒号右侧指定位置之前。负数表示从字符串末尾计数:-3 表示“倒数第三个字符”。也可以使用步长参数来跳过字符。

请注意,当访问字符串中不存在的索引时,会生成 IndexError 异常。

Python
# 声明变量 my_string 并赋空字符串,
# 用于演示错误
my_string = ""

# 尝试输出索引为 14 的元素,该索引超出了字符串范围
print('[14]=', my_string[14])  # 将产生 IndexError 错误

下面是列表切片示例:

Python
# 创建列表 my_list,包含混合类型的变量:
my_list = [0, 1, 2, 3, "4E", 5, 6, 7, [8, 8], 9]

# 使用 print() 输出列表 my_list
# 范围从 0 到 9,步长为 1
print(my_list[0:10:1])

# 使用 print() 输出列表 my_list
# 范围从列表开头到列表末尾
print(my_list[:])
Text
[0, 1, 2, 3, '4E', 5, 6, 7, [8, 8], 9]
[0, 1, 2, 3, '4E', 5, 6, 7, [8, 8], 9]

2.7.3 在列表中使用占位符 _

Python
# 创建包含混合值的列表
nlist = [10, 20, 30, 40, 50, 6.0, '7H']

# 解包列表元素,忽略前两个元素和第四个元素
_, _, n3, _, *nn = nlist

# 输出第三个元素和第五个元素之后的其余元素
print("n3=", n3, "其余数字", *nn)

# 创建另一个包含数值的列表
nlist = [10, 20, 30, 40, 50, 60, 70]

# 解包元素:保留第三个元素、最后一个元素,并将中间其余元素放入单独列表 nn
_, _, n3, *nn, nlast = nlist

# 输出第三个元素、最后一个元素以及其余元素
print("n3=", n3, ", 最后一个=", nlast, ", 以及所有其他数字", *nn)
Text
n3= 30 其余数字 50 6.0 7H
n3= 30 , 最后一个= 70 , 以及所有其他数字 40 50 60

2.7.4 嵌套列表(列表中的列表)

Python
# 创建由混合类型变量组成的嵌套列表
nested_list = [[11, 12], ['2B', 22], [31, [32, 3.2]]]

# 将嵌套列表输出到屏幕
print('嵌套列表:')
print(nested_list)

# 输出嵌套列表的数量
print('嵌套列表的数量:', len(nested_list))
Text
嵌套列表:
[[11, 12], ['2B', 22], [31, [32, 3.2]]]
嵌套列表的数量: 3
Python
# 输出第一个嵌套列表
# nested_list[0] 是列表 nested_list 的第一个元素
# 输出第二个嵌套列表
# nested_list[1] 是列表 nested_list 的第二个元素
# 输出第三个嵌套列表
# nested_list[2] 是列表 nested_list 的第三个元素
# 输出整个嵌套列表
# print(nested_list) 是把数据输出到屏幕的函数
print(nested_list[0])
print(nested_list[1])
print(nested_list[2])
print(nested_list)
Text
[11, 12]
['2B', 22]
[31, [32, 3.2]]
[[11, 12], ['2B', 22], [31, [32, 3.2]]]
Python
print(nested_list[0][0])  # 输出第一个嵌套列表(索引 0)中的第一个元素(索引 0),即数字 11
print(nested_list[0][1])  # 输出第一个嵌套列表(索引 0)中的第二个元素(索引 1),即数字 12
Text
11
12
Python
print(nested_list)        # 输出整个嵌套列表
print(nested_list[1][0])  # 输出第二个嵌套列表(索引 1)中的第一个元素(索引 0),即字符串 '2B'
print(nested_list[2][1])  # 输出第三个嵌套列表(索引 2)中的第二个元素(索引 1);该元素本身也是列表:[32, 3.2]
print(nested_list[2][1][0])  # 输出第三个嵌套列表第二个元素中的第一个元素,即数字 32
Text
[[11, 12], ['2B', 22], [31, [32, 3.2]]]
2B
[32, 3.2]
32

2.8 元组

在讨论列表之后,理解元组就变得很简单。这是因为它们本质上非常相似,主要区别如下:

元组通常用圆括号 () 括起来,而列表用方括号 [] 括起来。

元组是不可变的,而列表是可变的。这意味着元组创建之后不能再修改。元组中的值会被保留下来。

2.8.1 保存值

由于元组不可变,我们用它来存储需要保持不变的数据。因此,元组的用途相对有限。它常用于存储常量,以防止它们被修改。此外,元组的运行速度也更快。

除了这些差异之外,元组的行为与列表相似。可以通过索引访问它,可以对它进行迭代,也可以将它赋值给其他变量。下面给出几个示例。

Python
# 创建元组
ttuple = (10, 20, 30, 40, 50, 6.0, '7H')
# 输出元组
print('ttuple =', ttuple)
# 将元组中的第一个值赋给变量 aa
aa = ttuple[0]
# 输出变量 aa 的值
print('aa =', aa)
# 输出元组中索引为 1、6 和 -1 的元素值
print(ttuple[1], ' ', ttuple[6], ' ', ttuple[-1])
Text
ttuple = (10, 20, 30, 40, 50, 6.0, '7H')
aa = 10
20   7H   7H
Python
# 声明元组
ttuple = ('a', 'b', 'c', 'd', 'e')

# 使用 enumerate() 函数的 for 循环
# enumerate() 函数返回由元素索引和元素值组成的元组
for i, data in enumerate(ttuple):
    # 检查索引是否小于 3
    if i < 3:
        # 输出索引和元素值
        print(i, ':', data)
Text
0 : a
1 : b
2 : c
Python
# 创建元组
ttuple = (1, 2, 3)
# 输出元组
print("终端输出:", ttuple)
# 尝试修改元组元素的值
try:
    ttuple[2] = 300
except TypeError:
    print("错误:不能修改元组元素的值")

这大概就是本书中需要了解的关于元组的全部内容。

2.9 字典:按键索引

字典是一种用于以“键—值”对形式存储和组织信息的数据类型。字典提供了一种灵活而高效的数据处理方式,因此在许多编程任务中都是不可或缺的工具。

2.9.1 字典的主要特征

“键—值”对:字典中的数据以成对形式存储,每个键都对应某个确定的值。键充当唯一标识符,用于访问相应的值。

通过键进行索引:与列表使用数值索引不同,字典使用唯一键来访问值。这使得我们无需遍历整个集合,就能直接取得所需元素。

多样的值类型:字典中的值可以是任意数据类型的对象,包括字符串、数字、列表、其他字典,甚至函数。这使字典在各种用途下都具有很强的灵活性。

键不能重复:字典中的每个键都必须唯一。不允许存在两个或多个具有相同键的键值对。这保证了引用的确定性,并防止访问值时发生冲突。

值可以重复:键必须唯一,但字典中的值可以重复。如果需要,这允许不同键保存相同的值。

2.9.2 字典的使用示例

存储联系人信息:字典非常适合存储需要把唯一姓名或标识符与一组属性关联起来的数据。例如,可以使用字典保存一个人的姓名、电话、地址和电子邮件。

表示数据结构:字典可以用来建模各种数据结构,例如树、图和集合;其中元素之间通过不同关系相互连接。

统计文本中的词频:字典非常适合统计文本中单词出现的次数。每个单词可以作为键,其值表示该单词在文本中出现的次数。

设置和配置:字典常用于保存程序参数和配置,使得我们可以轻松修改这些值,而无需重写代码。

字典是 Python 中功能强大且通用的工具。它能够以“键—值”对形式高效地存储和管理数据,因此在许多编程领域中都不可替代。

2.9.3 为字典赋值

例如,可以用下面的格式把电话号码赋给字典:

Python
# 创建字典 phonebook1
phonebook1 = {}

# 向字典中添加记录
phonebook1["玛莎"] = 513476565
phonebook1["列沙"] = 513387234
phonebook1["娜斯佳"] = 513682762
phonebook1["圣诞老人"] = 513387234

# 将字典输出到屏幕
print(phonebook1)
Text
{'玛莎': 513476565, '列沙': 513387234, '娜斯佳': 513682762, '圣诞老人': 513387234}

也可以按如下方式初始化字典:

Python
# 创建字典 phonebook2
phonebook2 = {'列拉': 656477456, '娜斯佳': 656377243, '季莫费': 656662798}

# 将字典输出到屏幕
print(phonebook2)
Text
{'列拉': 656477456, '娜斯佳': 656377243, '季莫费': 656662798}
Python
# 声明字典 phonebook0
phonebook0 = {
    "科斯佳": [788567837, 788347278],  # 科斯佳有两个电话号码;这是一个列表
    "因娜": 513683222,
    "弗拉季克": 656477456
}

# 将字典 phonebook0 输出到屏幕
print("电话簿:")
print(phonebook0)
Text
电话簿:
{'科斯佳': [788567837, 788347278], '因娜': 513683222, '弗拉季克': 656477456}

2.9.4 遍历字典

与列表类似,字典也可以遍历。由于键和值以成对形式存储,我们可以使用 for 循环来处理它们。

Python
# 创建电话簿字典
phonebook1 = {
    "伊万诺夫 伊万": 1234567,
    "彼得罗夫 彼得": 89012345678,
    "西多罗夫 西多尔": 7890123456
}

# 遍历字典
for name, number in phonebook1.items():
    # 输出用户姓名及其电话号码
    print("%s 的电话号码是 %d" % (name, number))
Text
伊万诺夫 伊万 的电话号码是 1234567
彼得罗夫 彼得 的电话号码是 89012345678
西多罗夫 西多尔 的电话号码是 7890123456
Python
# 创建电话簿字典
phonebook1 = {
    "伊万诺夫 伊万": 1234567,
    "彼得罗夫 彼得": 89012345678,
    "西多罗夫 西多尔": 7890123456
}

# 遍历字典
for key, value in phonebook1.items():
    print(key, value)
Text
伊万诺夫 伊万 1234567
彼得罗夫 彼得 89012345678
西多罗夫 西多尔 7890123456
Python
# 创建电话簿字典
phonebook1 = {
    "伊万诺夫 伊万": 1234567,
    "彼得罗夫 彼得": 89012345678,
    "西多罗夫 西多尔": 7890123456
}

# 遍历字典中的键
for key in phonebook1.keys():
    print(key)
Text
伊万诺夫 伊万
彼得罗夫 彼得
西多罗夫 西多尔
Python
# 创建电话簿字典
phonebook1 = {
    "伊万诺夫 伊万": 1234567,
    "彼得罗夫 彼得": 89012345678,
    "西多罗夫 西多尔": 7890123456
}

# 遍历字典中的值
for value in phonebook1.values():
    print(value)
Text
1234567
89012345678
7890123456

2.9.5 删除值

要删除一对记录,可以使用内置功能 del 或方法 pop,并通过键来指定要删除的元素。

del 运算符按指定键从字典中删除元素。例如,下面的代码从字典 phonebook 中删除键为 “伊万诺夫 伊万” 的元素:

Python
phonebook = {
    "伊万诺夫 伊万": 1234567,
    "彼得罗夫 彼得": 89012345678,
    "西多罗夫 西多尔": 7890123456
}

del phonebook["伊万诺夫 伊万"]

print(phonebook)
Text
{'彼得罗夫 彼得': 89012345678, '西多罗夫 西多尔': 7890123456}

pop 运算符按指定键从字典中删除元素,并返回该元素的值。例如,下面的代码从字典 phonebook 中删除键为 “伊万诺夫 伊万” 的元素,并把该元素的值保存到变量 number 中:

Python
phonebook = {
    "伊万诺夫 伊万": 1234567,
    "彼得罗夫 彼得": 89012345678,
    "西多罗夫 西多尔": 7890123456
}

number = phonebook.pop("伊万诺夫 伊万")
print(phonebook)
print(number)
Text
{'彼得罗夫 彼得': 89012345678, '西多罗夫 西多尔': 7890123456}
1234567

2.9.6 合并两个字典

首先,可以使用 update() 方法。

Python
# 创建两个电话簿
phonebook1 = {
    "伊万诺夫 伊万": 1234567,
    "彼得罗夫 彼得": 89012345678,
    "西多罗夫 西多尔": 7890123456
}

phonebook2 = {
    "瓦西里耶夫 瓦西里": 9123456789,
    "普普金 普普金": 4567890123
}

# 更新电话簿 1
# 使用 update() 方法合并两个字典
phonebook1.update(phonebook2)

# 输出更新后的电话簿 1
print(phonebook1)
Text
{'伊万诺夫 伊万': 1234567, '彼得罗夫 彼得': 89012345678, '西多罗夫 西多尔': 7890123456, '瓦西里耶夫 瓦西里': 9123456789, '普普金 普普金': 4567890123}

现在使用一种更简单的工具,称为 double-star,即双星号写法。它可以创建第三个新字典,该字典是两个字典的组合,同时不影响两个原始字典。

Python
# 创建两个电话簿
phonebook1 = {
    "伊万诺夫 伊万": 1234567,
    "彼得罗夫 彼得": 89012345678,
    "西多罗夫 西多尔": 7890123456
}

phonebook0 = {
    "瓦西里耶夫 瓦西里": 9123456789,
    "普普金 普普金": 4567890123
}

# 创建新的电话簿 3
# 使用 ** 运算符合并两个字典
phonebook3 = {**phonebook1, **phonebook0}

# 输出新的电话簿 3
print(phonebook3)
Text
{'伊万诺夫 伊万': 1234567, '彼得罗夫 彼得': 89012345678, '西多罗夫 西多尔': 7890123456, '瓦西里耶夫 瓦西里': 9123456789, '普普金 普普金': 4567890123}

重复的键(如果存在)会在新字典中被覆盖:

Python
# 创建两个字典
dict_1 = {
    "苹果": 7,
    "香蕉": 5
}

dict_2 = {
    "香蕉": 3,
    "橙子": 4
}

# 合并两个字典
# 使用 ** 运算符
combined_dict = {**dict_1, **dict_2}

# 输出合并后的字典
print(combined_dict)
Text
{'苹果': 7, '香蕉': 3, '橙子': 4}

代码执行后,会创建一个新字典 combined_dict,其中包含字典 dict_1 和 dict_2 中的全部元素。

运算符 ** 用于合并两个字典。它根据键来合并两个字典中的元素。如果某个键同时存在于两个字典中,那么合并结果将采用第二个字典中的值。

在本例中,键 “香蕉” 同时存在于两个字典中。合并后,该键对应的值来自第二个字典,也就是 3。

2.10 集合

在 Python 中,集合是一种类似列表的集合类型,但集合中的每个元素都是唯一的,不能重复。

2.10.1 避免重复

来看一个例子:

Python
# 创建变量 sentence1,并为它赋值一个包含重复词语的字符串
sentence1 = "他的名字是谢苗,谢苗就是他的名字"
# 使用 split() 方法把字符串转换为单词列表
# 默认分隔符是空格,因此它会按词拆分字符串
words1 = sentence1.split()
# 为了便于观察,输出单词列表
# 输出应显示所有单词,包括重复项
print(words1)
# 将单词列表转换为集合,这会自动删除所有重复项
word_set1 = set(words1)
# 输出单词集合,以便看到重复项已经被删除
print(word_set1)
Text
['他的名字是谢苗,谢苗就是他的名字']
{'他的名字是谢苗,谢苗就是他的名字'}

如你所见,将列表转换为集合后,所有重复的词都会消失。集合中只保留唯一元素。

使用集合可以让我们轻松去除重复项,而无需编写额外代码来检查是否存在重复。这在数据处理中非常方便,例如需要去除列表中的重复项时。

2.10.2 两个集合的交集

Python
# 定义第一个单词集合
word_set1 = {'hello', 'world', 'name', 'is', 'first', 'python', 'and'}
# 定义第二个单词集合
word_set2 = {'this', 'is', 'a', 'test', 'name', 'first', 'and'}
# 使用 intersection() 方法求两个集合之间的交集。
# 集合交集返回一个新集合,其中包含所有同时出现在两个原始集合中的元素。
common_elements = word_set1.intersection(word_set2)

# 将集合中的公共元素输出到屏幕
print(common_elements)  # 将输出包含公共元素的集合
Text
{'name', 'is', 'and', 'first'}

2.10.3 两个集合的差集

在 Python 中,集合是以无序形式保存不重复元素的集合类型。集合的一项独特操作是可以求差集,即找出存在于一个集合中、但不存在于另一个集合中的元素。

Python
# 定义两个包含不同单词的集合
word_set1 = {'hello', 'his', 'Her', 'His', '娜斯佳'}
word_set2 = {'hello', 'Her', 'her', '安娜'}
# 获取并输出 word_set1 中存在、但 word_set2 中不存在的单词
# 预期输出:{'his', 'His', '娜斯佳'}
print(word_set1.difference(word_set2))
Text
{'his', 'His', '娜斯佳'}

在上面的例子中,与 word_set2 相比,单词 'his'、'His' 和 '娜斯佳' 是 word_set1 中独有的元素。

现在,如果我们想找出每个集合中各自独有的元素,也就是只存在于其中一个集合中的元素,就可以使用对称差集操作。

Python
# 输出 word_set1 与 word_set2 之间的对称差集
# 预期输出:{'his', 'His', 'her', '安娜', '娜斯佳'}
print(word_set1.symmetric_difference(word_set2))
Text
{'his', 'His', 'her', '安娜', '娜斯佳'}

在这种情况下,对称差集返回单词 'his'、'His'、'her'、'安娜' 和 '娜斯佳'。这些词对于两个集合而言都是“只属于其中一个集合”的元素,而不在它们的交集中。

一个常见问题是:能否对列表应用集合操作?Python 中的列表并不直接支持集合操作,因为列表是有序集合,并且可以包含重复元素。

Python
# 创建两个单词列表
words1 = ['hello', 'his', 'Her', 'His', '娜斯佳']
words2 = ['hello', 'Her', 'her', '安娜']
# 尝试使用 intersection 方法会导致错误,因为列表没有这种方法
# 如果取消下面代码的注释,将会引发 AttributeError:
# print(words1.intersection(words2))  # AttributeError: 'list' 对象没有属性 'intersection'

要在列表上使用集合操作,必须先用 set() 函数把列表转换为集合,执行所需操作;如有必要,再用 list() 函数把结果转换回列表。

Python
# 创建两个包含唯一单词的集合
word_set1 = {'hello', 'his', 'Her', 'His', '娜斯佳'}
word_set2 = {'hello', 'Her', 'her', '安娜'}
# 输出集合 word_set1 与 word_set2 的差集。
# difference 方法返回一个集合,其中包含 word_set1 中存在、但 word_set2 中不存在的元素。
# 预期输出:{'his', 'His', '娜斯佳'}
print('word_set1 和 word_set2 的差集:', word_set1.difference(word_set2))
# 输出集合 word_set1 与 word_set2 的对称差集。
# symmetric_difference 方法返回一个集合,其中包含只存在于其中一个集合中的元素。
# 预期输出:{'his', 'His', 'her', '安娜', '娜斯佳'}
print('word_set1 和 word_set2 的对称差集:', word_set1.symmetric_difference(word_set2))

# Python 中的列表不直接支持集合操作,因此不能直接使用 intersection 这类方法。
# 但是,可以把列表转换为集合,执行操作,然后在需要时把结果转换回列表。
# 创建两个单词列表
words1 = ['hello', 'his', 'Her', 'His', '娜斯佳']
words2 = ['hello', 'Her', 'her', '安娜']
# 将列表转换为集合,并求它们的对称差集
# 然后再把结果转换回列表
sym_diff_list = list(set(words1).symmetric_difference(set(words2)))
# 输出列表对称差集的结果
# 预期输出:['his', 'His', 'her', '安娜', '娜斯佳']
print('列表 words1 和 words2 的对称差集:', sym_diff_list)
Text
word_set1 和 word_set2 的差集: {'His', 'his', '娜斯佳'}
word_set1 和 word_set2 的对称差集: {'娜斯佳', '安娜', 'His', 'his', 'her'}
列表 words1 和 words2 的对称差集: ['娜斯佳', '安娜', 'His', 'his', 'her']

在这段代码中,我们创建了两个集合和两个列表。我们演示了集合上的差集和对称差集操作,并说明如何通过把列表转换为集合,使列表也能执行类似操作。代码中的注释详细描述了每一步以及预期输出。

2.11 条件与循环

条件是编程中进行决策的基础。我们使用条件来判断是否应当执行某个代码块。例如:“如果今天是星期一,就执行任务 A”。

for 和 while 循环用于多次重复执行同一代码块。for 循环通常用于重复次数已知的情况;while 循环则通常用于重复过程取决于某个条件是否成立的情况。例如,for 可用于遍历列表中的元素,而 while 可用于在达到某个条件之前持续执行某段代码。

2.11.1 运算符

Python 中的比较运算符包括:

Text
==   等于
!=   不等于
<    小于
<=   小于或等于
>    大于
>=   大于或等于

当我们使用这些运算符时,Python 会比较运算符两侧的值,并返回布尔值 True 或 False。

这些运算符是构造条件表达式的基础。条件表达式使程序能够根据不同的数据和场景作出反应。它们可用于检查用户输入、比较计算结果,以及根据内部状态和数据来控制程序行为。

示例:

Python
# 给变量 x 赋值 2
x = 2

# 将 x 与 2 比较,预期得到 True,因为 x 的确等于 2
print(x == 2)  # 结果:True

# 将 x 与 3 比较,预期得到 False,因为 x 不等于 3
print(x == 3)  # 结果:False

# 检查 x 是否小于 3,预期得到 True,因为 2 小于 3
print(x < 3)   # 结果:True

现在,让我们在条件结构中使用比较运算符。条件结构允许在条件为真(True)时执行特定代码块。

条件结构示例:

Python
# 给变量 x 赋值 2
x = 2

# 检查 x 是否等于 2
if x == 2:
    # 如果条件为真,输出消息“x 等于 2!”
    print("x 等于 2!")  # 预期输出:x 等于 2!
else:
    # 如果条件为假,输出消息“x 不等于 2。”
    print("x 不等于 2。")  # 由于条件为真,这行代码不会执行

在这段代码中,我们在 if 条件中使用了运算符 ==。如果条件为真,也就是说变量 x 的值确实等于 2,Python 就会执行 if 代码块中的语句。如果变量 x 等于除 2 以外的任何其他值,就会执行 else 代码块。

需要特别注意:当我们写 x == 2 时,并不是把值 2 赋给 x,而是在检查 x 的当前值是否等于数字 2。这是比较,不是赋值。

运算符 in 用于检查某个给定对象是否存在于列表等可迭代对象容器中。如果元素存在于容器中,该运算符返回 True;否则返回 False。

看下面的代码示例。我们定义两个变量 name1 和 name2,它们包含姓名;还定义一个由几个姓名组成的列表 groupA:

Python
# 声明两个字符串变量
name1, name2 = "萨沙", "廖沙"

# 创建包含姓名的列表 groupA
groupA = ["萨沙", "玛莎"]

# 检查 name1(萨沙)是否在 groupA 列表中
if name1 in groupA:
    print(name1, "在 A 组中找到")

# 检查 name2(廖沙)是否在 groupA 列表中
if name2 in groupA:
    print(name2, "在 A 组中找到")
else:
    # 如果在 groupA 中没有找到 name2(廖沙),则执行 else 分支
    print(name2, "未在 A 组中找到")

# 再添加一个姓名用于演示
name3 = "列娜"

# 检查 name3(列娜)是否在 groupA 列表中
if name3 in groupA:
    print(name3, "在 A 组中找到")
else:
    # 如果在 groupA 中没有找到 name3(列娜),则执行 else 分支
    print(name3, "未在 A 组中找到")
Text
萨沙 在 A 组中找到
廖沙 未在 A 组中找到
列娜 未在 A 组中找到

这个例子展示了如何有效地使用 in 运算符检查某个元素是否属于列表。

与相等运算符 == 不同,运算符 is 并不检查两个操作数的值是否相等。它检查两个操作数是否引用同一个对象。例如:

Python
x, y = ['a', 'b'], ['a', 'b']
z = y  # 将 z 赋值为与 y 指向同一对象的引用

print(x == y, ' x=', x, 'y=', y, hex(id(x)), hex(id(y)))
# 输出 True,因为 x 和 y 中的值相等

print(x is y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y)))
# 输出 False,因为 x 和 y 是不同对象(ID 不同)

print(y is z, ' y=', y, 'z=', z, hex(id(y)), hex(id(z)))
# 输出 True,因为 y 和 z 引用同一个对象

# 修改列表 y 中的第二个元素
y[1] = 'x'

print('修改 y 中的一个值之后')
print(x == y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y)))
# 输出 False,因为现在二者的值不再相等

print(x is y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y)))
# 输出 False,因为 x 和 y 是不同对象

print(y is z, ' y=', y, 'z=', z, hex(id(y)), hex(id(z)))
# 输出 True,因为 y 和 z 仍然引用同一个对象

# 将列表 x 添加到列表 y 中
y.append(x)

print('将列表 x 添加到列表 y 之后')
print(x == y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y)))
# 输出 False,因为 x 和 y 的值不同

print(x is y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y)))
# 输出 False,因为 x 和 y 是不同对象

print(y is z, ' y=', y, 'z=', z, hex(id(y)), hex(id(z)))
# 输出 True,因为 y 和 z 仍然引用同一个对象
Text
True  x= ['a', 'b'] y= ['a', 'b'] 0x100a77300 0x100b07c80
False x= ['a', 'b'] y= ['a', 'b'] 0x100a77300 0x100b07c80
True  y= ['a', 'b'] z= ['a', 'b'] 0x100b07c80 0x100b07c80
修改 y 中的一个值之后
False x= ['a', 'b'] y= ['a', 'x'] 0x100a77300 0x100b07c80
False x= ['a', 'b'] y= ['a', 'x'] 0x100a77300 0x100b07c80
True  y= ['a', 'x'] z= ['a', 'x'] 0x100b07c80 0x100b07c80
将列表 x 添加到列表 y 之后
False x= ['a', 'b'] y= ['a', 'x', ['a', 'b']] 0x100a77300 0x100b07c80
False x= ['a', 'b'] y= ['a', 'x', ['a', 'b']] 0x100a77300 0x100b07c80
True  y= ['a', 'x', ['a', 'b']] z= ['a', 'x', ['a', 'b']] 0x100b07c80 0x100b07c80

在这个例子中可以看到,== 和 is 的工作方式不同。== 比较列表中的值,而 is 检查变量是否指向内存中的同一个对象。这一点通过函数 id() 得到展示:id() 会返回对象在内存中的唯一标识符。一开始,x 和 y 具有相同的值,但它们指向不同对象,从不同的 ID 可以看出这一点。当我们把 y 赋给 z 时,z 开始指向与 y 相同的对象。

修改列表 y 中的元素不会影响 x,因为它们是不同对象。然而,由于 z 仍然指向与 y 相同的对象,y 的变化也会反映在 z 中。把 x 添加到列表 y 会使 y 的长度变大并改变其内容,但不会影响 x,也不会改变 y 和 z 指向同一个对象这一事实。

运算符 not 是逻辑运算符,用于反转布尔表达式的值:如果表达式为真(True),结果就是 False;反之亦然。

在构造复杂逻辑条件时,not 特别有用。它可以与其他运算符结合使用,例如 is(检查对象身份)和 in(检查元素是否存在于序列中)。

看几个例子:

Python
# 示例 1:简单使用 not
print(not False)  # 输出 True,因为 not 将 False 反转为 True

# 示例 2:使用 not 进行比较
print(not False == False)  # 输出 False,等价于表达式 True == False

# 示例 3:将 not 与 == 和 is 运算符结合使用
x, y = [1, 2, 3], [1, 2, 3]
z = y  # 将 z 赋值为与 y 指向同一对象的引用

# 比较列表的值
# 使用 == 运算符检查两个列表中的值是否相等
print(x == y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y)))
# 输出 True,因为列表 x 和 y 中的值相同,虽然它们是不同对象

# 使用 is not 比较对象身份
# 使用 is not 检查变量是否指向不同对象
print(x is not y, 'x=', x, 'y=', y, hex(id(x)), hex(id(y)))
# 输出 True,因为 x 和 y 虽然值相同,但它们是不同对象

# 使用 is 检查对象身份
print(y is z, 'y=', y, 'z=', z, hex(id(y)), hex(id(z)))
# 输出 True,因为 y 和 z 指向同一个对象
Text
True
False
True x= [1, 2, 3] y= [1, 2, 3] 0x104b5b300 0x104bebc80
True x= [1, 2, 3] y= [1, 2, 3] 0x104b5b300 0x104bebc80
True y= [1, 2, 3] z= [1, 2, 3] 0x104bebc80 0x104bebc80

2.11.1 使用条件结构 if

与其他编程语言一样,if 结构是执行条件逻辑的基本工具。前面我们已经看过几个例子。下面给出一些在 Python 中使用 if 条件的补充示例,用来展示代码块的工作方式:

Python
# 设置舒适温度,单位为摄氏度
temp_good = 22.0

# 当前温度;可以修改该值进行实验
temp_now1 = 48.0

# 为不同温度状态定义条件
statement1 = (temp_now1 <= (temp_good + 10.0)) & (temp_now1 >= (temp_good - 10.0))  # 舒适
statement2 = temp_now1 < (temp_good - 10.0)  # 寒冷
statement3 = temp_now1 > (temp_good + 10.0)  # 炎热

# 使用条件语句输出相应消息
if statement1:  # 不要忘记冒号
    print("好,现在是", temp_now1, "摄氏度。很舒适。我们走吧。")
    pass  # 这里可以添加其他操作

elif statement2:  # 不要忘记冒号
    print("不行,现在是", temp_now1, "摄氏度。太冷了。我们不能走。")
    pass  # 这里可以添加其他操作

elif statement3:
    print("不行,现在是", temp_now1, "摄氏度。太热了。我们不能走。")
    pass  # 这里可以添加其他操作

else:
    print("让我们检查一下温度。")  # 执行其他操作
    pass
Text
不行,现在是 48.0 摄氏度。太热了。我们不能走。

请注意,在 Python 中,一个 if 条件语句里可以使用的分支块数量没有限制。

2.11.3 for 循环

for 循环是一种程序控制结构,允许把某个代码块重复执行指定次数。当需要对集合(例如列表或数组)中的每个元素执行操作,或者需要把某个任务重复固定次数时,它特别有用。

Python 中 for 循环的语法如下:

Python
for variable in sequence:
    # 要执行的代码块

这里,variable 会依次取得 sequence 中每一个元素的值,循环内部的代码块会针对每个元素执行一次。

遍历列表元素:

Python
for element in [1, 2, 3, 4, 5]:
    print(element)

这会输出列表中的每一个元素。

与 range 结合使用:

Python
for number in range(10):
    print(number)

这会输出从 0 到 9 的数字。

for 循环在人工智能(AI)编程中起着重要作用,尤其是在数据处理、模型训练和任务自动化中:

数据处理。 循环常用于数据预处理,例如在把数据集用于机器学习模型之前,对数据集进行归一化或标准化。

模型训练。 在一些算法中,例如梯度下降,循环用于在训练过程中多次更新模型权重。

自动化和测试。 for 循环适合自动执行重复性任务,例如在不同数据集或不同参数下测试模型。

AI 中的特殊算法。 在深度学习领域,for 循环用于循环神经网络(RNN)中,以便遍历数据序列,例如文本。

Python
primes = [2, 3, 5]  # 定义一个质数列表

for prime in primes:  # 遍历质数列表
    print(prime, end=' ')  # 在同一行输出每个质数

print('\nrange(10) 中的元素是')
for n in range(10):  # 创建从 0 到 9 的范围
    print(n, end=',')  # 在同一行输出每个数字,并用逗号分隔

print('\nrange(3, 8) 中的元素是')
for n in range(3, 8):  # 创建从 3 到 7 的范围
    print(n, end=',')

print('\nrange(3, 10, 2) 中的元素是')
for n in range(-3, 10, 2):  # 创建从 -3 到 9、步长为 2 的范围
    print(n, end=',')

print('\nrange(10, -3, -2) 中的元素是')
for n in range(10, -3, -2):  # 创建从 10 到 -2、步长为 -2 的反向范围
    print(n, end=',')
Text
2 3 5
range(10) 中的元素是
0,1,2,3,4,5,6,7,8,9,
range(3, 8) 中的元素是
3,4,5,6,7,
range(3, 10, 2) 中的元素是
-3,-1,1,3,5,7,9,
range(10, -3, -2) 中的元素是
10,8,6,4,2,0,-2,

2.11.4 while 循环

while 循环是一种程序控制结构,只要某个条件成立,就会执行一个代码块。当迭代次数事先未知,或者迭代次数取决于程序运行时的动态因素时,这种循环特别有用。

Python 中 while 循环的语法如下:

Python
while condition:
    # 要执行的代码块

这里,只要条件保持为真,while 循环中的代码块就会持续执行。一旦条件变为假,循环就会停止。

等待状态变化:

Python
while limit_not_reached:
    # 执行一些操作

循环会持续执行,直到达到某个给定限制。

带退出条件的无限循环:

Python
while True:
    # 执行操作
    if exit_condition:
        break

循环会无限执行,直到满足某个特定退出条件。

while 循环会一直重复,直到指定的布尔条件不再成立。该条件控制循环内部操作的执行。看下面的例子:

Python
# 初始化计数器
count = 0

# 只要 count 小于 10,循环就继续执行
while count < 10:
    print(count, end=',')
    count += 1  # 等价于 count = count + 1
Text
0,1,2,3,4,5,6,7,8,9,

使用 break 和 continue 语句:

break 用于退出 for 或 while 循环。

continue 会跳过当前代码块,并回到循环条件处继续执行。

break 示例:

Python
# 输出所有数字,直到达到给定限制
count = 0

while True:
    print(count, end=',')
    count += 1

    if count >= 10:  # 如果达到限制,就中断循环
        break

print('\n')  # 循环结束后换行
Text
0,1,2,3,4,5,6,7,8,9,

continue 示例:

Python
# 只输出偶数:0, 2, 4, 6, 8
for n in range(10):
    if n % 2 != 0:  # 如果数字是奇数,就跳过它
        continue

    print(n, end=',')
Text
0,2,4,6,8,

带 else 块的循环:

当循环条件不再满足时,会执行 else 块中的代码。不过,如果循环内部执行了 break 语句,else 块会被跳过。需要注意的是,即使前面执行过 continue,else 块仍然会执行。

else 示例:

Python
# 输出从 0 到 4 的数字,然后输出达到限制的消息
count = 0
nlimit = 5

while count < nlimit:
    print(count, end=',')
    count += 1
else:
    print("计数器的值已达到 %d" % (nlimit))
Text
0,1,2,3,4,计数器的值已达到 5

2.11.8 三元条件运算符

Python 中的三元条件运算符是一种基于条件执行操作的紧凑方式。与传统的多行 if-else 条件结构不同,三元运算符可以把全部逻辑封装在一行中。这使代码更加整洁、简洁,并符合 DRY 原则(Don't Repeat Yourself,即“不要重复自己”)。

例如,考虑下面这种四行条件结构:

Python
if condition:
    variable = value_1
else:
    variable = value_2

使用三元条件运算符,可以把它有效地压缩为一行,其通用语法如下:

Python
variable = value_1 if condition else value_2

这种写法不仅减少了代码数量,也在保持完整条件逻辑功能的同时,使代码更容易阅读和理解。当需要根据某个条件为变量赋值时,三元运算符尤其有用。

Python
# 定义用于演示的变量
value_1 = "条件为真时的值"
value_2 = "条件为假时的值"

# 定义要检查的条件
# 本例中,条件检查数字是否为偶数
number = 10  # 可以修改该值,以检查不同条件

# 使用三元条件运算符为变量 result 赋值
# 如果 number 是偶数(number % 2 等于 0),就赋值 value_1;否则赋值 value_2
result = value_1 if number % 2 == 0 else value_2

# 输出结果
# 如果数字为偶数,就输出 value_1;如果为奇数,就输出 value_2
print(result)
Text
条件为真时的值

三元表达式 value_1 if number % 2 == 0 else value_2 根据数字是否为偶数,把两个值中的一个赋给变量 result。最后,结果会输出到屏幕上。

2.12 函数(方法)

函数是一种方便的代码结构化方式。通过函数,可以把代码组织成一个个代码块,并在需要时无限次调用这些代码块。这能显著减少代码重复,使代码更整洁、更易读,也更便于维护。此外,函数还是定义接口的良好方式,便于程序员之间共享代码。

2.12.1 用于定义函数的块结构

在 Python 中,函数使用块关键字 def 定义,后面跟函数名;函数名同时也是该代码块的名称。调用函数时,要使用函数名,并在后面加上圆括号 ();如果函数有参数,则参数写在圆括号中。先尝试最简单的函数:

Python
def hi():
    # 在屏幕上输出问候消息
    print("你好,欢迎使用这个简单函数!")

# 调用问候函数
hi()
Text
你好,欢迎使用这个简单函数!

在这个例子中,函数 hi 不接收任何参数;调用它时,会在屏幕上输出消息“你好,欢迎使用这个简单函数!”。这展示了在 Python 中创建和使用函数的基本原则。

2.12.2 带参数的函数

在前面介绍的最简单情况中,函数不需要参数。然而,函数通常会带有必需参数,这些参数是从调用代码传递给函数的变量。这样,函数就可以处理不同输入数据,其行为也更加灵活。

Python
# 定义一个带两个参数的函数:username 和 greeting
def greeting_student(username, greeting):
    # 使用 f 字符串插入变量,输出问候消息
    print(f"你好,{username},祝贺你!祝你{greeting}")

# 调用函数,并传递两个字符串参数
greeting_student("斯乔帕", "在函数使用中开启一段有趣旅程!")
Text
你好,斯乔帕,祝贺你!祝你在函数使用中开启一段有趣旅程!

函数还可以使用关键字 return 向调用代码返回值。

2.12.3 带返回值的函数示例

Python
# 定义一个对两个数求和的函数
def sum_two_numbers(a, b):
    return a + b  # 返回函数两个参数的和

# 给两个变量赋值,并调用函数求和
x, y = 2.0, 8.0

apb = sum_two_numbers(x, y)
print(f'{x} + {y} = {apb}')  # 输出结果:"2.0 + 8.0 = 10.0"

# 修改变量值,并再次调用函数
x, y = 20, 80

apb = sum_two_numbers(x, y)
print(f'{x} + {y} = {apb}, 太好了!')  # 输出结果:"20 + 80 = 100, 太好了!"
Text
2.0 + 8.0 = 10.0
20 + 80 = 100, 太好了!

2.12.4 变量作用域

Python 中存在 LEGB 规则(Local、Enclosing、Global、Built-in,即局部、外层、全局、内置),它决定解释器查找变量的顺序。当变量被找到时,查找就会停止。

Python
# 定义一个函数,在函数内部局部修改参数值
def sum_two_numbers(a, b):
    a += 1  # 在局部把参数 a 的值增加 1
    print('函数内部,a=', a)  # 输出 a 的局部值
    return a + b  # 返回修改后的 a 与 b 的和

x, y = 2.0, 8.0

print('调用函数之前,x=', x, 'y=', y)  # 输出调用函数之前 x 和 y 的值
apb = sum_two_numbers(x, y)           # 调用函数并保存结果
print('调用函数之后,%f + %f = %f' % (x, y, apb))  # 输出调用函数之后的结果
Text
调用函数之前,x= 2.0 y= 8.0
函数内部,a= 3.0
调用函数之后,2.000000 + 8.000000 = 11.000000

2.12.5 Lambda 函数(匿名函数)

匿名函数是不带名称的函数。在大多数编程语言中,定义函数时也会给它命名,以便之后在代码中通过这个名称调用它。不过,匿名函数不同:它们在定义时没有名称,通常就在创建它们的位置直接使用。

在 Python 中,匿名函数通过关键字 lambda 创建,因此也叫作 lambda 函数。lambda 函数可以接收任意数量的参数,但只能包含一个表达式。它们的语法被简化,适合创建小型函数。这类函数不需要显式写出 return 来返回值,表达式的结果会自动返回。

Python 中匿名函数的使用示例:

Python
result = (lambda x, y: x + y)(10, 20)
print(result)
Text
30

在这个例子中,一个用于把两个数相加的 lambda 函数被定义后立即用参数 10 和 20 调用。这个函数没有名称,只在定义处使用了一次。

当需要把一个简单函数作为参数传递给另一个函数时,匿名函数尤其有用。这在函数式编程中很常见,例如使用 map()、filter() 和 reduce() 等高阶函数时,lambda 函数可以作为紧凑而高效的处理函数。

lambda 函数是在一行中表示函数的紧凑形式。它是最简单、同时也很有用的函数形式之一。由于它非常简洁,因此非常适合在使用位置直接创建小型匿名函数。

lambda 函数也适合定义数学表达式。例如,线性函数可以定义为 f(x)=ax+b,其中 a 和 b 是常数。在 Python 中,可以用 lambda 函数表示如下:

Python
linear_function = lambda x, a, b: a * x + b
print("线性函数:f(x) = 2x + 3,当 x=5 时给出", linear_function(5, 2, 3))
Text
线性函数:f(x) = 2x + 3,当 x=5 时给出 13

类似地,二次函数 f(x)=ax²+bx+c 可以通过 lambda 函数定义如下:

Python
quadratic_function = lambda x, a, b, c: a * x**2 + b * x + c
print("二次函数:f(x) = 2x^2 + 3x + 1,当 x=2 时给出", quadratic_function(2, 2, 3, 1))
Text
二次函数:f(x) = 2x^2 + 3x + 1,当 x=2 时给出 15

lambda 函数经常与普通函数结合使用,尤其是在需要从紧凑函数返回值时。例如,它们可以作为参数传给 map()、filter() 和 reduce() 等高阶函数。

lambda 函数与 map() 函数结合使用的例子:

Python
numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(lambda x: x**2, numbers))
print("数字的平方:", squared_numbers)  # 输出:[1, 4, 9, 16, 25]
Text
数字的平方: [1, 4, 9, 16, 25]

2.13 类与对象

类是面向对象编程中的基本结构,用作创建对象的模板。类封装变量和函数(或方法),为其对象提供结构和行为。可以从一个类创建任意数量的对象,每个对象都会继承该类的结构、变量(属性)以及函数(方法)。

2.13.1 最简单的类

下面的例子展示了一个名为 C 的最简单类。类中定义了属性 ca,它是一个类属性。类内部用三引号括起来的注释用于提供类文档,或说明该类的用途。

Python
class C:
    ''' 名为 C 的最简单类 '''

    ca = "类属性"  # 定义类属性

# 使用 help() 函数获取类 C 的信息
help(C)

# 创建类 C 的两个实例
i1 = C()  # 创建类 C 的实例 i1
i2 = C()  # 创建类 C 的实例 i2

# 演示通过实例和类本身访问类属性
print('i1.ca =', i1.ca)  # 通过实例 i1 访问类属性
print('i2.ca =', i2.ca)  # 通过实例 i2 访问类属性
print('C.ca =', C.ca)    # 直接通过类本身访问类属性

# 修改类属性,并观察它对实例的影响
C.ca = "这是被修改后的类属性 'ca'"
print('修改之后:')
print('C.ca =', C.ca)    # 显示类属性修改后的值
print('i1.ca =', i1.ca)  # 实例 i1 反映类属性的变化
print('i2.ca =', i2.ca)  # 实例 i2 反映类属性的变化

这段代码展示了如何创建类的实例,以及实例如何与类属性关联。同时它还展示了类属性的修改会反映到该类的所有实例上,从而强调了面向对象编程中共享属性的性质。

类属性和对象实例属性存储在不同的字典中。对于类 C,字典 C.dict 包含类属性,而 i1.dict 和 i2.dict 存储对应对象实例的属性。当我们修改某个实例属性(例如 i1.ca)时,会为该实例创建一个新的字典,用来反映被修改的属性。如果没有进行修改,例如在给 i2.ca 赋值之前,实例字典为空,属性仍从类继承而来。当给 i2.ca 赋值后,也会为 i2 创建属性字典,反映实例层面的变化。需要注意的是,类层面属性的任何后续变化,都不会影响那些已经拥有自己属性字典的实例属性。

Python
class C:
    ca = "类属性 'ca' 的初始值"

# 创建类 C 的实例
i1 = C()
i2 = C()

# 修改类属性 ca
C.ca = "类属性 'ca' 的第 2 次修改"

# 输出类 C 的字典
print("类 C 的字典:", C.__dict__)

# 修改 i1 的实例属性 ca
i1.ca = "这是被修改后的实例属性 'ca'"

# 输出实例 i1 的字典
print("实例 i1 的字典:", i1.__dict__)

# 输出实例 i2 修改之前的字典
print("实例 i2 修改前的字典:", i2.__dict__)

# 修改 i2 的实例属性 ca
i2.ca = "现在修改实例属性 'ca'"

# 输出实例 i2 修改之后的字典
print("实例 i2 修改后的字典:", i2.__dict__)
Text
类 C 的字典: {'__module__': '__main__', 'ca': "类属性 'ca' 的第 2 次修改", '__dict__': <attribute '__dict__' of 'C' objects>, '__weakref__': <attribute '__weakref__' of 'C' objects>, '__doc__': None}
实例 i1 的字典: {'ca': "这是被修改后的实例属性 'ca'"}
实例 i2 修改前的字典: {}
实例 i2 修改后的字典: {'ca': "现在修改实例属性 'ca'"}

2.13.2 用于科学计算的类

Python
class Circle:
    """
    Circle 类用于执行与圆有关的计算。

    具体来说,它可以根据给定半径计算圆的面积。
    """

    pi = 3.14159  # 类属性,包含所有类实例都会使用的圆周率值

    def __init__(self, radius):
        """
        类构造函数,用给定半径初始化 Circle 类的新实例。

        :param radius: 圆的半径
        """
        self.radius = radius  # 实例属性,保存圆的半径

    def circle_area(self):
        """
        用于计算圆面积的方法。

        :return: 圆的面积
        """
        return self.pi * self.radius**2  # 使用公式 πr² 返回圆面积

# 创建半径为 10 的 Circle 类实例
r = 10
c10 = Circle(r)

# 输出初始值
print('修改前的 Circle.pi:', Circle.pi)
print('半径:', c10.radius)
print('修改前的 c10.pi:', c10.pi)

# 只修改实例 c10 的 pi 值
c10.pi = 3.14

# 输出修改后的值
print('通过 c10.pi 修改后的 c10.pi:', c10.pi)
print('通过 c10.pi 修改后的 Circle.pi:', Circle.pi)
print('c10 的圆面积 =', c10.circle_area())
print('半径为 100 的圆面积 =', Circle(100).circle_area())
Text
修改前的 Circle.pi: 3.14159
半径: 10
修改前的 c10.pi: 3.14159
通过 c10.pi 修改后的 c10.pi: 3.14
通过 c10.pi 修改后的 Circle.pi: 3.14159
c10 的圆面积 = 314.0
半径为 100 的圆面积 = 31415.899999999998

在这段代码中:

Circle 类描述一个圆,并能够计算它的面积。

属性 pi 是类属性,对所有实例共享,用来表示圆周率常数。

方法 init 初始化类实例,为实例设置半径。

方法 circle_area 使用实例属性计算圆面积。

示例展示了修改特定实例(c10)的属性 pi 并不会影响类属性 pi,从而强调类属性和实例属性之间的差异。

这个例子还说明了面向对象编程的基本概念,例如封装(把数据隐藏在类内部)和继承(创建子类时使用)。

2.13.3 子类(类继承)

在 Python 中,子类通常用于实现继承。继承允许创建新类,在不影响现有类当前使用方式的前提下,充分利用已有类的结构(属性和函数)。这也有助于通过减少代码重复来改进已有程序。

假设前面创建的 Circle 类代码已经发布,并被许多人使用。现在我们决定创建另一个类,用于根据圆的一部分所占比例来计算扇形面积。为此,我们可以创建名为 PartialCircle 的子类,它不会影响已经发布的 Circle 类的使用。

Python
class PartialCircle(Circle):
    ''' 基于 Circle 类的 PartialCircle 子类:计算圆扇形的面积。 '''

    def __init__(self, radius, portion):
        ''' 用 radius 和 portion 两个属性初始化。 '''
        super().__init__(radius)  # 从基类 Circle 继承属性
        self.portion = portion    # 子类属性,定义圆的比例

    def sector_area(self):
        ''' 计算圆扇形面积。 '''
        return self.portion * self.circle_area()  # 使用基类方法计算圆面积

读者可以运行这段代码,并花一些时间研究子类的结构、它与基类的关系、使用 self 为未来对象准备连接的方式,以及哪些属性和函数是从基类继承来的,哪些是在子类中新创建的。

Python
pc10 = PartialCircle(10., 0.5)  # 创建子类对象实例,半径为 10,占圆的 50%

print(pc10.pi)             # 使用来自基类的属性
print(pc10.radius)         # 同样是来自基类的属性
print(pc10.sector_area())  # 计算 50% 圆扇形的面积

现在,通过子类实例修改常数 pi 的值:

Python
pc10.pi = 3.14
print(pc10.pi)  # 值已改为 3.14

再通过基类 Circle 检查 pi 的值:

教材插图:Python 基础
教材插图:Python 基础 — 2.13.3 子类(类继承)
Python
print(c10.pi)  # 值保持不变。对子类实例的修改不会影响基类。

下面给出一个带有详细注释的完整代码清单,用 Circle 和 PartialCircle 两个类演示 Python 中的类继承。

Python
# 定义基类 Circle
class Circle:
    ''' Circle 类用于表示圆并计算圆面积。 '''

    def __init__(self, radius):
        ''' 使用一个属性 radius(半径)初始化类实例。 '''
        self.radius = radius  # 实例属性,保存圆半径
        self.pi = 3.14159     # 属性,保存 π 的值

    def circle_area(self):
        ''' 用于计算圆面积的方法。 '''
        return self.pi * (self.radius ** 2)  # 按公式 πr² 返回圆面积

# 定义继承 Circle 的子类 PartialCircle
class PartialCircle(Circle):
    ''' 基于 Circle 类的 PartialCircle 子类:计算圆扇形的面积。 '''

    def __init__(self, radius, portion):
        ''' 用 radius 和 portion(圆的比例)两个属性初始化。 '''
        super().__init__(radius)  # 调用基类初始化器来设置半径
        self.portion = portion    # 子类属性,定义需要计算面积的圆的比例

    def sector_area(self):
        ''' 计算圆扇形面积。 '''
        return self.portion * super().circle_area()  # 使用基类方法计算圆面积,并乘以比例

# 创建基类 Circle 的实例
c10 = Circle(10)  # 半径为 10 的 Circle 类实例
print("圆面积:", c10.circle_area())  # 计算并输出圆面积

# 创建子类 PartialCircle 的实例
pc10 = PartialCircle(10, 0.5)  # 半径为 10、比例为 50% 的 PartialCircle 类实例
print("50% 圆扇形面积:", pc10.sector_area())  # 计算并输出扇形面积

# 通过子类实例修改 π 的值
pc10.pi = 3.14
print("子类中的 pi 修改值:", pc10.pi)

# 检查基类中的 π 值是否发生变化
print("基类中的 pi 值:", c10.pi)  # 基类中的 π 值保持不变
Text
圆面积: 314.159
50% 圆扇形面积: 157.0795
子类中的 pi 修改值: 3.14
基类中的 pi 值: 3.14159

2.14 NumPy 数组:科学计算的便利工具

NumPy 数组与列表相似,但在科学计算中使用起来方便得多。在处理大规模数据时,NumPy 数组上的运算通常也快得多。

2.14.1 列表与 NumPy 数组及其与经典列表的区别

在 Python 中,列表和 NumPy 数组都是用于存储一组元素的数据结构。尽管它们表面上相似,但二者存在一些重要差异,在为具体任务选择合适工具时必须加以考虑。本章将详细讨论列表与 NumPy 数组的共同点和区别,并特别关注它们在不同计算场景中的适用性。

共同特征

可变性: 这两类结构在创建之后都允许修改,可以通过所谓的破坏性操作添加、删除或改变元素。

索引: 可以通过数值索引访问单个元素,索引反映元素在结构中的位置。

切片: 可以借助切片机制创建新的结构,其中包含原始数据的某些片段。

主要区别

来源: NumPy 数组来自外部库 NumPy,需要先导入;而列表是 Python 语言内置的数据类型。

逐元素运算: NumPy 数组支持直接对所有元素执行数学和逻辑运算,无须显式使用循环;列表本身不具备这一能力。

类型同质性: NumPy 数组要求元素属于同一种数据类型,而列表允许存储不同类型的元素。

多维性: NumPy 数组能够表示多维结构,包括矩阵和张量;这超出了普通一维列表的能力范围。

性能: 由于计算经过优化,并使用底层 C 语言代码实现,NumPy 数组上的运算通常显著快于列表上的类似操作。

内存效率: 与列表相比,NumPy 数组的数据存储更加紧凑,这在处理大量信息时尤为重要。

应用领域: 借助性能优势和对多维运算的支持,NumPy 数组广泛用于数学计算、数组数据处理和机器学习算法。

有意识地选择合适的数据结构,可以在不同计算任务中优化性能、内存使用和开发便利性。

2.14.2 NumPy 数组结构

首先,我们简要考察 NumPy 数组的结构,并把它与前面讨论过的列表进行比较。为了开始讨论,我们导入 NumPy 包。

Python
import numpy as np  # 导入 numpy 模块,并给它指定别名 np
Python
x1 = np.array([28, 3, 28, 0])  # 创建一维 NumPy 数组
print('x1=', x1)               # 输出数组 x1
# NumPy 数组看起来与列表相似
Text
x1= [28  3 28  0]

如上所示,NumPy 数组也用一对方括号包围起来,这一点与列表相似。

Python
import numpy as np

# 导入 numpy 模块,并给它指定别名 np
x2 = np.array([[51, 22.0], [0, 0], (18 + 9j, 3.)])  # 混合类型
# 创建一个包含三行两列的数组 x2。前两行包含整数,第三行包含复数和实数。

print('x2=', x2)  # 所有元素都会变成复数
# 输出数组 x2。由于数组中存在复数,数组中的所有数字都会转换为复数。
Text
x2= [[51.+0.j 22.+0.j]
 [ 0.+0.j  0.+0.j]
 [18.+9.j  3.+0.j]]

这是一个二维 NumPy 数组。它由两层方括号包围。列表本身并不具备真正的多维性,除非采用嵌套形式,即“列表中的列表”。

我们也可以借助列表来创建 NumPy 数组。在下面的示例中,我们先创建两个列表,然后由它们生成 NumPy 数组:

Python
# 人的体重列表(单位:kg)
list_w = [57.5, 64.3, 71.6, 68.2]
# 人的身高列表(单位:m)
list_h = [1.5, 1.6, 1.7, 1.65]
# 输出体重列表
print('人的体重:', list_w)
# 输出身高列表
print('人的身高:', list_h)
Text
人的体重: [57.5, 64.3, 71.6, 68.2]
人的身高: [1.5, 1.6, 1.7, 1.65]
Python
# 导入 NumPy 库
import numpy as np

# 声明人的体重和身高列表
list_w = [57.5, 64.3, 71.6, 68.2]
list_h = [1.5, 1.6, 1.7, 1.65]

# 将列表转换为 NumPy 数组
narray_w = np.array(list_w)
narray_h = np.array(list_h)

# 输出数组
print('体重数组:', narray_w)
print('身高数组:', narray_h)
Text
体重数组: [57.5 64.3 71.6 68.2]
身高数组: [1.5  1.6  1.7  1.65]

现在创建一个函数,用于输出给定 NumPy 数组的信息。

首先创建名为 getArrayInfo() 的函数。该函数接收一个 NumPy 数组作为参数,并打印关于该数组的以下信息:

• 数组第一个元素的值;

• 数组类型;

• 数组维数;

• 数组形状;

• 数组中的元素个数;

• 数组的数据类型;

• 数组的内存地址。

下面是 getArrayInfo() 函数的代码:

Python
def getArrayInfo(a):
    """获取给定数组的信息:getArrayInfo(array)"""

    # 打印数组第一轴上的元素
    print('数组第一轴上的元素:', a[0])
    # 打印数组类型
    print('类型:', type(a))
    # 打印数组维数
    print('维数,a.ndim:', a.ndim)
    # 打印数组形状(每条轴上的尺寸)
    print('数组形状,a.shape:', a.shape)
    # 打印数组中的元素总数
    print('元素个数,a.size:', a.size)
    # 打印数组元素的数据类型
    print('元素数据类型,a.dtype:', a.dtype)
    # 打印存储数组的内存地址(调试信息)
    print('内存地址:', a.data)

现在可以使用 getArrayInfo() 函数获取任意 NumPy 数组的信息。例如,可以像下面这样获取数组 narray_w 的信息:

Python
narray_w = np.array([57.5, 64.3, 71.6, 78.9])
getArrayInfo(narray_w)
Text
数组第一轴上的元素: 57.5
类型: <class 'numpy.ndarray'>
维数,a.ndim: 1
数组形状,a.shape: (4,)
元素个数,a.size: 4
元素数据类型,a.dtype: float64
内存地址: <memory at 0x102f5a680>

2.14.3 NumPy 数组的切片

切片同样适用于 NumPy 数组,其方式与列表类似。下面取一个数组切片。

Python
import numpy as np

list_w = [57.5, 64.3, 71.6, 68.2]
list_h = [1.5, 1.6, 1.7, 1.65]
narray_w = np.array([57.5, 64.3, 71.6, 78.9])

print(list_w[1:3])    # 第二个和第三个元素之间的切片
print(narray_w[1:3])  # 第二个和第三个元素之间的切片
Text
[64.3, 71.6]
[64.3 71.6]

现在分别向列表和 NumPy 数组添加一个元素。

Python
list_w.append(59.8)
print(list_w)

# 对于 NumPy 数组,我们使用:
print(np.append(narray_w, 59.8))
Text
[57.5, 64.3, 71.6, 68.2, 59.8]
[57.5 64.3 71.6 78.9 59.8]
Python
print(list_w, ', ', narray_w)
print(type(list_w), ', ', type(narray_w))
print(len(list_w), ', ', narray_w.ndim)  # 使用 len() 获取长度
Text
[57.5, 64.3, 71.6, 68.2, 59.8] ,  [57.5 64.3 71.6 78.9]
<class 'list'> ,  <class 'numpy.ndarray'>
5 ,  1
Python
nwh = (narray_w, narray_h)  # 这会形成一个由 NumPy 数组组成的元组
print(nwh)
# (array([57.5, 64.3, 71.6, 68.2]), array([1.5, 1.6, 1.7, 1.65]))

# 为了形成多维数组,可以使用如下方式(后面会更详细讨论):
arr = np.array([narray_w, narray_h])
# array([[57.5, 64.3, 71.6, 68.2 ],
#        [ 1.5,  1.6,  1.7,  1.65]])

在 NumPy 中,基本数据结构是多维数组。考察二维数组 arr。数组尺寸以元组 (2, 4) 表示,其中第一个数字表示行数,也就是沿轴 0 的记录数;第二个数字表示列数,也就是沿轴 1 的记录数。

转置是改变数组轴顺序的操作。在 NumPy 中,可以使用 .T 方法完成数组转置。该操作把行变为列、列变为行,从而改变数组的形状。

Python
import numpy as np

# 原始数组
arr = np.array([[57.5, 64.3, 71.6, 68.2],
                [ 1.5,  1.6,  1.7,  1.65]])

# 数组转置
arrT = arr.T

# 输出转置后的数组
print(arrT)
Text
[[57.5   1.5 ]
 [64.3   1.6 ]
 [71.6   1.7 ]
 [68.2   1.65]]

执行这段代码后,数组的形状会从原来的 (2, 4) 变为 (4, 2)。这意味着原本包含 2 行 4 列的数组,在转置后将包含 4 行 2 列。

从示例可以看到,原数组的第一列变成了转置数组的第一行,其余元素也依此类推。这个过程广泛用于数学计算、数据处理和机器学习中的数据准备与转换。

2.14.4 NumPy 数组的引用与副本

下面考察 NumPy 数组中的修改如何影响引用同一个数组的其他变量。

Python
import numpy as np

# 创建一个包含两行四列的 NumPy 数组
arr = np.array([[57.5, 64.3, 71.6, 68.2],
                [1.5, 1.6, 1.7, 1.65]])

# 创建第二个变量,它引用同一个数组
arrb = arr

# 输出 arrb 的内容
print(arrb)

# 修改 arr 的第一个元素
arr[0, 0] = 888.0

# 再次输出 arrb 的内容,可以看到它也发生了变化
print(arrb)
Text
[[57.5  64.3  71.6  68.2 ]
 [ 1.5   1.6   1.7   1.65]]
[[888.    64.3   71.6   68.2 ]
 [  1.5    1.6    1.7    1.65]]

从结果可以看到,修改 arr 中的元素后,arrb 的内容也发生了变化。这是因为 arrb 并不是 arr 的副本,而是指向同一个数据对象。通过其中一个变量所做的任何修改,都会通过另一个变量显示出来。

注意: 如果要在 NumPy 中创建数组的独立副本,应按如下方式使用 copy() 方法:

Python
arrc = arr.copy()

使用 copy() 时,会创建一个新的数据对象,它可以独立于原始对象进行修改。

在使用 NumPy 数组时,理解赋值与创建副本之间的区别非常重要。请看下面的示例:

Python
import numpy as np

# 创建二维数组
arr = np.array([[57.5, 64.3, 71.6, 68.2],
                [1.5, 1.6, 1.7, 1.65]])

# 尝试通过赋值创建“副本”
arrc = arr

在这种情况下,arrc 不是 arr 的独立副本。它只是指向同一个数组的新引用,因此对 arrc 的修改会影响 arr。要创建真正的副本,必须使用 copy() 方法:

Python
# 创建数组的独立副本
arrc = arr.copy()

现在,arrc 是 arr 的完全独立副本,对其中一个数组的修改不会影响另一个数组。不过需要记住,创建副本可能会消耗较多资源,因此应谨慎使用:

Python
# 修改原始数组中的元素
arr[0, 0] = 77.0

# 输出两个数组以进行比较
print('arr:', arr)
print('arrc:', arrc)

执行这段代码后可以看到,arr 发生了变化,但 arrc 保持不变。

注意: 只有在确实需要时才使用 copy() 方法,以避免不必要的内存和运行时间开销。

Python
import numpy as np  # 以别名 np 导入 NumPy 库

# 使用 np.array 函数创建二维数组 arr
arr = np.array([[57.5, 64.3, 71.6, 68.2],
                [1.5, 1.6, 1.7, 1.65]])

# 使用 copy 方法创建数组 arr 的独立副本
arrc = arr.copy()

# 输出数组 arrc 的内容
print('arrc', arrc)

# 将数组 arr 中位置 [0, 0] 的元素改为 77.0
arr[0, 0] = 77.0

# 输出修改后的数组 arr
print('arr', arr)

# 输出数组 arrc,以显示它没有发生变化
print('arrc', arrc)
Text
arrc [[57.5  64.3  71.6  68.2 ]
 [ 1.5   1.6   1.7   1.65]]
arr [[77.   64.3  71.6  68.2 ]
 [ 1.5   1.6   1.7   1.65]]
arrc [[57.5  64.3  71.6  68.2 ]
 [ 1.5   1.6   1.7   1.65]]

2.14.5 NumPy 数组中的轴

轴(axis)是使用 NumPy 数组时的一个基本概念。轴表示数组中的方向。因此,一维数组有一条轴(axis 0),二维数组有两条轴(axis 0 和 axis 1),依此类推。

多维数组可以通过合并一维数组构造出来。为此使用 np.stack() 方法,它可以沿指定轴把数组序列合并起来。该方法的语法如下:

Python
np.stack(数组序列, axis=轴编号)

其中,数组序列 是 NumPy 数组的列表或元组,轴编号 是需要沿其执行合并的轴索引。

假设我们有两个一维数组,希望把它们合并为一个二维数组,并让它们一上一下排列:

Python
import numpy as np  # 以别名 np 导入 NumPy 库

# 用于合并的假定一维数组
narray_w = np.array([57.5, 64.3, 71.6, 68.2])
narray_h = np.array([1.5, 1.6, 1.7, 1.65])

# 沿轴 0 合并数组(纵向)
arr = np.stack([narray_w, narray_h], axis=0)

# 输出结果
print(arr)
Text
[[57.5  64.3  71.6  68.2 ]
 [ 1.5   1.6   1.7   1.65]]

经常会遇到需要把多维数组转换为一维数组的情况。NumPy 提供了 np.ravel() 方法,它可以把多维数组“拉直”为一维形式。

Python
import numpy as np  # 以别名 np 导入 NumPy 库

# 用于合并的假定一维数组
narray_w = np.array([57.5, 64.3, 71.6, 68.2])
narray_h = np.array([1.5, 1.6, 1.7, 1.65])

# 沿轴 0 合并数组(纵向)
arr = np.stack([narray_w, narray_h], axis=0)

# 输出原始二维数组
print(arr)

# 将二维数组转换为一维数组
rarr = np.ravel(arr)

# 输出结果
print(rarr)
Text
[[57.5  64.3  71.6  68.2 ]
 [ 1.5   1.6   1.7   1.65]]
[57.5  64.3  71.6  68.2   1.5   1.6   1.7   1.65]

图:NumPy 数值计算入门

在使用 NumPy 数组的过程中,经常会遇到需要改变数组维度的情况。例如,可以把形状为 (2, 4) 的二维数组转换为长度为 8 个元素的一维数组。

在机器学习领域,经常需要沿数组的某一指定轴对元素求和。NumPy 为此提供了 np.sum 函数,它可以沿给定轴高效地对元素求和。

Python
import numpy as np  # 以别名 np 导入 NumPy 库

# 假设我们有如下二维数组:
arr = np.array([[57.5, 64.3, 71.6, 68.2],
                [ 1.5,  1.6,  1.7,  1.65]])

# 输出原始数组,便于观察
print(arr)

# 按列对数组元素求和(axis=0)
column_sum = np.sum(arr, axis=0)
print('按列求和:', column_sum, column_sum.shape)

# 按行对数组元素求和(axis=1)
row_sum = np.sum(arr, axis=1)
print('按行求和:', row_sum, row_sum.shape)
Text
[[57.5  64.3  71.6  68.2 ]
 [ 1.5   1.6   1.7   1.65]]
按列求和: [59.   65.9  73.3  69.85] (4,)
按行求和: [261.6    6.45] (2,)

按列求和: 结果是一个一维数组,其中每个元素表示原始数组相应列中数字的总和。该数组的维度为 (4,)。

按行求和: 结果同样是一个一维数组,其中每个元素表示相应行中数字的总和。该数组的维度为 (2,)。

沿数组的不同轴求和会降低数组的维度,并改变其形状。

2.14.7 按元素计算

在 Python 中,不同的数据结构支持不同类型的操作。用于保存数字集合的两种常见类型是列表(list)和 NumPy 数组(numpy.array)。它们在执行数学运算时的行为并不相同。

Python 列表可以使用 + 运算符连接在一起,这种操作称为拼接(concatenation)。拼接就是把一个列表附加到另一个列表的末尾,从而生成一个包含两个原始列表全部元素的新列表。

如果有两个列表 list_w 和 list_h,执行 list_w + list_h 会创建一个新列表,其中先放入 list_w 的所有元素,再放入 list_h 的所有元素。

Python
list_w = [57.5, 64.3, 71.6, 68.2, 59.8]
list_h = [1.5, 1.6, 1.7, 1.65]

listwh = list_w + list_h
print('拼接后的列表:', listwh)
Text
拼接后的列表: [57.5, 64.3, 71.6, 68.2, 59.8, 1.5, 1.6, 1.7, 1.65]

与列表不同,NumPy 数组支持按元素运算。这意味着,当在两个 NumPy 数组之间使用 + 运算符时,Python 会把这两个数组中对应位置的元素相加。

如果 narray_w 和 narray_h 是长度相同的 NumPy 数组,那么操作 narray_w + narray_h 会创建一个新的 NumPy 数组,其中每个元素都是两个原始数组对应元素的和。

Python
import numpy as np  # 以别名 np 导入 NumPy 库

narray_w = np.array([57.5, 64.3, 71.6, 68.2])
narray_h = np.array([1.5, 1.6, 1.7, 1.65])
narraywh = narray_w + narray_h
print('按元素相加后的数组:', narraywh)
Text
按元素相加后的数组: [59.   65.9  73.3  69.85]

当需要执行按元素乘法时,NumPy 数组尤其方便。例如,在把重量从千克转换为磅时,就经常会使用这种操作。

要把千克转换为磅,可以将以千克为单位的重量数组乘以数值 2.20462,因为 1 千克等于 2.20462 磅。

Python
import numpy as np  # 以别名 np 导入 NumPy 库

weights_kg = np.array([57.5, 64.3, 71.6, 68.2])
weights_lbs = weights_kg * 2.20462
print('以磅为单位的重量:', weights_lbs)
Text
以磅为单位的重量: [126.76565  141.757066 157.850792 150.355084]

因此,NumPy 数组为大规模数据的数学运算提供了强大而灵活的工具。

前面我们讨论过使用普通列表以及 zip() 等函数来执行按元素操作,但这种方法可能并不高效,尤其是在处理大量数据时。相比之下,可以使用 NumPy 的能力来显著加快计算。

Python
import numpy as np

# 使用普通列表计算两个列表元素之和的示例
list1 = [20, 30, 40, 50, 60]
list2 = [4, 5, 6, 2, 8]
sum_lists = [x + y for x, y in zip(list1, list2)]
print('列表求和:', sum_lists)

# 现在使用 NumPy 数组执行相同的操作
sum_arrays = (np.array(list1) + np.array(list2)).tolist()
print('NumPy 数组求和:', sum_arrays)
Text
列表求和: [24, 35, 46, 52, 68]
NumPy 数组求和: [24, 35, 46, 52, 68]

在使用 NumPy 的示例中,我们先把列表转换为 NumPy 数组,执行按元素加法,然后再把结果转换回列表。当处理大型数据集时,这种方式要高效得多。

2.14.8 创建多维数组的实用方法

在机器学习和数学计算等领域,多维数组是不可或缺的工具。这是因为在处理大量数据时,需要高效地生成、操作和处理数据。NumPy 库提供了一系列函数,用于处理不同维度的数据数组。

函数 np.arange(start, stop, step, dtype) 会创建一个一维数组,其中包含从 start 开始、到 stop 结束但不包含 stop 的数字序列,步长为 step。参数 dtype 指定数组元素的数据类型。如果未指定该参数,则数据类型会根据输入数据自动推断。

Python
np.arange(2, 8, 0.5, dtype=float)
Text
array([2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5, 7. , 7.5])

这段代码会返回一个实数数组,间隔为 0.5,从 2 开始,到小于 8 的数结束。

与 np.arange 不同,函数 np.linspace(start, stop, num) 会返回一个数组,其中包含从 start 到 stop 区间内均匀分布的 num 个数,并且包含区间的两个端点。当需要得到精确数量的元素时,它非常适合使用。

Python
np.linspace(1., 4., 6)
Text
array([1. , 1.6, 2.2, 2.8, 3.4, 4. ])

这段代码会生成一个包含 6 个数的数组,这些数在 1 到 4 之间均匀分布,并且包含 1 和 4。

使用这些函数可以简化为算法测试、数据可视化以及其他计算任务创建数组的过程。它们有助于避免手动输入数值,并能自动化数据预处理过程。

用指定值初始化数组。

Python
import numpy as np

a = np.array([1., 2., 3.])
a.fill(9.9)  # 用同一个值填充所有元素
print(a)
Text
[9.9 9.9 9.9]

首先创建一个包含三个元素 [1., 2., 3.] 的一维数组 a。随后使用 .fill(9.9) 方法,把数组中的所有元素都替换为 9.9。最终得到的数组由相同的数值 9.9 组成。

创建未初始化取值的数组。

Python
import numpy as np

x = np.empty((3, 4))  # 指定形状(维度)为 (3, 4),不初始化取值
print(x)
Text
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

函数 np.empty((3, 4)) 会创建一个大小为 3×4 的数组(3 行 4 列),但不会初始化其中的值。这意味着数组中可能包含该内存区域中原本已经存在的任意数值;具体显示结果取决于运行环境。

用零初始化数组。

Python
import numpy as np

x = np.zeros((6, 6))  # 用零初始化
print(x)
Text
[[0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]]

函数 np.zeros((6, 6)) 会创建一个大小为 6×6 的数组,其中每个元素都初始化为 0。

创建数组、用一初始化并继续乘法操作。

Python
import numpy as np

y = np.ones((2, 2)) * 2  # 2×2 数组,所有元素先为 1.0,再乘以 2
print(y)
Text
[[2. 2.]
 [2. 2.]]
教材插图:Python 基础
教材插图:Python 基础 — 2.14.8 创建多维数组的实用方法

函数 np.ones((2, 2)) 会创建一个大小为 2×2 的数组,其中每个元素初始化为 1.0。随后该数组乘以 2,结果中每个元素都等于 2。

把一个数组赋值给另一个数组的切片。

Python
import numpy as np  # 导入 NumPy 库

# 创建第一个 6×6 数组,并用零初始化
x = np.zeros((6, 6))
print("原始数组 x:")
print(x)

# 创建第二个 2×2 数组,并用一初始化
y = np.ones((2, 2))
print("\n数组 y:")
print(y)

# 将数组 y 的每个元素乘以 2
y *= 2
print("\n乘以 2 之后的数组 y:")
print(y)

# 把数组 y 赋值给数组 x 的一个切片。
# 这里选取第 3 行到第 5 行(不包括第 5 行),以及第 3 列到第 5 列(不包括第 5 列),
# 并把这个区域赋值为数组 y 的值。
x[3:5, 3:5] = y

print("\n把数组 y 赋值给切片之后的数组 x:")
print(x)
Text
原始数组 x:
[[0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]]

数组 y:
[[1. 1.]
 [1. 1.]]

乘以 2 之后的数组 y:
[[2. 2.]
 [2. 2.]]

把数组 y 赋值给切片之后的数组 x:
[[0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 2. 2. 0.]
 [0. 0. 0. 2. 2. 0.]
 [0. 0. 0. 0. 0. 0.]]

这些示例展示了 NumPy 库中用于创建和操作 Python 数据数组的多种函数。

2.14.9 使用外部包:TensorFlow

现代机器学习专家的工具箱中包含许多库和软件包,它们专门用于简化并加速可扩展机器学习模型的构建过程。TensorFlow 就是其中一种强大的工具,本课程也将使用它。

TensorFlow 是一个外部库,并不包含在 Python 的标准发行版中,因此在开始使用之前必须先安装。可以借助包管理系统 pip 来完成安装,在命令行或终端中执行以下命令:

Text
pip install tensorflow

执行该命令后,pip 系统会自动下载并安装 TensorFlow 的最新版本以及所有必要依赖项。

在学习和编写代码的过程中,你可能会遇到与缺少某些模块有关的错误。此时,需要以类似方式使用 pip 安装缺失的模块。

TensorFlow 成功安装后,可以通过标准导入命令将它导入 Python 脚本:

Python
import tensorflow as tf  # 为方便起见,将 TensorFlow 以别名 tf 导入

这样就可以访问 TensorFlow 的所有函数和类,用于构建和训练机器学习模型。

TensorFlow 还提供了 tensorflow.numpy(tfnp)模块,用于以类似 NumPy 的方式处理数组。它与 NumPy 兼容,并允许使用许多相同的函数和方法。创建数组的示例如下:

Python
import tensorflow.experimental.numpy as tfnp  # 导入用于数组操作的模块

# 创建一个 3×4、用零填充的数组
x = tfnp.zeros((3, 4))  # 使用 zeros 函数创建由零填充的数组

# 将 TensorFlow NumPy 数组转换为标准 NumPy 数组,便于输出
x_np = x.numpy()  # 转换为标准 NumPy 数组

# 使用 NumPy 的标准输出格式显示数组
print(x_np)

# 输出数组大小(应为 12,因为是 3 行乘以 4 列)
print(tfnp.size(x))
Text
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
12

在这个示例中,tfnp.zeros 创建了一个 3 行 4 列、用零填充的数组。随后我们把它输出到屏幕,并显示数组中的元素总数。

张量是一个数学对象,是标量、向量和矩阵概念向更高维度的推广。张量广泛用于物理学、工程学,当然也广泛用于机器学习和深度学习领域。张量表示多维数据数组,可以具有任意数量的维度,这些维度也称为秩。

张量的秩。 张量的秩(或阶)决定了张量的维度数量。例如,标量(单个数字)是 0 阶张量,向量(数字列表)是 1 阶张量,矩阵(数字表格)是 2 阶张量。秩高于 2 的张量通常称为多维张量。

张量的形状。 张量的形状描述了它在每个维度上的大小。例如,一个大小为 3×4 的矩阵具有形状 [3, 4],这表示该矩阵包含 3 行和 4 列。

张量的元素。 张量中的每个元素都是一个数,它可以是整数、实数,甚至在特定上下文中可以是更复杂的数据类型。张量元素通常通过数学运算共同处理。

张量可以用于表示多种数据:

• 标量数据(温度、价格等)可以表示为 0 阶张量。

• 向量数据(力、速度等)或时间序列可以表示为 1 阶张量。

• 图像可以表示为二维像素矩阵,因此可表示为 2 阶张量;如果考虑颜色通道,也可以表示为 3 阶张量。

• 视频数据中每一帧都是一幅图像,因此可以表示为 4 阶张量(时间、高度、宽度、颜色通道)。

Python
import tensorflow as tf

y = tf.random.normal([3, 4], mean=0.0, stddev=1.0)

函数 tf.random.normal 会生成指定形状的张量。这里 [3, 4] 表示 3 行 4 列,张量元素来自正态(高斯)分布。参数 mean 和 stddev 分别决定该分布的均值和标准差。在本例中,它们被设为 0.0 和 1.0,对应标准正态分布。

Python
x = tf.exp(y)

函数 tf.exp 会应用到张量 y 的每个元素上。指数函数 exp(x) 计算的是 e^x,其中 e 是自然对数的底,约等于 2.71828。这种变换常用于科学和工程计算。

Python
y_transposed = tf.transpose(y)

tf.transpose 函数会交换张量的维度。对于矩阵(二维张量)而言,这会把行变成列,把列变成行。转置经常用于把张量准备成适合某些数学运算的形状,例如矩阵乘法所要求的维度对齐。

Python
result_matrix = tf.matmul(x, y_transposed)

函数 tf.matmul 会执行两个张量的乘法,并把它们解释为矩阵。矩阵乘法不是按元素乘法;它会把结果矩阵中的每个元素计算为第一矩阵中相应行与第二矩阵中相应列的点积。为了能够执行矩阵乘法,第一矩阵的列数必须等于第二矩阵的行数。结果是一个新的矩阵,其大小由第一矩阵的行数和第二矩阵的列数决定。

Python
import tensorflow as tf

# 用标准正态分布中的值初始化一个 3×4 张量
y = tf.random.normal([3, 4], mean=0.0, stddev=1.0)

# 输出张量 y 的值
print('y')
print(y.numpy())

# 对张量 y 的每个元素求指数
x = tf.exp(y)  # 计算 e 的 y 中每个元素次幂
print(x.numpy())

# 转置张量 y
y_transposed = tf.transpose(y)
print(y_transposed.numpy())

# 将张量 x 与转置后的张量 y 相乘
result_matrix = tf.matmul(x, y_transposed)  # 计算矩阵乘积
print(result_matrix.numpy())
Text
y
[[ 0.71528995  2.1142166  -1.1875231  -0.16214192]
[-0.1809633  -0.21809964  1.1487513  -0.02445018]
[ 1.6564487   0.6891605   1.2565229   1.3516059 ]]
[[2.0447795  8.283093   0.30497572 0.8503205 ]
 [0.834466   0.8040453  3.1542516  0.9758463 ]
 [5.240667   1.9920425  3.5131845  3.863625  ]]
[[ 0.71528995 -0.1809633   1.6564487 ]
[ 2.1142166  -0.21809964  0.6891605 ]
 [-1.1875231   1.1487513   1.2565229 ]
 [-0.16214192 -0.02445018  1.3516059 ]]
[[18.474825  -1.847019  10.62796  ]
 [-1.6071612  3.273221   7.218715 ]
[ 3.161762   2.5584767 19.690228 ]]

TensorFlow 中的张量是处理数据的基本对象,旨在高效执行计算,尤其是在图形处理器(GPU)和分布式系统中。TensorFlow 的张量操作可以完成各种数据处理任务,包括数学运算、形状变换等。

NumPy 数组仍然是 NumPy 中的核心对象,而 NumPy 是 Python 科学计算的核心库。在机器学习项目中,NumPy 和 TensorFlow 经常配合使用,因为 NumPy 提供了许多有用的数据预处理操作,这些操作随后可以用于 TensorFlow。

为了在 TensorFlow 张量和 NumPy 数组之间交互,可以使用 tf.convert_to_tensor() 和 .numpy() 方法。这些方法可以轻松地把数据从 NumPy 数组转换为 TensorFlow 张量,也可以从 TensorFlow 张量转换回 NumPy 数组,从而提供便利的数据集成与兼容性。

下面看一个代码示例,其中使用 TensorFlow 操作张量,并使用 NumPy 执行点积计算:

Python
# 导入必要的库
import tensorflow as tf  # 导入 TensorFlow,用于处理张量
import numpy as np       # 导入 NumPy,用于处理 np 数组

# 在 TensorFlow 中创建张量
x = tf.constant([[1, 2], [3, 4]], dtype=tf.float32)  # 创建 2×2 张量 x
y = tf.constant([[5, 6], [7, 8]], dtype=tf.float32)  # 创建 2×2 张量 y

# 将 TensorFlow 张量转换为 np 数组
x_np = x.numpy()  # 将张量 x 转换为 np 数组
y_np_transposed = tf.transpose(y).numpy()  # 转置张量 y 并转换为 np 数组

# 在 np 数组之间执行点积
result = np.dot(x_np, y_np_transposed)  # 计算 x_np 与 y_np_transposed 的点积

# 输出点积结果
print("x 与转置后的 y 的点积:")
print(result)
Text
x 与转置后的 y 的点积:
[[17. 23.]
 [39. 53.]]

下面的代码展示了 TensorFlow 的一些基本功能,包括创建和操作张量、按元素运算、转置、矩阵乘法,以及与 NumPy 的交互。注意,在 TensorFlow 中,可以使用 .numpy() 方法把张量的值作为 NumPy 数组取出:

Python
# 步骤 1:导入必要的库
# 导入 TensorFlow 和 NumPy
import tensorflow as tf
import numpy as np

# 步骤 2:使用 TensorFlow 张量
# 张量是 TensorFlow 中用于存储和处理数值数据的基本数据类型

# 创建一个大小为 3×4、以零初始化的张量
x = tf.Variable(tf.zeros((3, 4)))
print("未初始化张量 X:\n", x.numpy())

# 用正态分布中的随机值初始化另一个张量
y = tf.random.normal((3, 4))
print("含随机值的张量 Y:\n", y.numpy())

# 步骤 3:按元素运算
# 对张量 y 的所有元素执行指数运算
x_exp = tf.exp(y)
print("x_exp = e^y:\n", x_exp.numpy())

# 步骤 4:转置与矩阵乘法
# 转置张量 y
y_transposed = tf.transpose(y)
print("转置后的 Y:\n", y_transposed.numpy())

# 执行 x_exp 与转置后的 y 的矩阵乘法
result_matrix = tf.matmul(x_exp, y_transposed)
print("x_exp 与转置后的 Y 的矩阵乘法结果:\n", result_matrix.numpy())

# 步骤 5:与 NumPy 交互
# 将张量转换为 np 数组,以执行兼容 NumPy 的操作
x_np = x_exp.numpy()
y_np_transposed = y_transposed.numpy()

# 使用 NumPy 执行点积
np_result = np.dot(x_np, y_np_transposed)
print("使用 NumPy 计算的点积结果:\n", np_result)
Text
未初始化张量 X:
 [[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

含随机值的张量 Y:
[[ 0.4931968  -0.31107786  1.0522738  -0.91028893]
[ 0.7813833  -0.3026102  -1.2939718   0.28406715]
[ 0.6851111  -1.1481444  -0.64575064 -0.14548281]]

x_exp = e^y:
 [[1.6375427  0.73265684 2.864156   0.40240794]
 [2.1844919  0.7388871  0.27417964 1.3285221 ]
 [1.9839922  0.3172249  0.52426887 0.8646048 ]]

转置后的 Y:
[[ 0.4931968   0.7813833   0.6851111 ]
 [-0.31107786 -0.3026102  -1.1481444 ]
[ 1.0522738  -1.2939718  -0.64575064]
 [-0.91028893  0.28406715 -0.14548281]]

x_exp 与转置后的 Y 的矩阵乘法结果:
[[ 3.227286   -2.5339868  -1.6273712 ]
 [-0.07329397  1.5059394   0.27794188]
[ 0.6444511   1.0214796   0.53070307]]

使用 NumPy 计算的点积结果:
[[ 3.227286   -2.5339868  -1.6273712 ]
 [-0.07329397  1.5059394   0.27794188]
[ 0.6444511   1.0214796   0.53070307]]

2.14.10 原地操作

在机器学习领域,我们经常需要处理大量数据。为了提高这类工作的效率,我们会尽量使用不会导致数据在计算机内存中过度移动的操作。这类操作称为原地操作。

Python
import tensorflow as tf

# 在 TensorFlow 中创建两个变量
x = tf.Variable([1, 2, 3], dtype=tf.float32)
y = tf.Variable([4, 5, 6], dtype=tf.float32)

# 打印操作前对象的标识符
print('操作前 id(x):', id(x))
print('操作前 id(y):', id(y))

# 标准加法操作,会创建新的张量
temp_y = y + x  # 在 TensorFlow 中,这会创建一个新的张量
# 如果希望继续把 y 作为变量使用,需要用结果创建一个新的变量
y = tf.Variable(temp_y)
print('标准加法操作后的 id(y):', id(y))

# 使用 assign_add 方法执行原地操作
y.assign_add(x)  # 这会把 x 原地加到 y 上,不改变 y 的标识符
print('使用 assign_add 进行原地操作后的 id(y):', id(y))

# 使用 assign_add 模拟 += 的原地效果
x.assign_add(y)  # 这会把 y 原地加到 x 上
print('使用原地加法操作后的 id(x):', id(x))
Text
操作前 id(x): 6079315920
操作前 id(y): 6069364304
标准加法操作后的 id(y): 6069363968
使用 assign_add 进行原地操作后的 id(y): 6069363968
使用原地加法操作后的 id(x): 6079315920

2.14.11 在 NumPy 多维数组中使用切片

可以使用切片从多维数组中读取指定的行和列。切片允许我们选择数组的一个子集,用于读取或修改。

Python
import numpy as np  # 导入 NumPy 库以处理数组

# 创建一个用于示例的二维浮点数组(矩阵)
x = np.array([
    [1.343, 3.358, 6.210, 6.438],
    [7.653, -2.504, 12.811, 1.550],
    [26.785, 0.124, 4.275, -2.182]
])

# 输出原始数组
print("原始数组 x:")
print(x)

# 从数组 x 中读取第二行和第三行
print("\n数组 x 的第二行和第三行:")
second_and_third_rows = x[1:3]  # 从第二行(索引 1)到第三行(索引 2)的切片
print(second_and_third_rows)

# 读取第二行中第二列到第三列的元素
print("\n第二行中第二列到第三列的元素:")
second_row_second_to_third_column = x[1:2, 1:3]  # 第二行以及索引为 1 和 2 的列
print(second_row_second_to_third_column)

# 将第二行第三列的值改为 88.0
x[1, 2] = 88.0  # 将索引为 [1, 2] 的元素赋值为 88.0

# 输出修改一个值后的数组
print("\n修改一个值后的数组 x:")
print(x)

# 将第二行中第二列到第三列的值都改为 88.0
x[1, 1:3] = 88.0  # 修改第二行中索引为 1 和 2 的列

# 输出修改切片值后的数组
print("\n修改一段切片值后的数组 x:")
print(x)

这个代码清单使用数组 x 作为示例,演示 NumPy 数组中的切片操作以及数组值的修改。

要提取数组 x 的第二行和第三行,可以使用以下代码:

Python
print(x)  # 为了直观显示,先输出整个数组
x[1:3]    # 获取第二行和第三行的切片

在这个示例中,x[1:3] 指的是数组 x 中索引为 1 和 2 的行。需要记住,Python 的索引从零开始,并且切片的右边界不包含在结果中。也就是说,x[1:3] 返回索引为 1 和 2 的行,但不会返回索引为 3 的行。

如果希望读取第二行中第二列到第三列的元素,可以这样做:

Python
x[1:2, 1:3]  # 选择第二行以及索引从 1 到 2 的列切片

这里,x[1:2, 1:3] 会创建一个切片,选择第二行(索引 1)以及索引为 1 和 2 的列。

要修改数组中的特定值,例如把第二行第三列的值改为 88.0,可以执行以下操作:

Python
x[1, 2] = 88.0  # 为元素赋予新值
print(x)        # 输出修改后的数组

这种赋值只会修改数组中的一个具体元素,不会改变其他元素。

最后,如果希望一次修改多个值,例如把第二行中第二列到第三列的所有值都改为 88.0,可以使用:

Python
x[1:2, 1:3] = 88.0  # 一次为多个元素赋予新值
print(x)            # 输出更新后的数组

这样会使第二行中第二列到第三列的元素都变为 88.0。

2.14.12 广播运算(broadcasting)

下面使用 NumPy 库在 Python 中详细说明广播运算(broadcasting)的概念。这个机制允许我们对原本尺寸并不完全兼容的数组执行算术运算。

NumPy 数组具有一个名为形状(shape)的属性,它是一个元组,用来表示数组每个维度的大小。例如,由函数 np.arange(6) 创建的数组 y 的形状为 (6,),这表示它是一个包含六个元素的一维数组。

在处理不同形状的数组时,在对它们执行运算之前,通常需要把它们转换为兼容的形状。为此可以使用 reshape 方法。在示例中,数组 x 首先通过 np.arange(24) 创建,其形状为 (24,),随后使用 x.reshape((4, 6)) 将其转换为形状为 (4, 6) 的二维数组。

现在,数组 x 和 y 的形状已经兼容:x 的形状为 (4, 6),y 的形状为 (6,),因此可以执行广播运算。NumPy 会自动沿第一个维度(索引为 0 的维度)“拉伸”数组 y,使其形状与 x 匹配。结果是,y 会被重复四次,以对应 x 中的四行。

应用广播机制后,表达式 x + y 会按元素执行加法:数组 y 的每个元素会与数组 x 每一行中对应位置的元素相加。

Python
import numpy as np

# 创建一维数组 y
y = np.arange(6)
print('y = ', y, 'y 的形状:', y.shape)

# 创建一维数组 x 并改变其形状
x = np.arange(24).reshape((4, 6))
print('X = \n', x, 'X 的形状:', x.shape)

# 使用广播机制执行加法
result = x + y
print('X + y = \n', result)

结果 result 将是一个二维数组,其中每一列都增加了数组 y 中相应的元素。

Python
# 以别名 np 导入 NumPy 库
import numpy as np

# 使用 arange 函数创建一维数组 y,该函数生成从 0 到 n-1 的数字序列
y = np.arange(6)

# 输出数组 y 及其形状
print('y = ', y, 'y 的形状:', y.shape)

# 创建与 y 相关长度的一维数组 x,但元素从 0 到 23
x = np.arange(24)

# 使用 reshape 方法改变数组 x 的形状,使其成为二维数组
# 维度为 4 行 6 列,这样就可以与数组 y 进行广播运算
x = x.reshape((4, 6))

# 输出数组 x 及其新的形状
print('X = \n', x, 'X 的形状:', x.shape)

# 执行 x 与 y 的加法。由于广播机制,y 会自动扩展到 x 的形状,
# 并进行按元素相加。
result = x + y

# 输出相加结果
print('X + y = \n', result)

如果在 Python 中运行这段代码,它会展示 NumPy 如何处理广播运算:在执行按元素操作时,较小的数组会被自动“扩展”,从而与较大的数组兼容。

数组乘法与形状变换。

Python
# 导入 NumPy 库
import numpy as np

# 假设数组 X 和 y 已在前面定义
# 输出数组 X 和 y 的形状
print('X 的形状:', X.shape, 'y 的形状:', y.shape)

# 计算数组 X 和 y 的点积
# np.dot() 函数返回数组对应元素乘积之和
print(np.dot(X, y))

# 使用 np.reshape() 方法把数组 X 改变为三维数组 Z
# 参数 (2, 3, 4) 指定数组的新形状
Z = np.reshape(X, (2, 3, 4))

# 输出新的三维数组 Z
print(Z)

点积(np.dot())。 操作 np.dot(X, y) 用于计算 X 与 y 之间的点积。在数学和线性代数中,点积意味着把两个数列中对应元素的乘积求和。

改变数组形状(reshape)。 reshape 方法可以在不改变数组数据的情况下改变数组的形状。在这个示例中,数组 X 从原始形状转换为形状为 (2, 3, 4) 的三维数组 Z。

用单一数值填充数组,以及对不同形状数组执行广播加法。

Python
# 使用 arange 和 reshape 创建二维数组 a
# 数组中填入从 0 到 11 的连续数字,并改变形状为 (3, 4)
a = np.arange(12).reshape(3, 4)

# 使用 fill 方法将整个数组 a 填充为数值 100
a.fill(100)

# 输出填充后的数组 a
print(a)

# 假设数组 Z 已在前面定义为三维数组
# 数组 a 与 Z 相加,用于演示广播机制
# 数组 a 会扩展到数组 Z 的形状,并执行按元素相加
print(Z + a)

# 创建并填充一维数组 a
a = np.arange(4)
a.fill(100)

# 输出一维数组 a
print(a)

# 将一维数组 a 与三维数组 Z 相加
# 这里展示 NumPy 的广播能力:形状不同的数组可以通过自动扩展较小数组来相加
print(Z + a)

下面给出一个带有详细注释的完整 Python 代码清单,演示使用 NumPy 进行数组操作:

Python
# 导入 NumPy 库
import numpy as np

# 创建一个包含从 0 到 5 的连续数字的一维数组 y
y = np.arange(6)

# 输出数组 y 的信息
print('y = ', y, 'y 的形状:', y.shape)

# 创建一个包含从 0 到 23 的连续数字的一维数组 x
x = np.arange(24)

# 将数组 x 改变形状为二维数组 (4, 6)
x = x.reshape((4, 6))

# 输出数组 x 的信息
print('x = ', x, 'x 的形状:', x.shape)

# 使用 np.dot 函数计算数组 x 和 y 的点积
# 需要注意,执行这种运算时数组的尺寸必须匹配
dot_product = np.dot(x, y)
print('x 和 y 的点积:', dot_product)

# 将数组 x 改变形状为三维数组 z,形状为 (2, 3, 4)
z = x.reshape((2, 3, 4))

# 输出三维数组 z
print('z =\n', z)

# 使用 arange 创建二维数组 a,并改变其形状为 (3, 4)
a = np.arange(12).reshape((3, 4))

# 将数组 a 填充为数值 100
a.fill(100)

# 输出填充后的二维数组 a
print('a =\n', a)

# 演示数组 a 与三维数组 z 相加时的广播机制
# 数组 a 会自动“扩展”到数组 z 的形状,以执行按元素相加
broadcasted_sum = z + a

# 输出相加结果
print('z 和 a 相加的结果:\n', broadcasted_sum)

# 创建一维数组 a,并用数值 100 填充
a = np.arange(4)
a.fill(100)

# 输出一维数组 a
print('a =', a)

# 将一维数组 a 与三维数组 z 相加
# NumPy 会自动“扩展”数组 a,以执行加法运算
broadcasted_sum = z + a

# 输出相加结果
print('z 和 a 相加的结果:\n', broadcasted_sum)
教材插图:Python 基础
教材插图:Python 基础 — 2.14.12 广播运算(broadcasting)

2.14.13 维度兼容规则

当计划在两个数组之间执行运算时,NumPy 需要比较它们的形状。这个比较从最后一个维度(最右侧维度)开始,然后向第一个维度(最左侧维度)推进。若要使两个维度被认为是兼容的,必须满足以下条件之一:

维度大小相等。

其中一个数组在该维度上的大小为 1。

教材插图:Python 基础
教材插图:Python 基础 — 2.14.13 维度兼容规则
教材插图:Python 基础
教材插图:Python 基础 — 2.14.13 维度兼容规则

如果这些条件中至少有一个不满足,NumPy 就会生成 ValueError 错误,指出形状不兼容。

为了说明广播运算,可以考虑几个示例:

如果把一维数组与二维数组相加,NumPy 会“扩展”一维数组,在二维数组的每一行上重复它。

当两个长度不同的一维数组相加时,如果其中一个数组的长度为 1,NumPy 会将它“拉伸”到另一个数组的长度。

如果给二维数组加上一个由单列组成的一维数组,NumPy 会沿每一列“扩展”这个一维数组,从而与二维数组执行加法。

图中直观展示了 NumPy 如何实现广播运算:它展示了原始数组、数组如何被“扩展”,以及加法运算后的最终结果。

应用这些规则可以简化许多类型的计算,尤其是在需要对不同形状的数组执行运算时。这一点在数组可能具有不同大小和形状的场景中尤为重要。

通过使用广播机制,我们可以减少编写冗余代码和循环的需求,使程序更加高效,也更容易理解。

在下面的代码中,我们会考察三个示例,展示 NumPy 广播运算的不同方面:

Python
import numpy as np

# 示例 1:一维数组与二维数组相加
# 创建一维数组
one_d_array = np.array([1, 2, 3])

# 创建二维数组
two_d_array = np.array([
    [0, 0, 0],
    [10, 10, 10],
    [20, 20, 20],
    [30, 30, 30]
])

# 尽管两个数组形状不同,广播运算仍允许将它们相加
broadcasted_sum_1 = one_d_array + two_d_array
print("示例 1:一维数组与二维数组相加")
print(broadcasted_sum_1)

# 示例 2:两个长度不同的一维数组相加
# 创建一个只含一个元素的一维数组
single_element_array = np.array([100])

# 广播运算会“扩展”较小的数组,使其可以与 one_d_array 相加
broadcasted_sum_2 = single_element_array + one_d_array
print("\n示例 2:两个长度不同的一维数组相加")
print(broadcasted_sum_2)

# 示例 3:由一列组成的一维/二维列数组与二维数组相加
# 创建一个表示列的数组
column_array = np.array([[0], [10], [20], [30]])

# 广播运算会将这一列沿水平方向“扩展”,以便执行加法
broadcasted_sum_3 = column_array + one_d_array
print("\n示例 3:单列数组与二维数组相加")
print(broadcasted_sum_3)

图:数组相加。

2.14.14 TensorFlow 数组与 NumPy 数组之间的转换

Python
# 以别名 np 导入 NumPy 库,用于处理数组
import numpy as np

# 导入 TensorFlow,用于处理张量和深度学习
import tensorflow as tf

# 创建一个包含 24 个数字的一维数组,从 0 到 23
x = np.arange(24)

# 将一维数组转换为尺寸为 4×6 的二维数组(矩阵)
x = x.reshape(4, 6)

# 输出 NumPy 矩阵 x 以进行检查
print("NumPy 矩阵 x:")
print(x)

# 将 NumPy 数组 x 转换为 TensorFlow 张量
tfx = tf.convert_to_tensor(x, dtype=tf.float32)

# 输出张量 tfx 以进行检查
print("\nTensorFlow 张量 tfx:")
print(tfx)

# 将 TensorFlow 张量转换回 NumPy 数组
npa = tfx.numpy()

# 检查 npa 是否确实为 NumPy 数组
print("\n转换回 NumPy 的数组 npa:")
print(npa)
print("npa 的类型:", type(npa))
Text
NumPy 矩阵 x:
[[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]
 [12 13 14 15 16 17]
 [18 19 20 21 22 23]]

TensorFlow 张量 tfx:
tf.Tensor(
[[ 0.  1.  2.  3.  4.  5.]
 [ 6.  7.  8.  9. 10. 11.]
 [12. 13. 14. 15. 16. 17.]
 [18. 19. 20. 21. 22. 23.]], shape=(4, 6), dtype=float32)

转换回 NumPy 的数组 npa:
[[ 0.  1.  2.  3.  4.  5.]
 [ 6.  7.  8.  9. 10. 11.]
 [12. 13. 14. 15. 16. 17.]
 [18. 19. 20. 21. 22. 23.]]
npa 的类型: <class 'numpy.ndarray'>

这个代码清单展示了创建 NumPy 数组、将其转换为 TensorFlow 张量,并再转换回 NumPy 数组的过程。

Python
# 导入所需库
import numpy as np
import tensorflow as tf

# 创建包含从 0 到 23 的连续数字的 NumPy 数组
npa = np.arange(24).reshape(4, 6)  # 将数组形状改为 4×6

# 输出原始 NumPy 数组
print("原始 NumPy 数组:")
print(npa)

# 将 NumPy 数组转换为 TensorFlow 张量
tfy = tf.convert_to_tensor(npa, dtype=tf.float32)

# 输出 TensorFlow 张量
print("\n转换得到的 TensorFlow 张量:")
print(tfy)

# 将 TensorFlow 张量转换回 NumPy 数组
npa_from_tfy = tfy.numpy()

# 输出由 TensorFlow 张量得到的 NumPy 数组
print("\n转换回 NumPy 数组:")
print(npa_from_tfy)

# 为了检查原始数组与获得的 NumPy 数组是否相同,
# 可以使用 np.array_equal() 函数
print("\nNumPy 数组是否完全相同:", np.array_equal(npa, npa_from_tfy))
Text
原始 NumPy 数组:
[[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]
 [12 13 14 15 16 17]
 [18 19 20 21 22 23]]

转换得到的 TensorFlow 张量:
tf.Tensor(
[[ 0.  1.  2.  3.  4.  5.]
 [ 6.  7.  8.  9. 10. 11.]
 [12. 13. 14. 15. 16. 17.]
 [18. 19. 20. 21. 22. 23.]], shape=(4, 6), dtype=float32)

转换回 NumPy 数组:
[[ 0.  1.  2.  3.  4.  5.]
 [ 6.  7.  8.  9. 10. 11.]
 [12. 13. 14. 15. 16. 17.]
 [18. 19. 20. 21. 22. 23.]]

NumPy 数组是否完全相同: True

这段代码展示了如何把 NumPy 数组转换为 TensorFlow 张量,在 TensorFlow 环境中对其进行操作,然后再把张量转换回 NumPy 数组。当需要在同一个工作流程中结合 NumPy 与 TensorFlow 操作时,这非常有用。

2.14.15 在 NumPy 中提取子数组

NumPy 数组的另一个特点是,它们很容易被划分为子数组。

Python
print(bmi[bmi > 25])  # 只输出 BMI 大于 25 的值
Text
[25.556 25.117 25.051]

这段代码展示了使用布尔索引筛选数组元素的能力。这里的 bmi 是一个假定的 NumPy 数组,其中包含不同个体的身体质量指数(BMI)值。表达式 bmi > 25 会创建一个新的数组,其中包含与原始数组 bmi 每个元素对应的布尔值(True 或 False):当元素值大于 25 时,对应位置为 True。当这个布尔表达式放在方括号中使用时,NumPy 只返回原始数组 bmi 中对应布尔值为 True 的元素,也就是所有大于 25 的 BMI 值。

Python
import numpy as np  # 导入 NumPy 库

# 创建包含测试数据的 bmi 数组
# 为了演示,使用 20 到 30 之间的随机数
bmi = np.random.uniform(20, 30, 10)

# 输出原始 bmi 数组
print("原始 BMI 数组:", bmi)

# 使用布尔索引筛选大于 25 的值
# 其工作方式是创建一个布尔数组,在其中检查每个元素是否满足 > 25 的条件,
# 然后用这个布尔数组从原始数组中选择元素。
filtered_bmi = bmi[bmi > 25]

# 输出筛选后的值
print("筛选出的 BMI > 25 的值:", filtered_bmi)

这段代码先导入 NumPy,使用随机值创建示例数组 bmi,然后使用布尔索引筛选并输出数组中大于 25 的值。

Python
import numpy as np  # 导入 NumPy 库

# 创建包含测试数据的 bmi 数组
# 为了演示,使用从 20 到 30 范围内的固定值数组
bmi = np.array([21.3, 24.5, 27.9, 30.1, 23.4, 25.6, 27.4, 22.0, 28.5, 26.1])

# 输出原始 bmi 数组
print("原始 BMI 数组:", bmi)

# 使用布尔索引筛选大于 25 的值
# 创建一个布尔数组,其中每个元素都被检查是否满足 > 25 的条件
# 这个布尔数组随后用于从原始数组中选择元素
filtered_bmi = bmi[bmi > 25]

# 输出筛选后的值
print("筛选出的 BMI > 25 的值:", filtered_bmi)
Text
原始 BMI 数组: [21.3 24.5 27.9 30.1 23.4 25.6 27.4 22.  28.5 26.1]
筛选出的 BMI > 25 的值: [27.9 30.1 25.6 27.4 28.5 26.1]

2.14.16 NumPy 与通用函数(ufunc)

通用函数(universal functions,简称 ufunc)是 NumPy 库中的一种机制,用于对多维数组中的元素执行按元素操作。这些函数具有几个重要特征。

按元素处理。 ufunc 会分别作用于数组中的每个元素,因此可以在不显式使用循环的情况下高效执行向量化操作。

支持广播(broadcasting)。 这意味着 ufunc 可以正确处理不同大小的数组,通过扩展较小数组以匹配较大数组的形状。

类型转换。 ufunc 支持对数组元素的数据类型进行自动转换,以便执行运算。

标准函数。 通常,ufunc 实现的是标准的数学与逻辑运算。

在 NumPy 中,通用函数是 numpy.ufunc 类的实例。许多内置的 NumPy 函数都实现为 ufunc,并且使用已编译的 C 语言编写,因此具有很高的性能。

ufunc 的类型。

基本 ufunc:处理标量值。

广义 ufunc:处理子数组(向量、矩阵等),并在不同维度上应用操作。

ufunc 示例。

exp(x, /[, out, where, casting, order, ...]):计算输入数组所有元素的指数。

log(x, /[, out, where, casting, order, ...]):逐元素计算数组每个元素的自然对数。

NumPy 提供了使用 frompyfunc 函数创建自定义 ufunc 的能力。这样可以把已有的 Python 函数封装为通用函数,从而扩展 NumPy 库的能力。

为了更深入地学习通用函数及其在 NumPy 中的应用,建议查阅 NumPy 文档。

2.14.17 NumPy 数组与向量/矩阵

NumPy 数组是 NumPy 库的基本构造块,并且广泛用于计算。理解线性代数中传统向量和矩阵概念与 NumPy 数组行为之间的差异,有助于高效编程和调试代码。

NumPy 数组的特点。

形状灵活。 与线性代数中严格的一维向量和二维矩阵不同,NumPy 数组可以具有任意数量的维度。这使它能够表示更加复杂的数据结构。

类型同质。 在一个 NumPy 数组中,所有元素都必须具有同一种类型。这不同于其他编程语言中的某些数据结构,后者的元素可能具有不同类型。

运算向量化。 NumPy 数组上的操作通常按元素执行,并且可以通过向量化提高性能,这不同于数学意义上对向量和矩阵执行的某些运算。

维度与形状。 NumPy 数组的维度决定其形状。例如,一维数组类似于向量,二维数组类似于矩阵。不过,在 NumPy 中,这些结构统一作为 ndarray 处理。

NumPy 主要使用数组结构。NumPy 数组的行为与我们在线性代数中学习的传统向量和矩阵概念相似,但同时又存在很大差异。下面对此作进一步说明。

与向量和矩阵这样简单的一维、二维结构不同,NumPy 数组默认最多可以有 32 个维度,而且这个数量可以更改。这使它们可以用于需要处理高维数据的复杂任务。

由于具有多维性和灵活性,NumPy 数组非常适合处理复杂机器学习模型中的数据,因为这些模型常常需要处理多维数据集。

一维 NumPy 数组类似于线性代数中通常所说的向量。区别在于,一维数组并不区分行向量或列向量。它只是一个形状为 (n,) 的一维数组,其中 n 是数组长度。在大多数情况下,它的行为像行向量,但并不完全等同。例如,transpose() 函数不会影响它。这是因为 transpose() 函数交换的是具有两个或更多轴的数组的轴。

在线性代数中,列向量在 NumPy 中应被视为只有一列的二维数组这一特殊情况。可以通过给一维数组添加一个额外轴,创建类似于线性代数中列向量的数组。请看下面的示例。

首先,考察如何在 NumPy 中创建一维数组、对其转置,以及给它添加新轴以转换为二维数组:

Python
import numpy as np  # 导入 NumPy 库

# 创建包含三个元素的一维数组
a = np.array([1, 2, 3])

# 打印数组、其形状、转置后的数组及其形状
# 对一维数组而言,转置不会改变数组形状
print("原始数组:", a)
print("原始数组的形状:", a.shape)
print("转置后的数组:", a.T)
print("转置后数组的形状:", a.T.shape)

# 添加一个新轴,将数组转换为二维数组
# 这样可以创建列向量
an = a[:, np.newaxis]

# 打印新的二维数组及其形状
print("\n二维列向量:", an)
print("列向量的形状:", an.shape)

# 现在可以对二维数组进行转置
# 转置会把列向量变成行向量
print("\n转置后的二维数组(行向量):", an.T)
print("行向量的形状:", an.T.shape)
Text
原始数组: [1 2 3]
原始数组的形状: (3,)
转置后的数组: [1 2 3]
转置后数组的形状: (3,)
二维列向量: [[1]
 [2]
 [3]]
列向量的形状: (3, 1)
转置后的二维数组(行向量): [[1 2 3]]
行向量的形状: (1, 3)

在 NumPy 中,可以使用 np.array() 函数创建数组。一维数组本质上是一个元素列表。在我们的示例中,数组 a 被创建为 [1, 2, 3]。

Python
a = np.array([1, 2, 3])

当打印数组 a、它的形状(shape)以及其转置版本 a.T 的形状时,会看到如下结果:

Python
print(a, a.T, a.shape, a.T.shape)

输出中的 [1 2 3] 是原始数组,(3,) 是它的形状,表示数组有 3 个元素且是一维数组。在 NumPy 中,一维数组没有行和列的区别,因此转置不会影响它,a.T 的形状仍然是 (3,)。

现在尝试通过添加一个新轴,把一维数组变为二维数组。可以使用切片和 np.newaxis 完成这一点。切片 : 表示取该维度中的所有元素,而 np.newaxis 会添加一个新轴,使数组成为二维数组。

Python
an = a[:, np.newaxis]

随后打印新数组 an 及其形状:

Python
print(an, an.shape)

输出中的 [[1] [2] [3]] 展示了新的二维数组,(3, 1) 是它的形状,表示数组有 3 行 1 列,因此它就是一个列向量。

现在我们已经有了二维数组,就可以使用 .T 对其转置。转置会把行变成列,也会把列变成行。

Python
print(an.T, an.T.shape)

输出中的 [[1 2 3]] 是转置后的数组,原先的行/列关系被交换。形状 (1, 3) 表示现在有 1 行 3 列,因此它是一个行向量。

首先考察如何从一维数组创建二维数组。假设有一个值为 [1, 2, 3] 的一维数组 a。我们希望将其转换为二维数组,以便使用面向矩阵的操作。

Python
import numpy as np

# 原始一维数组
a = np.array([1, 2, 3])

# 在数组 a 的末尾添加一个新维度
a1 = a.reshape(a.shape + (1,))
print(a1, a1.shape)  # 输出二维数组及其形状 (3, 1)
Text
[[1]
 [2]
 [3]] (3, 1)

在上述代码中,reshape 方法通过添加一个维度来改变数组形状。我们使用 a.shape + (1,),把当前数组形状 (3,) 增加一个维度。

现在考察另一种添加维度的方法:使用对象 None。

Python
# 添加一个新维度,而不显式指定元素
aN = a[:, None]
print(aN, aN.shape)  # 与前面的输出类似:二维数组及其形状 (3, 1)

这里,a[:, None] 使用切片语法和 None,告诉 NumPy 添加一个新维度。这等价于使用 np.newaxis,后面我们也会看到这种写法。

也可以不仅在数组末尾添加维度,还可以在数组开头添加维度。

Python
import numpy as np

# 在数组 a 的开头添加一个新轴
a0 = a[np.newaxis, :]
print(a0, a0.shape)  # 输出行向量形式的二维数组 (1, 3)

我们使用 np.newaxis 添加了一个新维度。在这个示例中,新维度添加在开头,因此数组形状变为 (1, 3),表示一个具有一行三列的二维数组,即行向量。

Python
# 以别名 np 导入 NumPy 库
import numpy as np

# 创建包含三个元素的一维数组
a = np.array([1, 2, 3])

# 在数组 a 的末尾添加一个新维度,将其变为二维数组
# 这是通过 reshape 方法改变数组形状完成的
a1 = a.reshape(a.shape + (1,))

# 输出结果及得到的数组形状
# 预期输出:具有三行一列的二维数组
print("在数组末尾添加维度:")
print(a1, a1.shape)

# 使用对象 None 添加一个新维度
# 这等价于在数组末尾添加一个新轴
aN = a[:, None]

# 输出结果及得到的数组形状
# 预期输出:与前面类似,为具有三行一列的二维数组
print("\n使用 None 添加维度:")
print(aN, aN.shape)

# 使用 np.newaxis 在数组 a 的开头添加一个新轴
# 这会创建一个行向量
a0 = a[np.newaxis, :]

# 输出结果及得到的数组形状
# 预期输出:一行三列的二维数组,即行向量
print("\n在数组开头添加轴:")
print(a0, a0.shape)
Text
在数组末尾添加维度:
[[1]
 [2]
 [3]] (3, 1)

使用 None 添加维度:
[[1]
 [2]
 [3]] (3, 1)

在数组开头添加轴:
[[1 2 3]] (1, 3)

2.14.18 汉克尔矩阵

汉克尔矩阵是一种在副对角线(从右向左延伸的对角线)上元素保持常数的矩阵。在这里,列数组与行数组相加会得到一个矩阵,其中每个元素都是原始一维数组 a 中相应元素之和。

Python
# 以别名 np 导入 NumPy 库
import numpy as np

# 创建包含三个元素的一维数组
a = np.array([1, 2, 3])

# 使用对象 None 添加一个新维度
aN = a[:, None]
print("\n使用 None 添加维度:")
print(aN, aN.shape)

# 使用 np.newaxis 在数组 a 的开头添加一个新轴
a0 = a[np.newaxis, :]
print("\n在数组开头添加轴:")
print(a0, a0.shape)

# 应用一个创建汉克尔矩阵的小技巧:将 aN 与 a0 相加
# 这里使用 broadcasting,即 NumPy 允许对形状不同的数组执行算术运算的机制。
# 结果会得到一个汉克尔矩阵。
print("\n创建汉克尔矩阵的一个有趣技巧:")
hankel_matrix = aN + a0
print(hankel_matrix)
Text
使用 None 添加维度:
[[1]
 [2]
 [3]] (3, 1)

在数组开头添加轴:
[[1 2 3]] (1, 3)

创建汉克尔矩阵的一个有趣技巧:
[[2 3 4]
 [3 4 5]
 [4 5 6]]

虽然二维 NumPy 数组与线性代数中的矩阵相似,但二者之间存在关键差异,理解这些差异对正确应用它们非常重要。其中,乘法运算的差异尤为重要:点积可通过 np.dot() 或运算符 @ 执行,而运算符 * 用于按元素乘法。有些操作可能会改变数组维度,因此需要特别谨慎,并理解这些变化会如何影响数据分析。此外,NumPy 中也存在一个矩阵类,它模拟线性代数矩阵的行为,但由于可能逐渐被弃用,并不推荐使用。

2.15 生成图形

Python 具有创建图形的能力,因为有多种可用的可视化模块。作为示例,下面考察如何创建一个包含不同大小和颜色的随机圆点的演示图。

首先导入所需模块:

Python
import numpy as np  # 用于处理数组的模块
import matplotlib.pyplot as plt  # 用于创建图形的模块

下一步生成一组数据:

Python
n = 80  # 点的数量
x = np.random.rand(n)  # 随机生成的 X 轴坐标
y = np.random.rand(n)  # 随机生成的 Y 轴坐标
colors = np.random.rand(n)  # 为每个点随机生成的颜色
areas = np.pi * (18 * np.random.rand(n))**2  # 随机生成的圆面积,半径范围约为 0 到 18

现在使用生成的数据创建图形:

Python
plt.scatter(x, y, s=areas, c=colors, alpha=0.8)  # 创建带有圆点的散点图
plt.show()  # 显示图形

在这个图中,每个点都是一个圆,其大小和颜色随机生成。参数 alpha 控制圆的透明度,在圆点重叠时可以增加视觉层次感。

Python
# 导入所需库
import numpy as np  # 用于处理数组和执行数学运算
import matplotlib.pyplot as plt  # 用于数据可视化

# 生成用于图形的数据
n = 80  # 圆的数量
x = np.random.rand(n)  # 圆心的随机 X 坐标
y = np.random.rand(n)  # 圆心的随机 Y 坐标
colors = np.random.rand(n)  # 圆的随机颜色
areas = np.pi * (18 * np.random.rand(n))**2  # 圆的随机半径转换为面积

# 创建图形
plt.scatter(x, y, s=areas, c=colors, alpha=0.8)  # 使用 scatter 函数绘制圆
# 其中 x 和 y 是圆心坐标,s 是圆的大小(以面积给出),
# c 是圆的颜色,alpha 是圆的透明度。

plt.show()  # 显示图形

图:随机生成、用不同颜色填充的圆形区域。

Python
import numpy as np
import matplotlib.pyplot as plt

# 导入 numpy 库用于处理数组,导入 matplotlib.pyplot 用于数据可视化。
n = 80  # 要生成的点数

# 为 n 个点生成随机坐标。
x = np.random.rand(n)  # X 轴随机坐标
y = np.random.rand(n)  # Y 轴随机坐标

# 为点生成随机颜色和大小。
colors = np.random.rand(n)  # 每个点的随机颜色
areas = np.pi * (18 * np.random.rand(n))**2  # 点的随机大小,表示圆的面积

# 使用生成的数据创建散点图。
plt.scatter(x, y, s=areas, c=colors, alpha=0.8)  # 's' 表示大小,'c' 表示颜色,'alpha' 表示点的透明度
plt.show()  # 显示散点图

# 生成用于绘制曲线的数据。
x = range(1000)  # 生成从 0 到 999 的数列
y = [i ** 2 for i in x]  # 创建 y 值列表,其中每个值是对应 x 值的平方

# 绘制函数 y = x^2 的图形
plt.plot(x, y)  # 'plot' 用于绘制折线图
plt.show()  # 显示图形

# 生成用于直方图的数据。
x = np.linspace(0, 1, 1000)**1.5  # 在 0 到 1 之间创建 1000 个点,并将值提升到 1.5 次方以改变分布

# 绘制 x 值分布的直方图。
plt.hist(x)  # 'hist' 用于绘制直方图
plt.show()  # 显示直方图

2.16 代码效率评估

可以用两种方式评估代码性能。下面给出一些典型代码示例,读者可以用它们来分析自己程序的计算效率。这些示例使用 time 和 numpy 模块,展示标准 Python 操作与 NumPy 库中优化操作之间的性能差异。

Python
import time
import numpy as np

# 创建一个大型整数列表
g = list(range(10_000_000))

# 将列表转换为数据类型为 float64 的 NumPy 数组
q = np.array(g, 'float64')

# 测量使用 Python 内置函数 sum 执行求和所需的时间
start = time.process_time()  # 记录开始时间
sg = sum(g)  # 对列表元素求和
t_elapsed = (time.process_time() - start)  # 计算耗时
print(f'总和:{sg}, 耗时:{t_elapsed}')

# 测量使用 np.sum 对 NumPy 数组执行求和所需的时间
start = time.process_time()  # 再次记录开始时间
sq = np.sum(q)  # 对 NumPy 数组元素求和
t_elapsed = (time.process_time() - start)  # 计算耗时
print(f'总和:{sq}, 耗时:{t_elapsed}')

# 使用 %%timeit 魔法命令自动测量单元格中代码的执行时间
# %%timeit
# sg = sum(g)  # 这一行展示如何在 Jupyter 等交互环境中使用 timeit
Text
总和:49999995000000, 耗时:0.030974000000000057
总和:49999995000000.0, 耗时:0.006296999999999997

在这个代码示例中,我们先导入所需模块,然后创建一个大型整数列表,并将其转换为 NumPy 数组。随后使用标准函数 sum 和函数 np.sum 分别测量对列表和数组元素求和所需的时间。结果展示了标准 Python 操作与 NumPy 中优化操作之间的性能差异。代码中的注释有助于理解每一步的作用以及如何解释输出。

2.17 结论

掌握 Python 编程语言、其模块以及基本编程技术的基础知识之后,我们已经进入这样一个阶段:可以应用这些知识来解决计算问题,并学习机器学习技术。这个过程不仅能够帮助实现复杂的计算任务,也会促进 Python 编程技能的逐步提升。

需要强调的是,实践经验以及在真实项目中持续应用所学知识,是深入掌握这门语言及其能力的关键因素。经常解决不同复杂程度的问题,不仅可以巩固基础知识,还可以接触更高级的编程技术和方法。

自测

哪一项最能概括“Python 基础”这一章的重点?

在机器学习中,理论定义需要通过数值示例和可视化进行检验。

哪些做法有助于巩固本章内容?

参加测试