4. 变量类型

本章介绍 Python 的内置变量类型。

我认为以下内置变量类型是在 Python 中经常用到、或者必须有所了解的:

类型

关键字

说明

例子

【数字】

整型

int

整数

1, -1

浮点型

float

浮点数

1.0

复数型

complex

复数

complex(1,2)

【序列】

列表

list

一串有序的可变数据序列,每一项数据可以是任意类型。

[1, 2]

元组

tuple

一串有序的不可变数据序列,在创建时就固定了每一项的数据值。

(1, 2)

字符串

str

一串文本字符组成的不可变序列。

"string"

【映射】

字典

dict

一些互不相同的键及它们各自对应的值组成的键值对数据集。

{"a":1, "b":2}

【集合】

集合

set

一些互不相同的数据值组成的无序可变数据集。

{1, 2}

【其他】

布尔型

bool

表示真或假的逻辑类型。

True

空对象

None

表示空。

None

以上并不是 Python 的全部内置类型:

  • 一些高级的、复杂的变量类型,例如 range 构造器,不再在这里列出。它们会在后续的章节进行介绍。

  • 一些较少使用到的类型,比如 byte 二进制字节类型,不会在本文的任何章节介绍。

4.1. 布尔型与空对象

在介绍其他的变量类型之前,先介绍这两个特殊的类型。

4.1.1. 布尔型

布尔型有真(True)或假(False)两种逻辑值,单词的首字母大写。常用的逻辑运算:

  • 逻辑与:全真才为真 x and y

  • 逻辑或:含真即为真 x or y

  • 逻辑非: not x

  • 逻辑异或:相异为真 x ^ y

以上 xy 均是布尔型变量。

[19]:
x = True
y = False

print(x and y, x or y, not x, x ^ y)
False True False True

4.1.2. 空对象

None 是 Python 中的空对象,它或许并不常用,但读者有必要了解。

空对象的逻辑值为假:

[20]:
x = None
print(bool(x))
False

4.2. 数字类型:int, float, complex

数字类型没有太多需要介绍的地方。

  • 四则运算:+, -, *, /

  • 整除与取余:c = a // b, d = a % b;或者 c, d = divmod(a,b)

    • 这里的整除是指向负无穷取整,例如 -5//2 的结果是 -3

    • 复数不能参与整除或取余运算。

  • 乘方:a ** b,或者 pow(a, b)

  • 取模:abs(a)。如果 a 是复数,那么会计算模长;如果是整数或浮点数,实质上就是取绝对值。

  • 自运算:a += 1a 自增 1;同理有 -=, *=, /=

值得注意的点:

  • 只要有除法参与的数学运算,其结果一定是浮点型

  • 只要有浮点型参与的数学运算,其结果也一定是浮点型

  • Python 的内部机制已经处理了整数溢出的问题,因此无须担心。

  • 虽然在数学上不合法,但是在 Python(以及一众编程语言)中,0 ** 0 等于 1。

特别地,浮点型中还包含两个特殊的值,分别是”非数“(Not a Number, nan)与”正/负无穷“(Infinity, inf):

[21]:
x, y, z = 'nan', 'inf', '-inf'
print(float(x), float(y), float(z))
nan inf -inf

复数的使用非常少见,随便举个例子吧:

[22]:
x = complex(1, 5)
y = complex(2, -1)
z = x + y
print(z, abs(z))
(3+4j) 5.0

4.2.1. 类型转换与取整

Python 中从浮点型到整型的强制类型转换会截断小数点之后的部分:

[23]:
a, b, c, d = 1.2, 1.6, -1.2, -1.6
print(int(a), int(b), int(c), int(d))
1 1 -1 -1

要实现复杂的取整控制,可以调用 Python 内置的 math 模块:

  • floor:向负无穷取整。

  • ceil:向正无穷取整。

[24]:
import math  # 导入 math 模块

print(math.floor(a), math.ceil(b), math.floor(c), math.ceil(d))
1 2 -2 -1

不过在我个人的实践中,取整与四舍五入进位的任务通常由 numpy 库代劳;有兴趣的读者,可以阅读 Numpy 的相关函数:

4.2.2. 比较数字大小

常规的数字大小判断:

  • 小于 < 与小于等于 <=

  • 大于 > 与大于等于 >=

  • 等于 == 与不等于 !=

[25]:
x = 3
y = 4
print(x != y)
True

特别地,Python 还支持“三元比较”:

[26]:
print(3 < 4 == 4, 3 > 2 < 4, 1 < 3 <= 5)
True True True

重要

不要试图用双等号比较两个浮点值的是否相等!

浮点计算是有精度的,直接比较它们是否相等是不明智的:

[27]:
x, y = 0.1, 0.2
z = 0.3

print(x+y, z, x+y==z)
0.30000000000000004 0.3 False

关于高精度的数学计算,推荐配合科学计算库 NumPy 使用。

4.3. 列表:list

Python 的三种常用序列 list, tuple, str, 我们先讲列表 list;列表大概是最接近其他编程语言的序列了。

  • 列表序号从 0 开始。

  • Python 中的列表类似于其他语言的数组,不过列表的长度可变、并且元素不必是同一类型的。

虽然列表中可以包含不同类型的元素,但从编程习惯上说,个人不推荐这样做。

[28]:
x = [1, 2, 'a', 4]
y = []  # 空列表
print(x)
[1, 2, 'a', 4]

Python 中的列表默认支持常见所有的序列操作:

  • 索引元素:单个选取 x[index] ,切片型选取 x[start:end:step]

  • 元素个数: len(x)

  • 追加:单个元素 x.append(item) ,追加一个列表 x.extend(y)

  • 插入: x.insert(index, item)

  • 排序:

    • 按值排序:x.sort() ,或者带返回值的 sorted(x)

    • 反序:x.reverse() ,或者带返回值的 reversed(x)

  • 查询:

    • 判断是否包含 item in x

    • 判断包含的次数 x.count(item)

    • 返回索引序数 x.index(item)

  • 删除:

    • 按索引:弹出并返回一个元素 x.pop(index) ,直接删除元素 del(x[index])

    • 按值:移除等于给定值的项 x.remove(item)

    • 清空: x.clear()

  • 最值:最大值 max(x) ,最小值 min(x)

上述叙述中, xy 均表示列表, index 表示序数(整数类型), item 表示列表元素。

4.3.1. 索引元素

列表中最基础的操作就是根据索引序号,取出单个(或多个)元素:

[29]:
x = [1, 2, 3, 4, 5]
print(x[0], x[4])
1 5

Python 支持负数索引,比如 x[-1] 表示列表的倒数第 1 位元素:

[30]:
x = [1, 2, 3, 4, 5]
print(x[-2])
4

Python 支持一种切片语法,可以指定索引序号的起始、终止、步长,来选取多个元素。

  • x[start:end] :从 x[start] 依次选取到 x[end-1]

  • x[start:end:step] :从 x[start]step 个元素选取依次,直到 x[end-1] (或它之前最后一个能被选取到的元素)。步长可以是负数,但相应地必须有 start >= end

  • x[start:] 或者 x[:end] :从 x[start] 选取到末尾,或者从起始选取到 x[end-1] 。这其实是忽略了冒号一侧的项,也可以用空值 None 补位

重要

切片选取的结束是第 end-1 个元素,而不是第 end 个元素。

Python这样设计的原因是,这样保证了切片 x[start:end] 的长度恰好是 end 减去 start,而不是 end-start+1.

[31]:
# 选取 0 到 3,注意末尾被舍去
x = [1, 2, 3, 4, 5]
print(x[0:4])
[1, 2, 3, 4]
[32]:
# 选取 0 到 3 (或4),每 2 个选取一次
print(x[0:4:2], x[0:5:2])
[1, 3] [1, 3, 5]
[33]:
# 负数步长
print(x[::-1])
[5, 4, 3, 2, 1]
[34]:
# 从 1 选取到末尾,或从起始选取至倒数第 2 项
print(x[1:], x[:-1])
[2, 3, 4, 5] [1, 2, 3, 4]
[35]:
# 也可以忽略某一项,等同于 None
print(x[::2], x[None:None:2])
[1, 3, 5] [1, 3, 5]

赋值可以直接进行:

[36]:
x = [1, 2, 3, 4, 5]
x[:2] = [6, 7]  # 本质是将右侧解包,然后分别传入两个元素位
print(x)
[6, 7, 3, 4, 5]

4.3.2. 元素个数

Python 的 len() 函数是一个独立的函数,并不是通过 x.len() 的方式调用的。这其中设计的差别,读者可以仔细体会。

该函数不止适用于列表,也适用于其他序列类型。

[37]:
x = [1, 2, 3, 4, 5]
print(len(x))
5

4.3.3. 追加

Python 的追加元素使用 x.append()

[38]:
x = [1, 2, 3, 4, 5]
x.append(-2)

print(x)
[1, 2, 3, 4, 5, -2]

要追加一个列表,使用 x.extend()

[39]:
x = [1, 2, 3, 4, 5]
x.extend([-2, -1])

print(x)
[1, 2, 3, 4, 5, -2, -1]

注意, x.append() 这种 Python 内置实例的方法并不返回任何值。所以,你不能把它赋值到另一个变量:

[40]:
y = [1, 2, 3, 4, 5].append(-2)
print(y)  # 无效的赋值,因为它并没有返回值
None

要实现这种“返回值”效果,可以考虑下面两种方法之一:

  • 加号:Python 支持用加号连接两个可变列表,从而把它们“融合”在一起

  • 星号:Python 支持用前缀星号的方式进行列表展开

[41]:
x = [1, 2, 3]
y1 = x + [4, 5]
y2 = [*x, 4, 5]

print(y1, y2)
[1, 2, 3, 4, 5] [1, 2, 3, 4, 5]

4.3.4. 插入

x.insert(index, item) 将元素插入到第 index 个元素的位置,原第 index 及以后的元素依次后移:

[42]:
x = [1, 2, 3]
x.insert(1, -1)  # 插入到 x[1] 处

print(x, x[1])
[1, -1, 2, 3] -1

要实现类似上一小节的“返回值”效果,仍然可以使用加号或者星号两种方式:

[43]:
x = [1, 2, 3]
item = -1

y1 = x[:1] + [item] + x[1:]
y2 = [*x[:1], item, *x[1:]]

print(y1, y2)
[1, -1, 2, 3] [1, -1, 2, 3]

4.3.5. 排序

4.3.5.1. 按值排序

x.sort() 对列表进行排序,默认是按升序。Python 的排序是稳定排序。

[44]:
x = [1, 4, 3, 2]
x.sort()

print(x)
[1, 2, 3, 4]

添加 reverse=True 选项,可以按降序进行排列。

[45]:
x = [1, 4, 3, 2]
x.sort(reverse=True)

print(x)
[4, 3, 2, 1]

如果列表内的元素不是数字,是列表或其他序列,会按 < 比较的方式来排序。下面是对列表元素进行排序:

[46]:
x = [[1, 2], [4, 3, 2], [3, 4], [3, 2]]
x.sort()

print(x)
[[1, 2], [3, 2], [3, 4], [4, 3, 2]]

要实现类似上一小节的“返回值”效果,仍然可以使用加号或者星号两种方式:

[47]:
x = [1, 4, 3, 2]

print(sorted(x, reverse=True))
[4, 3, 2, 1]

4.3.5.2. 反序

列表反序有三种方式:

  • 就地反序: x.reverse()

  • 带返回值的反序:

    • 利用 reversed 生成器: list(reversed(x))

    • 利用步长为 -1 的切片: x[::-1]

[48]:
x = [1, 4, 3, 2]
x_copy = [1, 4, 3, 2]

x.reverse()
y1 = list(reversed(x_copy))
y2 = x_copy[::-1]
print(x, x==y1, x==y2)
[2, 3, 4, 1] True True

4.3.6. 查询

要查询一个元素是否在列表中,使用 in 关键字:

[49]:
x = [1, 2, 3, 4]

print(2 in x, 5 in x)
True False

相反地,要确定一个元素是否不再列表中,使用 not in 来取非:

[50]:
x = [1, 2, 3, 4]

print(2 not in x, 5 not in x)
False True

要查询一个元素在列表中的位置,使用 x.index(item)

  • 如果列表中该元素出现了多次, x.index() 只会返回最靠前的那项对应的索引序数。

  • 如果要返回该元素在列表中出现的所有位置,可以使用列表解析(参考下方的列表解析一节)功能。

[51]:
x = [1, 2, 3, 2]
item = 2
y1 = x.index(item)
y2 = [i for i, v in enumerate(x) if v == item]

print(y1, y2, sep='\n')
1
[1, 3]

如果 item 不在列表中,这样会弹出 ValueError 错误,提示你要查询的对象并不在列表中:

[52]:
x.index(5)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-52-96ad5df81983> in <module>
----> 1 x.index(5)

ValueError: 5 is not in list

使用 x.count() 函数,可以避免该错误:

[53]:
x = [1, 2, 3, 2]

print(x.count(2), x.count(5))
2 0

4.3.7. 删除

4.3.7.1. 按索引删除

x.pop() 弹出列表末尾的元素,或用 x.pop(index) 弹出第 index 位的元素:

[54]:
x = [1, 2, 3, 4]
y = x.pop()
print(y, x)
4 [1, 2, 3]
[55]:
x = [1, 2, 3, 4]
y = x.pop(2)
print(y, x)
3 [1, 2, 4]

或者直接用 del(index) 命令来删除元素 x[index]

[56]:
x = [1, 2, 3, 4]
del(x[1])
print(x)
[1, 3, 4]

4.3.7.2. 按值删除

给定一个值 item ,用 x.remove(item) 可以删除列表中等于 item 的项:

  • 类似于 x.index() ,它只会删除最靠前的匹配项。

  • 考虑使用列表解析(参考下方的列表解析一节)来移除所有的匹配项。

[57]:
x = [1, 2, 3, 2]
x_copy = [1, 2, 3, 2]
item = 2

x.remove(item)
y = [k for k in x_copy if k != item]
print(x, y, sep='\n')
[1, 3, 2]
[1, 3]

4.3.7.3. 清空列表

使用 x.clear() 来清空列表为 []

[58]:
x = [1, 2, 3, 4]
x.clear()
print(x)
[]

4.3.8. 最值

max()min() 函数返回列表中的最值。类似于 len() ,这两个函数对其他的序列变量类型也同样有效。

[59]:
x = [1, 3, 4, 2]

print(min(x), max(x))
1 4

4.4. 高级列表操作

4.4.1. 列表展开

可以“就地”把列表拆开成多个以逗号分隔的元素。除了上文提到的展开为列表中的项(比如 [1, *x, 2] ),更常见的是拆成函数的传入参数。比如取余函数 divmod() 接受两个参数:

[60]:
x = [7, 2]

print(divmod(*x))  # 7÷2=3 … 1
(3, 1)

4.4.2. 列表解析

列表解析(也称列表推导)通过 for 循环与 if 判断来筛选列表中的元素。

[61]:
x = [1, 2, 3, 4, 5, 6]
y = [k+10 for k in x[:3]]
z = [k for k in x if k % 2 == 1]

print(y, z, sep='\n')
[11, 12, 13]
[1, 3, 5]

4.5. 元组

Python 中的元组与列表类似,但是元素不可变。

  • 元组与列表有不同的使用场景。

  • 元组的元素之间以逗号隔开,外侧的圆括号是可选的。

[62]:
x = 1, 2, 3  # 等同于 x = (1, 2, 3)
y = ()       # 空元组
z = (1,)     # 单元素元组

元组也可以解包,将内部值赋给多个变量:

[63]:
a, b, c = x
print(a, b, c)
1 2 3

元组的索引与列表的方式一致,只是不能赋值给元组中的元素:

[64]:
x = 1, 2, 3, 4, 5
x[::2]
[64]:
(1, 3, 5)

元组的长度也可以用 len() 函数获取:

[65]:
x = 1, 2, 3
print(len(x))
3

4.6. 字符串

字符串是一种不可变序列。因此,字符串不存在“就地改动”,任何字符串方法都不会改变原字符串的值。

它在创建时可以用单引号包括,也可以用双引号包括。内部的引号可以用反斜线 \ 转义:

[66]:
x = "It's a string."
y = 'It\'s a string.'
print(x, y, x == y)
It's a string. It's a string. True

字符串可以用加号与乘号来进行连接与重复;如果要连接的均是字符串面值(直接用引号括起的值,而不是字符串变量),可以用空格进行连接。

[67]:
x = "abc" * 3 + "def"
y = ("This is a single-line string "
     "but I can write it across lines.")
z = x + 'qwe'  # 字符串变量也可用加号与面值连接
print(x, y, z, sep='\n')
abcabcabcdef
This is a single-line string but I can write it across lines.
abcabcabcdefqwe

多行字符串可以用三个连续的单引号(或双引号)包括。多行字符串内容的所有换行都会被保留;可以在行尾添加一个反斜线 \ 来忽略当前行的换行:

[68]:
x = """\
This is
a multiline
string.
"""
print(x)
This is
a multiline
string.

4.6.1. 强制类型转换

str() 可以强制将其他类型转换为字符串(如果能够转换):

[69]:
x = 123
y = True
z = [3, [1, 2], 4]

print(str(x), str(y), str(z), sep='\n')
123
True
[3, [1, 2], 4]

字符串也可以转换为列表,本质是按字符依次拆开:

[70]:
x = "abcdefg"
print(list(x))
['a', 'b', 'c', 'd', 'e', 'f', 'g']

4.6.2. 转义与 r 字符串

常见的转移字符包括:

字符

含义

\b

退格

\n

换行

\r

回车(移动到行首)

\t

制表符

\\

被转义的反斜线

如果不想让反斜线进行转义,在字符串的 左侧引号之前 添加 r 实现:

[71]:
x = "String with\tTAB."
y = r"String with\tTAB."
print(x, y, sep='\n')
String with     TAB.
String with\tTAB.

4.6.3. 字符串索引

字符串的索引仍然是字符串。Python 中没有字符类型,单个字符只是长度为 1 的字符串。

字符串的索引仍然支持

[72]:
x = "This is a string."
x[:6]
[72]:
'This i'
[73]:
x[::2]
[73]:
'Ti sasrn.'

唯一不同于列表索引的是,字符串在切片选取时,可以超出索引范围而不报错:

[74]:
x = "This is a string."
print(x[5:2333])
is a string.

4.6.4. 分割字符串

Python 中字符串分割函数主要有 split()rsplit()splitline() 三个方法。

使用 x.split(sep, maxsplit) 来用 sep 字符串分割字符串 x ,并指定最多分割 maxsplit 次:

  • 分割字符串长度可以大于 1

  • 默认的 maxsplit=-1 ,即会完全分割字符串

[75]:
x = "This is a string."

print(x.split('s'))     # 默认完全分割
print(x.split('s', 1))  # 最多分割 1 次,即分割成 2 份
print(x.split('z'))     # 空分割
print(x.split())        # 默认以空格分割
['Thi', ' i', ' a ', 'tring.']
['Thi', ' is a string.']
['This is a string.']
['This', 'is', 'a', 'string.']

我们经常把这个技巧用在 for 循环语句中:

[76]:
fruits = "apple,pear,orange,banana"
for fruit in fruits.split(','):
    print(fruit)
apple
pear
orange
banana

Python 还提供了 rsplit() ,可以从字符串的右侧开始分割。从下例比较两者的区别:

[77]:
x = "This is a string."

print(x.split('s', 1))
print(x.rsplit('s', 1))
['Thi', ' is a string.']
['This is a ', 'tring.']

最后是 splitlines(keepends=False) ,它可以按行分隔符集中的所有符号进行分割(因此比手动地使用 split('\n') 的效果更好):

  • keepends=True 时,它能保留换行符。

  • 关于该函数认定的换行符号集,参考 官方文档:str.splitlines()

  • 它与 split() 函数的另一个差别是对末尾空白行的处理,读者可以参考下例。

[78]:
x = "line 1\nline 2\r\nline 3\n"

print(x.splitlines())
print(x.splitlines(keepends=True))
print(x.split('\n'))  # 多一行
['line 1', 'line 2', 'line 3']
['line 1\n', 'line 2\r\n', 'line 3\n']
['line 1', 'line 2\r', 'line 3', '']

4.6.5. 合并字符串:join()

x.split(sep) 相反, 函数 sep.join(lst) 则是将一个列表用指定的分割符连接起来,组成一个字符串:

[79]:
data = ["apple", "pear", "orange", "banana"]
s = ' ~ '.join(data)
print(s)
apple ~ pear ~ orange ~ banana

4.6.6. 替换子字符串:replace()

字符串替换方法 x.split(old, new, count) 用 new 来替换(从左向右)前 count 次搜索到的 old 子字符串。

[80]:
x = "This is a string."
y1 = x.replace("s", "t")
y2 = x.replace("s", "t", 2)
print(y1, y2, sep='\n')
Thit it a ttring.
Thit it a string.

4.6.7. 检查首尾匹配:startswith() / endswith()

x.startswith(prefix)x.endswith(suffix) 这两个方法来检查字符串首尾端的子字符串是否匹配 prefix 或 suffix。比较常用的情形可能是检查文件的扩展名:

[81]:
files = ["doc-A.txt", "doc-B.md", "Doc-C.txt"]

for f in files:
    if f.startswith('doc'):
        print(f"doc - {f}")
    if f.endswith('txt'):
        print(f"txt - {f}")
doc - doc-A.txt
txt - doc-A.txt
doc - doc-B.md
txt - Doc-C.txt

4.6.8. 清除首尾字符:strip()

strip() 清除两侧匹配的字符串,或者用 lstrip() 单独清除左侧的,或用 rstrip() 单独清除右侧的。

[82]:
files = ["doc-A.txt", "doc-B.md", "Doc-C.txt"]

for f in files:
    s = (f"strip: {f.strip('d'):10}\t"
         f"lstrip: {f.lstrip('doc-'):10}\t"
         f"rstrip: {f.rstrip('txt')}")
    print(s)
strip: oc-A.txt         lstrip: A.txt           rstrip: doc-A.
strip: oc-B.m           lstrip: B.md            rstrip: doc-B.md
strip: Doc-C.txt        lstrip: Doc-C.txt       rstrip: Doc-C.

默认的 strip() 函数会清除字符串两侧的空格——这有时在读取文件时会用到:

[83]:
x = "line 1 \n line 2 \r\n\tline 3\n"
lines = x.splitlines()
lines_strip = [line.strip() for line in lines]
print(lines, lines_strip, sep='\n')
['line 1 ', ' line 2 ', '\tline 3']
['line 1', 'line 2', 'line 3']

4.6.9. 字符串格式化:format() 与 f 字符串

字符串格式化是一种将字符串中的一部分(通常以 {} 标记)以外部数据(比如变量值或者其他字符串)替换的方法。

  • 方法 x.format() 在任何版本的 Python 3 中受到支持

  • 带有前缀 f 的格式化字符串(即 f-string 或 f 字符串)在 Python 3.6 开始被支持

字符串还有一种以百分号 % 进行格式化的方法,是从 Python 2 时期延续下来的语法(在 Python 3 中也可以使用)。本文不再介绍这部分内容,有兴趣的读者可以自行查阅。

我们常常需要把变量的值(可能不是字符串类型)嵌入到字符串中,比如:

[84]:
lang, ver = "Python", 3
x = "We are learning " + lang + " " + str(ver) + "."
print(x)
We are learning Python 3.

上例中虽然用加法连接实现了这一点,但的确非常狼狈。

4.6.9.1. format() 方法

Python 支持用 x.format() 的形式来格式化字符串:

[85]:
lang, ver = "Python", 3
x = "We are learning {} {}.".format(lang, ver)

# 也可以按名称访问
y = "We are learning {mylang} {myver}.".format(mylang=lang, myver=ver)

# 甚至可以进行字典展开,参考字典章节
d = {"mylang": lang, "myver": ver}
z = "We are learning {mylang} {myver}.".format(**d)

print(x, x==y, x==z, sep='\n')
We are learning Python 3.
True
True

上面的几种写法都可以得到相同的结果。如你所见,在格式化字符串中,我们用花括号来占位。默认的, x.format() 的输入参数都会被转为字符串格式。

  • 如果要在格式化字符串中打印花括号,请使用双写花括号(比如 {{ )。

  • 如果传入的参数是键值对(即 key=value 的形式),请在花括号内注明键名称。这样的优势是代码可读性好。

利用重复的键名或者重复的序号,可以反复使用同一个目标:

[86]:
# 复用 lang 变量
x = "We are learning {0} {1}. I love {0}!".format(lang, ver)
y = "We are learning {mylang} {myver}. I love {mylang}!".format(mylang=lang, myver=ver)
print(x, x==y, sep='\n')
We are learning Python 3. I love Python!
True

一个更复杂的例子是将输入参数转为特定的字符串格式,比如将浮点数转换为保留指定小数位的字符串。这时候需要用到冒号 : 来指定格式——格式指定位于冒号右侧,而键名(如果无则留空)位于冒号左侧:

[87]:
val, digits = 3.1415926535, 5
x = "PI = {:.5f}...".format(val)
y = "PI = {val:.5f}...".format(val=val)
z = "PI = {0:.{1}f}...".format(val, digits)
print(x, x==y, x==z, sep='\n')
PI = 3.14159...
True
True

Python 支持的格式有:

格式声明

格式示例

解释

输入

输出

s

{:5s}

字符串,以最小宽5位输出

"abc"

” abc”

任意数

e

{:.2e}

科学计数法小数点后2位(默认6)

1234.567

“1.23e+03”

E

/

同上,但输出时大写字母 E

1234.567

“1.23E+03”

f

{:.4f}

保留小数点后4位(默认6)

1234.567

“1234.5670”

F

/

同上,但输出时大写 NAN 与 INF

float('inf')

“INF”

g

/

接受数字,并自行判断输出格式

{:.5g}

5位有效数字(自动定点格式)

1234.567

“1234.6”

{:.3g}

3位有效数字(自动科学计数格式)

1234.567

“1.23e+03”

{:g}

(默认6位)

float('nan')

“nan”

G

{:.3G}

同上,但输出时使用大写的 E、NAN 与 INF

1234.567

“1.23E+03”

+

{:+.2f}

正负数均标明符号;保留2位小数

1234.567

“+1234.57”

{: .2f}

正数空格,负数标明负号;保留2位小数

1234.567

” 1234.57”

%

{:.1%}

百分比,保留2位小数

0.12

” 12.0%”

整数

d

{:d}

十进制整数

123

“123”

b

{:b}

二进制整数

123

“1111011”

o

{:o}

八进制整数

123

“173”

x

{:x}

十六进制整数

123

“7b”

X

/

同上,但使用大写的 a~f

123

“7B”

格式符

>

{:>4d}

强制右对齐(数字对象默认)

123

” 123”

{:0>4d}

以0而不是空格补位

123

“0123”

<

{:<4d}

强制左对齐(其他对象默认)

123

“123”

^

{:^4d}

强制居中对齐

123

“123”

其他

,

{:,}

以逗号千分位分隔

1234

“1,234”

#

{:#b}

显式保留输入类型;如二进制以”0b”开头

123

“0b1111011”

下面是几个例子:

[88]:
# 在数字两侧各添加3个星号
x = 1234
print("{:*^{}d}".format(x, len(str(x))+6))
***1234***
[89]:
# 输入给定十进制数的其他进制形式
x = 123
for base in "dboxX":
    print("Type {base}: {val:#{base}}".format(base=base, val=x))
Type d: 123
Type b: 0b1111011
Type o: 0o173
Type x: 0x7b
Type X: 0X7B

4.6.9.2. f 字符串

上面的例子均由 x.format() 方法实现,但是不足之处在于不能方便地直接利用已有的变量值。比如上文中给浮点数保留指定位数的例子,变量 valdigits 仍然需要显式地作为 format() 方法的输入参数:

[90]:
val, digits = 3.1415926535, 5
x = "PI = {0:.{1}f}...".format(val, digits)
y = "PI = {myval:.{mydigits}f}...".format(myval=val, mydigits=digits)
print(x, x==y, sep='\n')
PI = 3.14159...
True

当然,采用上文所介绍过字典展开的方式,你可以用先声明一个字典,然后在传入时用双写星号前缀来展开……

[91]:
val, digits = 3.1415926535, 5
d = {"myval": val, "mydigits": digits}
x = "PI = {myval:.{mydigits}f}...".format(**d)
print(x)
PI = 3.14159...

这样, x.format() 方法的输入就显得不是太累赘。

但并不是所有数据都适合写入同一个字典里的。因此,我推荐使用更方便的 f 字符串来格式化字符串,在字符串的左侧引号之前添加字母 f 即可:

[92]:
# Python >= 3.6
val, digits = 3.1415926535, 5
x = f"PI = {val:.{digits}f}..."
print(x)
PI = 3.14159...

x.format() 方法一样,f 字符串支持格式化字串的所有格式。f 字符串也允许在花括号内进行合法的 Python 表达式书写:

[93]:
print(f"{2**3:0>4d}")
0008

但是,花括号中的表达式不能显示地含有反斜线:

[94]:
print(f"A multiline string:\n{'a'+'\n'+'b'}")
  File "<ipython-input-94-65ada2ec2e1b>", line 1
    print(f"A multiline string:\n{'a'+'\n'+'b'}")
          ^
SyntaxError: f-string expression part cannot include a backslash

你可以通过将带反斜线的值赋值到变量来规避这一点:

[95]:
x = "{}\n{}".format('a', 'b')
print(f"A multiline string:\n{x}")
A multiline string:
a
b

4.6.10. 大小写转换*

Python 提供了丰富的大小写转换支持,包括

  • 全体强制大小写 upper()/lower()

  • 仅首字母大写 capitalize()

  • 每个单词首字母大写 title()

[96]:
x = "It's a string. This is another."
n = len(x) + 10

d = {
    "全大写": x.upper(),
    "全小写": x.lower(),
    "首字母大写": x.capitalize(),
    "单词首字母大写": x.title()
}
for k, v in d.items():
    print(f"{k:10}\t{v}")
全大写             IT'S A STRING. THIS IS ANOTHER.
全小写             it's a string. this is another.
首字母大写           It's a string. this is another.
单词首字母大写         It'S A String. This Is Another.

注意, title() 方法会将一些并非单词头的字母识别为单词头(比如上例中 It's 的字母 s)。要避免这一情形,可以配合正则表达式处理,参考 官方文档:str.title()

4.7. 字典:dict

Python 的字典以键值对(key-value pairs)的形式存储数据:

  • 每一项数据之间用逗号 , 分隔,所有数据外侧用一组花括号 {} 包裹

  • 每一项数据都是一组键值对,键与值之间用冒号 : 分隔

  • 一个字典内,不能存在相同的键名

  • 在 Python >= 3.7 的版本中,字典的项变更为 有序的 (指在循环中被迭代,或被强制转换成序列时,键或键值对的顺序是稳定的),其顺序与创建时每一项被加入的顺序相同。

[97]:
x = {}  # 空字典
y = {"a": 1, "b": 3}
print(y)
{'a': 1, 'b': 3}

字典的每一项数据的值可以是任意的数据类型。同时,不同于 JSON 文件中的键,Python 字典的键也可以是大多数类型(而不仅仅是字符串)。

从编程习惯上讲,我并不推荐在字典键中使用字符串以外的其他类型。

字典可以用 len() 来返回其键值对的个数,用 in 来查询一个键是否在字典中:

[98]:
x = {}
print(len(x))
print("a" in x)
0
False

4.7.1. 字典初始化

字典有许多初始化方式。

  • 依次添加键值对

  • 利用 dict() 构造

    • 从成对序列数据中构造

    • 显式地传入字面键值

  • 利用 fromkeys() 方法

  • 字典解析

最朴素的方式是依次添加键值对。先新建一个空字典,然后依次向内添加键值对:

[99]:
d = {}
d["a"] = 1
d["c"] = 3
d["b"] = 2
print(d)
{'a': 1, 'c': 3, 'b': 2}

在键与值分别存储在两个序列中时,我们可以利用 for 循环:

[100]:
keys = "a", "c", "b"
vals = 1, 3, 2

d = {}
for k, v in zip(keys, vals):
    d[k] = v
print(d)
{'a': 1, 'c': 3, 'b': 2}

从成对序列数据中构造要求一种整合的数据存储方式。如果键与值是“成对地”存储在一个序列中,可以直接使用 dict() 来进行初始化:

[101]:
data = [["a", 1], ["c", 3], ["b", 2]]
d = dict(data)
print(d)
{'a': 1, 'c': 3, 'b': 2}

在字典数据不多时,也可能考虑显式地传入字面键值(键自动视为字符串):

[102]:
d = dict(a=1, c=3, b=2)
print(d)
{'a': 1, 'c': 3, 'b': 2}

使用 fromkeys() 方法能够快速初始化已知键名的字典,将所有键都赋同一个初始值(比如 None ):

[103]:
keys = "a", "c", "b"
d = {}.fromkeys(keys, None)
print(d)
{'a': None, 'c': None, 'b': None}

类似于列表解析,字典也支持解析:

[104]:
data = [["a", 1], ["c", 3], ["b", 2]]
d1 = {x[0]: x[1] for x in data}

keys = "a", "c", "b"
vals = 1, 3, 2
d2 = {k: v for k, v in zip(keys, vals)}

print(d1, d1==d2)
{'a': 1, 'c': 3, 'b': 2} True

4.7.2. 字典的视图

由于字典有键、值、项(键值对)这三个概念,Python 也提供了对应的三种视图:

  • 键视图: x.keys() ,一个依序的、每个键为一个元素的序列

  • 值视图: x.values() ,一个与键视图中的键依次对应的值组成的序列

  • 项视图: x.items() ,一个依上述顺序的、每个元素是一个键值对元组的序列

用 for 循环来展示一下这三种视图:

[105]:
x = {"a": 1, "c": 3}
for k in x.keys():
    print(f"key: {k}")

for v in x.values():
    print(f"val: {v}")

for i in x.items():
    print(f"item: {i}")
key: a
key: c
val: 1
val: 3
item: ('a', 1)
item: ('c', 3)

x.keys() 中循环其实与在 x 中循环的结果是相同的,都是遍历字典的键:

[106]:
for k in x:
    print(k)
a
c

字典的项视图 items() 常常被用在 for 循环中,解包成两个循环变量:

[107]:
x = {"a": 1, "c": 3}
for k, v in x.items():
    print(f"{k} = {v}")
a = 1
c = 3

4.7.3. 按键索引值:get() / setdefault()

要按字典键索引其对应的值,有以下几种方法:

  • 如果确认键 key 位于字典 x 中,可以直接使用该键来索引 x[key]

  • 如果键 key 可能不在 x 中:

    • x.get(key, default=None) 方法,失败时它会返回一个备用值 default

    • x.setdefault(key, default=None) 方法,失败时它会把键值对 key: default 添加到字典

[108]:
x = {"a": 1, "c": 3}
print(x["a"])
1

更安全的选择是使用 x.get(key, default=None) 方法。它的作用是:如果 key 存在于 x 的键中,那么返回该键对应的值;否则,返回 default 指定的值。

[109]:
x = {"a": 1, "c": 3}
print(x.get("b", "Not in dict"))
Not in dict

另一个选择是利用 x.setdefault(key, default=None) 方法。如果 key 存在于 x 的键中,那么返回该键对应的值;否则,将 default 值与 key 键组成键值对,加入到字典 x 中。

[110]:
x = {"a": 1, "c": 3}
y1 = x.setdefault("b", 2)
y2 = x["b"]  # 键值对已经被添加
print(y1, y2)
2 2

4.7.4. 删除项:pop() / popitem()

最简单的自然是 del() 删除函数:

[111]:
x = {"a": 1, "c": 3, "b": 2}
del(x["a"])
print(x)
{'c': 3, 'b': 2}

字典的 x.pop(key) 与列表在表现上类似,也是返回一个值的同时将其从容器中删除:

[112]:
x = {"a": 1, "c": 3, "b": 2}
y = x.pop("a")
print(x, y)
{'c': 3, 'b': 2} 1

特别指出,方法 pop() 也有一个名为 default 的参数,会在字典不包含键时返回该 default 值(如果不显示地给出 default 的值,那么会造成 KeyError)。这一点与 setdefault() 方法相映成趣。

[113]:
x = {"a": 1, "c": 3, "b": 2}
y = x.pop("a", 10)    # 正常返回 x["a"],并删除键 "a"
z = x.pop("a", 100)   # 没有键 "a",返回默认值 100
print(x, y, z)
{'c': 3, 'b': 2} 1 100

最后,介绍 popitem() ,这个方法被用到的较少。它并不指定键名来抛出一个项,而是按照字典键的顺序,反向地依次抛出字典地项——即后进先出(LIFO),如同对栈容器进行弹栈操作一样。

对于 Python 3.7 之前的版本,它并不是依照 LIFO 顺序弹出字典项的,而是按照随机选择的顺序。

[114]:
x = {"a": 1, "c": 3, "b": 2}
while len(x) > 0:
    y = x.popitem()
    print(y)
('b', 2)
('c', 3)
('a', 1)

4.7.5. 更新字典:update()

通过 x.update(y) ,字典 x 可以根据字典 y 的键值对来就地更新字典 x 的数据:

  • 在字典 x 与 y 中都存在的键,以 y 中的值为准

  • 仅在一个字典中存在的键,得以保留

[115]:
x = {"a": 1, "c": 3, "b": 2}
y = {"a": -10, "d": 4}
x.update(y)
print(x)
{'a': -10, 'c': 3, 'b': 2, 'd': 4}

利用循环,我们可以实现与 update() 方法相同的效果:

[116]:
x = {"a": 1, "c": 3, "b": 2}
y = {"a": -10, "d": 4}

for k in y:
    x[k] = y[k]
print(x)
{'a': -10, 'c': 3, 'b': 2, 'd': 4}

4.7.6. 更改键名*

字典并没有单独提供更改键名的方法,但这也是一个实用场景。由于 Python >= 3.7 版本引入了字典键的顺序,要在更改键名时保持键的顺序也显得重要。

先说明一种会打乱键顺序的键名更改方法,那就是将指定键用 pop(oldkey) 弹出然后赋值给 d[newkey] 。下面以将键名改为小写为例:

[117]:
d = dict(a=1, B=2, c=3, D=4)
dkey = dict(B="b", D="d")

for oldkey, newkey in dkey.items():
    d[newkey] = d.pop(oldkey)
print(d)
{'a': 1, 'c': 3, 'b': 2, 'd': 4}

可以看到,字典 d 的键顺序也被打乱了。要保留顺序,只能遍历所有的字典键:

[118]:
d =  {"a": 1, "B": 2, "c": 3, "D": 4}
dkey = {"B": "b", "D": "d"}

oldkeys = tuple(d.keys())
for k in oldkeys:
    # 如果有新键名则使用 dkey[k],否则使用旧键名 k
    d[dkey.get(k, k)] = d.pop(k)

print(d)
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

4.8. 集合

Python 中的集合借鉴了数学意义上的集合概念,要求内部的元素不能重复。

  • 集合 set 是可变的;Python 也提供了一种不可变集合 frozenset ,其不涉及可变性部分的用法与 set 大致相同。

集合用花括号括起的、逗号分隔的多个项来表示。但是,空集合不能使用 {} 来指明(因为这代表空字典),请使用 set() 指明。

[119]:
x = set([1, 2, 3, 2])
y = {1, 2, 3}
z = set()   # 空集合
print(x, y, z)
{1, 2, 3} {1, 2, 3} set()

集合同样支持长度度量 len() ,包含性检查 in ,以及循环遍历。

[120]:
x = {1, 2, 3}
print(len(x), 4 in x)

for k in x:
    print(k, end=' ')
3 False
1 2 3

4.8.1. 集合运算与包含关系

集合运算包括交集、并集、差集、对称差集,包含关系包括(真)子集、(真)超集。

运算

说明

运算符

示例

集合方法

示例

&

a & b

同时位于两个集合中的元素

intersection

a.intersection(b, c, ...)

\|

a \| b

至少位于其一集合中的元素

union

a.union(b,c, ...)

-

a - b

只位于 a 而不位于 b 的元素

difference

a.difference(b, c, ...)

对称差

^

a ^ b

位于且仅位于其一集合中的元素

symmetric_difference

a.symmetric_difference(b)

子集

<=

a <= b

a 的所有元素都在 b 中

issubset

a.issubset(b)

真子集

<

a < b

a 是 b 的子集且 a、b 不同

/

/

超集

>=

a >= b

b 的所有元素都在 a 中

issuperset

a.issuperset(b)

真超集

>

a > b

b 是 a 的子集且 a、b 不同

/

/

互斥

/

/

a 与 b 没有共同元素

isdisjoint()

a.isdisjoint(b)

注意,上述命令中:

  • 交、并、差可以输入多个集合(如最后一列所示)。如果使用运算符,则用重复使用相同的运算符连接即可。

  • 对于“运算符”这一列命令,变量 a 与 b 都必须是集合(或者不可变集合)类型。对于“集合方法”这一列命令,只有 a 必须是严格的集合(或者不可变集合)类型,b 可以是任意的可迭代对象。

  • 上述集合方法也可以用 set 来调用。例如,取差集可以写为 set.difference(a, b)

  • 上述运算的返回值的结果均以 a 为准。例如,如果 a 是 set 类型而 b 是 frozenset 类型,那么返回值将是 set 类型。

[121]:
a, b = {1, 2, 3}, {2, 4, 5}
set_funcs = {
    "交": set.intersection,
    "并": set.union,
    "差": set.difference,
    "对称差": set.symmetric_difference,
    "子集": set.issubset,
    "超集": set.issuperset,
    "互斥": set.isdisjoint
}

for text, func in set_funcs.items():
    print(f"{text:8}\t{func(a, b)}")
交               {2}
并               {1, 2, 3, 4, 5}
差               {1, 3}
对称差             {1, 3, 4, 5}
子集              False
超集              False
互斥              False

4.8.2. 更新集合

集合的更新是就地更新,也涉及到上面的集合运算:

  • 直接更新 a.update(b, ...) ,即将并集赋回,等同于 a |= b | ...

  • 只保留交集 a.intersection_update(b, ...) ,即将交集赋回,等同于 a &= b & ...

  • 只保留差集 a.difference_update(b, ...) ,等同于 a -= b | ...

  • 只保留对称差集 a.symmetric_difference_update(b) ,等同于 a ^= b

以上命令中,除了对称差以外的运算都可以输入多个集合。

[122]:
a, b = {1, 2, 3}, {2, 4, 5}
set_funcs = {
    "(并)更新": set.update,
    "交更新": set.intersection_update,
    "差更新": set.difference_update,
    "对称差更新": set.symmetric_difference_update
}

for text, func in set_funcs.items():
    a_copy = a.copy()  # 拷贝一个副本,避免直接对 a 改动
    func(a_copy, b)
    print(f"{text:8}\t{a_copy}")
(并)更新           {1, 2, 3, 4, 5}
交更新             {2}
差更新             {1, 3}
对称差更新           {1, 3, 4, 5}

4.8.3. 增删元素

本节中的命令只对可变集合(set 类型)有效,对不可变集合(frozenset 类型)无效 。

  • 增加:add(elem)

  • 移除:

    • 移除指定:

      • remove(elem) :移除一个不存在集合中的元素时会造成 KeyError

      • discard(elem) :尝试移除一个元素,如果不存在集合中则静默

    • 随机移除: pop() ,随机返回一个集合中的元素,并将其从集合中移除

    • 清空: clear()

[123]:
a = {1, 2, 3}
a.add(4)
print("After add\t", a)

a.remove(3)
print("After remove\t", a)

a.discard(100)
print("After discard\t", a)
After add        {1, 2, 3, 4}
After remove     {1, 2, 4}
After discard    {1, 2, 4}

其中, pop()clear() 十分易懂,这里就不再用代码说明了。

4.9. 其他

Python 中提供了一个比较奇怪的变量,即单下划线 _ —— 虽然从理论上讲,下划线是变量名中可以使用的合法字符,这个变量名本应如同单字符变量名 xs 一样自然。

它的作用一般有:

  1. 在交互式 Python 运行环境中,它会自动记录最后一次代码执行的结果。

  2. 习惯上,我们将不需要用到的变量值用该变量标记。例如:

    • 不使用的返回值:x, _ = divmod(7, 2)

    • 不使用的循环变量值:[None for _ in range(3)]

    • 不使用的匿名变量输入值:lambda _: 1

  3. 避免编辑器的语法检查。Python 语法检查器会忽略 _ 变量;而其他变量如果声明而未在后文引用,检查器会发出警告。