Logo
Section 1 / 5

元组、列表和字符串技巧

学习如何使用元组、列表以及与字符串相关的技巧来提升我们的代码质量。

检查点

元组、列表和字符串技巧

学习如何使用元组、列表以及与字符串相关的技巧来提升我们的代码质量。

Python 包含一套强大的数据类型和数据结构。我们将在本章中多次回顾这一说法及其变体。为数据选择合适的表示方式可能至关重要。在最佳情况下,错误的数据类型选择可能导致显著的性能下降;在最坏情况下,它可能引发逻辑错误,导致结果不正确。

本章提供关于传统和“冷门”数据结构与类型的技巧,包括标准容器、计数器以及各种数值类型。我们将学习如何:

  • 处理复数、有理数和无穷大
  • 轻松创建模块
  • 转换列表
  • 统计元素数量
  • 识别元组的不可变性

本章还包含关于高级类设计的建议(类属性和自定义对象打印输出)。

构建单元素元组

创建单元素元组比较困难。我们先尝试像单元素列表和单元素集合那样创建一个单元素元组:

1print(type([0])) 2print(type({0})) 3print(type((0)))

结果并不是元组,而是一个整数——第一个也是唯一一个元素本身。这是因为 Python 中的括号有多种用途:它们参与创建元组(与逗号配合使用)、定义函数、定义子类、调用函数,以及改变表达式的计算顺序等。在最后一个例子中,外层括号用于调用函数,内层括号用于改变计算顺序!

要让 Python 知道正在创建一个元组,只需在第一个元素后添加一个逗号。真正构建元组的是逗号,而不是括号。

1print(type((0,))) 2a = 0, # 只是一个逗号! 3print(type(a))

也许内层括号其实是多余的。我们能否将其去掉?让我们计算一下单元素元组的长度。

1print(len(0,))

错误的原因是逗号也有多种用途:它既用于创建元组,也用于函数调用中的参数分隔和函数定义中的参数分隔。在上面的例子中,Python 将其视为第二种用途,把 0 当作第一个参数处理了。我们或许可以避免使用单元素元组,可以用一个不可扩展的单一标量变量来替代单元素容器。

使用原始字符串提高可读性

原始字符串以字母 r 或 R 作为前缀,位于引号之外。在原始字符串中,转义字符——反斜杠 \——不再具有特殊含义。它不再是转义字符,而只是一个普通的反斜杠。因此,所有特殊组合字符(如 \n 和 \v)都失去了特殊意义,变成两个字符的字符串:

1print(r'\n' + '\n' + r'\n', len('\v'), len(r'\v'))

考虑一个包含许多反斜杠的字符串,比如正则表达式。在普通(非原始)字符串中,每个反斜杠都必须用另一个反斜杠转义,这会产生大量难以辨认的反斜杠,就像这段文字一样:

1regex = '\\n\\\.\\\\n' 2print(regex)

注意:上面的正则表达式匹配的是一个由换行符、一个字面反斜杠、一个句点、另一个字面反斜杠和另一个换行符组成的字符串。原始字符串能显著提升可读性:

1regex = r'\n\\.\\n' 2print(regex)

如果我们在原始字符串中需要保留某个特殊转义字符怎么办?这无法直接实现。要么改用普通字符串,要么通过字符串拼接将原始字符串与普通字符串结合:

1mixed_string = '\n' + r'\\.' + '\n' 2print(mixed_string)

最后,原始字符串末尾的反斜杠仍然具有转义作用。r'\' 并不是一个单独的反斜杠,而是一个未完成的字符串。

1print(r'\')

解包列表和元组

我们可以使用索引操作符从序列(如元组、列表或字符串)中提取单个元素:

1seq = 1, 2, 3, 4 2x = seq[0] 3y = seq[1] 4z1 = seq[2] 5z2 = seq[3]

另一种方法是使用多重赋值(也称为同时赋值)。左侧的项目数量必须与序列大小一致。

1x, y, z1, z2 = seq 2print(x, ", ", y, ", ", z1, ", ", z2)

多重赋值在序列元素数量已知且不变时效果最佳,因为我们需要在赋值左侧列出所有变量,且这些变量必须与序列元素一一对应。

不过,这里有一个问题:我们可以使用星号(*)操作符来收集序列中剩余的所有元素,即使我们不确定序列的大小。目前只需知道序列至少有两个元素即可。以下语句将序列解包为变量 x(第一个元素)、y(第二个元素)和 z(剩余元素组成的列表)。如果序列只有两个元素,则 z 为空列表:

1x, y, *z = seq 2print(x, y, z, sep=' | ')

左侧带星号的变量不必位于末尾,它可以出现在中间甚至开头。但不能使用多个星号,否则无法匹配:

1*x, y, z = seq 2print(x, y, z, sep=' | ') 3*x, y, z = seq[:2] # 取前两个元素 4print(x, y, z, sep=' | ') 5# 再看一个字符串示例: 6start, *rest, end = 'Hello, world' 7print(start, ''.join(rest), end, sep=' | ')

打印列表

如果我们想以人类可读的方式打印 Python 列表——去除方括号、逗号和引号——print(l) 并不是理想方案:

1l = list('hello') + list(range(5)) 2print(l)

我们需要一种方法:将列表中的每个元素转换为字符串(使用 str()),然后用我们选择的分隔符(例如空格)连接这些字符串,使用 str.join() 方法。列表推导式非常适合这个任务:

让我们在列表的每个字符后添加一个空格。

1print(' '.join(str(x) for x in l))

看起来像列表推导式的方括号缺失并非错误。实际上,我们使用的是推导式表达式,以更清晰地说明这一常被忽视的机制。

Python 还有许多其他推导式表达式。集合推导式用花括号 {} 包裹,几乎与列表推导式相同,只是结果是一个去重后的集合。虽然我们也可以对列表推导式应用 set() 来达到相同效果,但集合推导式效率高得多。

我们来试试加方括号的情况:

1print(' '.join([str(x) for x in l]))

如果列表是递归的——包含其他复合项,如列表、元组、集合,或它们的任意组合——我们可以将打印与扁平化结合起来。

扁平化列表

扁平化列表是指将其转换为每个元素均为简单类型(如数字、字符串、布尔值或 None)的列表。如果列表只包含简单元素,它已经是扁平的。某些函数或其他表达式会产生嵌套列表,例如列表的列表。列表的嵌套深度可能超过两层(比如列表的列表的列表)。

在下面的代码中,我们对一组字符进行扁平化处理:

1def flatten(seq): 2 return ' '.join((flatten if isinstance(x, (list, tuple, set)) \ 3 else str)(x) for x in seq) 4 5letters = list('hello') 6print(flatten([[letters], [letters, 1, 2, (3,)]]))

一些重要说明

  1. letters 比“列表”这样的名字更适合用作临时列表的名称。

Python 的内置函数不受恶意篡改保护。最常见的篡改方式是重新定义其标识符。当我们希望给新变量起一个简洁明了的名字时,就可能发生这种情况。例如,为什么不叫一个新 list?语句 list = [2, 4, 6, 8, 10] 在我们或他人重用代码时不试图通过类构造器创建新列表的情况下是可行的。但之后 list 不再是构造器;它甚至不再是函数。它只是一个列表,正如我们最初想要的一样。

  1. list() 函数(或列表构造器)会将任何内容转换为列表,包括字符串。

  2. isinstance() 函数比 type() 函数更适合作为类型检查工具。

type() 和 isinstance() 的主要区别在于:前者报告参数的最直接类,而后者检查参数是否属于某个类(直接或间接)。

好了!当需要判断对象是否属于某个类或其任何父类时,应使用 isinstance(),因为后续操作依赖于父类可能具有的某些特性。当需要确切知道对象的最终类型时,应使用 type()。

  1. 条件运算符决定对列表中每个元素应用哪个函数。它选择 flatten 或 str 这两个函数名之一,然后根据名称调用所选函数。

  2. 列表推导式没有用方括号包裹,这是正确的。

flatten() 函数的实际用途比我们最初设想的还要广泛。

坚固如石

似乎用元组能实现的事情,用列表也能实现。从实际用途来看,列表看起来就像是可变的元组。元组拥有与列表相同的属性和方法(除了少数罕见的方法 tuple.__getnewargs__()),这一点可以通过获取两类的属性和方法集合并求差集来验证。

下面的代码展示了列表与元组之间的属性和方法差异:

1print(set(dir(list)) - set(dir(tuple)))

那么,为什么还要使用元组呢?有两个原因,都与其不可变性有关。

什么是元组?

元组是 Python 中少数几种不可变数据类型之一,行为类似常量。一旦创建元组,就无法添加、删除或修改其中的元素。如果需要一个在程序执行期间不应变化的元素集合,就应该使用元组。假设我们要创建一组常用常量,并意外更改了 π 的值:

1r = 1 # 可以初始化 r 的值 2some_constants = [math.pi, math.e, 1, 0] 3PI, E, ONE, ZERO = range(4) 4some_constants[PI] = 4 # 哦不... 5area = some_constants[PI] * r * r 6print(area)

这种技巧在元组中行不通,因此以下代码会报错:

1some_constants = (math.pi, math.e, 1, 0) 2some_constants[PI] = 4

记住,是逗号创建了元组,而不是括号:

some_constants = math.pi, math.e, 1, 0

元组保护其内容免受有意或无意的破坏。此外,它们没有隐藏的收缩或扩展机制。由于缺少这种机制,元组的创建速度比同尺寸的列表更快。对于小元组(少于 128 个元素),性能差距非常显著,可达 2 到 20 倍。更大的元组仍然更高效,但优势不如小元组明显。只要可能,就应优先使用元组而非列表。

简而言之:让你的代码更安全、更快!

没有树?那就用 dict()

Python 包含一套强大的数据类型和数据结构。

更多数据类型和数据结构由标准库提供。似乎总能找到适合特定场景的容器。更多细节请参见“浪费空间,节省时间”一节。

该集合中不包含树结构(无论是二叉树还是其他形式)。为什么?

树的一个常见用途是搜索。平衡二叉搜索树的最坏情况时间复杂度为 O(log⁡N)O(\log N),相比线性列表的 O(N)O(N) 搜索时间有明显提升。

而 Python 字典(内置数据类型)平均搜索时间复杂度为 O(1)O(1),这是无法超越的,因此我们应该使用 dict。

关于元组、列表和字符串的测验

Quiz

单选题

已知 t = (1, 2, 3, 4),执行 t[2] = 5 后,输出是什么?

1 / 5

通用技巧

模块与类相关技巧