有些 Python 入门教程把元组称为“不可变列表”,然而这并没有完全概括元组的特点。除了用作不可变的列表,它还可以用于没有字段名的记录。
如果把元组当作一些字段的集合,那么数量和位置信息就变得非常重要了。
lax_coordinates = (33.9425, -118.408056) # 洛杉矶国际机场的经纬度
# 东京市的一些信息:市名、年份、人口(单位:百万)、人口变化(单位:百分比)和面积(单位:平方千米)。
city, year, pop, chg, area = ('Tokyo', 2003, 32450, 0.66, 8014)
# 一个元组列表,元组的形式为 (country_code, passport_number)。
traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]
for passport in sorted(traveler_ids):
print('%s/%s' % passport)# 在迭代的过程中,passport变量被绑定到每个元组上。
# for 循环可以分别提取元组里的元素,也叫作拆包(unpacking)
for country, _ in traveler_ids:
print(country)
元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。除非我们用 * 来表示忽略多余的元素。Python 爱好者们很喜欢用元组拆包这个说法,但是可迭代元素拆包这个表达也慢慢流行了起来,比如“PEP 3132—Extended IterableUnpacking”的标题就是这么用的。
最好辨认的元组拆包形式就是平行赋值,也就是说把一个可迭代对象里的元素,一并赋值到由对应的变量组成的元组中。
lax_coordinates = (33.9425, -118.408056)
latitude, longitude = lax_coordinates # 元组拆包
不使用中间变量交换两个变量的值:
b, a = a, b
用 *
运算符把一个可迭代对象拆开作为函数的参数:
print(divmod(20, 8))
t = (20, 8)
print(divmod(*t))
quotient, remainder = divmod(*t)
print(quotient, remainder)
os.path.split()
函数会返回以路径和最后一个文件名组成的元组 (path, last_part):
import os
_, filename = os.path.split('/home/luciano/.ssh/idrsa.pub')
print(filename) # idrsa.pub
在 Python 中,函数用 *args
来获取不确定数量的参数算是一种经典写法了。于是 Python 3 里,这个概念被扩展到了平行赋值中:
a, b, *rest = range(5)
print(a, b, rest) # 0 1 [2, 3, 4]
a, b, *rest = range(2)
print(a, b, rest) # 0 1 []
*
前缀只能用在一个变量名前面,但是这个变量可以出现在赋值表达式的任意位置
a, *body, c, d = range(5)
print(a, body, c, d) # 0 [1, 2] 3 4
*head, b, c, d = range(5)
print(head, b, c, d) # [0, 1] 2 3 4
接受表达式的元组可以是嵌套式的,例如 (a, b, (c, d))。只要这个接受元组的嵌套结构符合表达式本身的嵌套结构,Python 就可以作出正确的对应。
metro_areas = [
('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833)),
]
print('{:15} | {:^9} | {:^9}'.format('', 'lat.', 'long.'))
fmt = '{:15} | {:9.4f} | {:9.4f}'
for name, cc, pop, (latitude, longitude) in metro_areas:
if longitude <= 0: # 输出限制在西半球的城市
print(fmt.format(name, latitude, longitude))
#----------------------结果------------------------------------
| lat. | long.
Mexico City | 19.4333 | -99.1333
New York-Newark | 40.8086 | -74.0204
Sao Paulo | -23.5478 | -46.6358
在 Python 3 之前,元组可以作为形参放在函数声明中,例如def fn(a, (b, c), d):
。然而 Python 3 不再支持这种格式。具体原因见于“PEP 3113—Removal of Tuple ParameterUnpacking”。
collections.namedtuple
2 是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。
在之前的开胃小菜中是这样新建 Card 类的
Card = collections.namedtuple('扑克牌', ['点数', '花色'])
用具名元组来记录一个城市的信息
from collections import namedtuple
# 创建一个具名元组需要两个参数,一个是类名,另一个是类的各个
# 字段的名字。后者可以是由数个字符串组成的可迭代对象,或者是由空
# 格分隔开的字段名组成的字符串。
City = namedtuple('City', 'name country population coordinates')
tokyo = City('Tokyo', 'JP', 36.933, (35.689722, 139.691667))
print(tokyo)
City(name='Tokyo', country='JP', population=36.933, coordinates=(35.689722, 139.691667))
print(tokyo.population) # 你可以通过字段名或者位置来获取一个字段的信息
print(tokyo.coordinates)
print(tokyo[1])
除了从普通元组那里继承来的属性之外,具名元组还有一些自己专有的属性。_fields
类属性、类方法_make(iterable)
和实例方法 _asdict()
。
print(City._fields) # 包含这个类所有字段名称的元组
LatLong = namedtuple('LatLong', 'lat long')
delhi_data = ('Delhi NCR', 'IN', 21.935, LatLong(28.613889, 77.208889))
# 用 _make() 通过接受一个可迭代对象来生成这个类的一个实例,
# 它的作用跟 City(*delhi_data) 是一样的
delhi = City._make(delhi_data)
# _asdict() 把具名元组以 collections.OrderedDict 的形式返
# 回,我们可以利用它来把元组里的信息友好地呈现出来
print(delhi._asdict())
for key, value in delhi._asdict().items():
print(key + ':', value)
元组是一种很强大的可以当作记录来用的数据类型。它的第二个角色则是充当一个不可变的列表。
要把元组当作列表来用的话,最好先了解一下它们的相似度如何。
从下表中可以清楚地看到,除了跟增减元素相关的方法之外,元组支持列表的其他所有方法。还有一个例外,元组没有 __reversed__
方法,但是这个方法只是个优化而已,reversed(my_tuple)
这个用法在没有 __reversed__
的情况下也是合法的。
列表或元组的方法和属性(那些由object类支持的方法没有列出来)
方法或属性 | 列表 | 元组 | 详解 |
---|---|---|---|
s.__add__(s2) |
● | ● | s + s2,拼接 |
s.__iadd__(s2) |
● | s += s2,就地拼接 | |
s.append(e) |
● | 在尾部添加一个新元素 | |
s.clear() |
● | 删除所有元素 | |
s.__contains__(e) |
● | s 是否包含 e | |
s.copy() |
● | 列表的浅复制 | |
s.count(e) |
● | ● | e 在 s 中出现的次数 |
s.__delitem__(p) |
● | 把位于 p 的元素删除 | |
s.extend(it) |
● | 把可迭代对象 it 追加给 s | |
s.__getitem__(p) |
● | ● | s[p],获取位置 p 的元素 |
s.__getnewargs__() |
● | 在 pickle 中支持更加优化的序列化 | |
s.index(e) |
● | ● | 在 s 中找到元素 e 第一次出现的位置 |
s.insert(p, e) |
● | 在位置 p 之前插入元素e | |
s.__iter__() |
● | ● | 获取 s 的迭代器 |
s.__len__() |
● | ● | len(s),元素的数量 |
s.__mul__(n) |
● | ● | s * n,n 个 s 的重复拼接 |
s.__imul__(n) |
● | s *= n,就地重复拼接 | |
s.__rmul__(n) |
● | ● | n * s,反向拼接 * |
s.pop([p]) |
● | 删除最后或者是(可选的)位于 p 的元素,并返回它的值 | |
s.remove(e) |
● | 删除 s 中的第一次出现的 e | |
s.reverse() |
● | 就地把 s 的元素倒序排列 | |
s.__reversed__() |
● | 返回 s 的倒序迭代器 | |
s.__setitem__(p,e) |
● | s[p] = e,把元素 e 放在位置p,替代已经在那个位置的元素 | |
s.sort([key],[reverse]) |
● | 就地对 s 中的元素进行排序,可选的参数有键(key)和是否倒序(reverse) |
在 Python 里,像列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作,但是实际上切片操作比人们所想象的要强大很多。
在切片和区间操作里不包含区间范围的最后一个元素是 Python 的风格,这个习惯符合 Python、C 和其他语言里以 0 作为起始下标的传统。这样做带来的好处如下。
当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3)
和 my_list[:3]
都返回 3 个元素。
当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop - start)即可。
可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成 my_list[:x]
和 my_list[x:]
就可以了,如下所示:
l = [10, 20, 30, 40, 50, 60]
print(l[:2]) # 在下标2的地方分割
print(l[2:])
print(l[:3]) # 在下标3的地方分割
print(l[3:])
一个众所周知的秘密是,我们还可以用 s[a:b:c]
的形式对 s 在 a 和 b之间以 c 为间隔取值。c 的值还可以为负,负值意味着反向取值。
s = 'bicycle'
print(s[::3]) # bye
print(s[::-1]) # elcycib
print(s[::-2]) # eccb
a:b:c
这种用法只能作为索引或者下标用在 []
中来返回一个切片对象:slice(a, b, c)
。
对seq[start:stop:step]
进行求值的时候Python会调用seq.__getitem__(slice(start, stop, step))
。就算你还不会自定义序列类型,了解一下切片对象也是有好处的。例如你可以给切片命名4,就像电子表格软件里给单元格区域取名字一样。
例如要解析如下所示的纯文本文件形式的收据,这时使用有名字的切片比用硬编码的数字区间要方便得多
invoice = """
0.....6 40 52 55
1909 Pimoroni PiBrella $17.50 3 $52.50
1489 6mm Tactile Switch x20 $4.95 2 $9.90
1510 Panavise Jr. - PV-201 $28.00 1 $28.00
1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95
"""
SKU = slice(0, 6)
DESCRIPTION = slice(6, 40)
UNIT_PRICE = slice(40, 52)
QUANTITY = slice(52, 55)
ITEM_TOTAL = slice(55, None)
line_items = invoice.split('\n')[2:]
for item in line_items:
print(item[UNIT_PRICE], item[DESCRIPTION])
#---------------------结果----------------------
$17.50 Pimoroni PiBrella
$4.95 6mm Tactile Switch x20
$28.00 Panavise Jr. - PV-201
$34.95 PiTFT Mini Kit 320x240
[]
运算符里还可以使用以逗号分开的多个索引或者是切片,外部库NumPy 里就用到了这个特性,二维的 numpy.ndarray
就可以用 a[i,j]
这种形式来获取,抑或是用 a[m:n, k:l]
的方式来得到二维切片。
要正确处理这种 []
运算符的话,对象的特殊方法 __getitem__
和 __setitem__
需要以元组的形式来接收a[i, j]
中的索引。也就是说,如果要得到 a[i, j]
的值,Python 会调用 a.__getitem__((i, j))
。
Python 内置的序列类型都是一维的,因此它们只支持单一的索引,成对出现的索引是没有用的。
省略(ellipsis)的正确书写方法是三个英语句号(...
),而不是Unicdoe 码位 U+2026 表示的半个省略号(…)。省略在 Python 解析器眼里是一个符号,而实际上它是 Ellipsis
对象的别名,而 Ellipsis
对象又是 ellipsis
类的单一实例。
它可以当作切片规范的一部分,也可以用在函数的参数清单中,比如 f(a, ..., z)
,或 a[i:...]
。在NumPy 中,...
用作多维数组切片的快捷方式。如果 x 是四维数组,那么 x[i, ...]
就是 x[i, :, :, :]
的缩写。如果想了解更多,请参见“Tentative NumPyTutorial”。
目前还没有发现在 Python 的标准库里有任何Ellipsis
或者是多维索引的用法。
如果把切片放在赋值语句的左边,或把它作为 del
操作的对象,我们就可以对序列进行嫁接、切除或就地修改操作。
l = list(range(10))
print(l) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
l[2:5] = [20, 30]
print(l) # [0, 1, 20, 30, 5, 6, 7, 8, 9]
del l[5:7]
print(l) # [0, 1, 20, 30, 5, 8, 9]
l[3::2] = [11, 22]
print(l) # [0, 1, 20, 11, 5, 22, 9]
# 赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代
# 对象。即便只有单独一个值,也要把它转换成可迭代的序列。
l[2:5] = 100
# Traceback (most recent call last):
# File "", line 1, in
# TypeError: can only assign an iterable
l[2:5] = [100]
print(l) # [0, 1, 100, 22, 9]
python进阶书目串烧(一)—— 特殊方法、序列数组、列表推导、生成器表达
python进阶书目串烧(二)—— 元组拆包、具名元组、元组对比列表、切片
python进阶书目串烧(三)—— 序列、排序、列表对比数组
python进阶书目串烧(四)—— 内存视图、NumPy、列表对比双向队列
python进阶书目串烧(五)—— 泛映射类型、字典推导、映射的弹性键查询
python进阶书目串烧(六)—— 字典变种、不可变映射类型、集合推导
python进阶书目串烧(七)—— 字典原理、字典与集合特征对比
B2中涉及到拆包的位置:1.1 解压序列赋值给多个变量,1.2 解压可迭代对象赋值给多个变量 ↩︎
用 namedtuple
构建的类的实例所消耗的内存跟元组是一样的,因为字段名都被存在对应的类里面。这个实例跟普通的对象实例比起来也要小一些,因为 Python 不会用 __dict__
来存放这些实例的属性。 ↩︎
B3中涉及到切片的位置:第5条(了解切割序列的办法),第6条(在单次切片操作内,不要同时指定start、end和stride) ↩︎
命名切片在B2的位置:1.11 命名切片 ↩︎