元组的基本概念
元组的基本操作
在 Python 中,使用圆括号括起逗号分隔的不同的数据项
会被视为一个元组(tuple)。在不引起歧义的情况下,最外层的括号可以省略,例如:
因此,要注意有些时候,一个元素后面不小心多加了一个逗号,它会被当做只有一个元素的元组处理,可能会导致严重的问题:
也可以使用 tuple
类的构造函数从一个可迭代对象中构造元组。
元组是一种不可变类型,元组一旦创建便不能更改,任何尝试对元组元素的修改都会发生错误。相比列表,元组仅支持 .count()
和 .index()
方法,分别用于统计某个元素出现的次数以及查找某个元素的位置。
看起来元组只是一个不可变的列表,元组能实现的功能列表一般也能实现,那为何 Python 要引入这种类型呢?
元组最大的特性也就是其不可变,而有些时候(例如在函数式编程中)并不需要改变元素的值,此时为了防止元素意外变动,就需要使用元组。例如在某些大型工程中函数众多,可能会有某个排序函数意外改变原有序列,如果使用元组就能提前发现问题所在。
元组这种不可变的特点使得它可以作为字典的键:
而列表则不行。
在表示数据时,元组经常用于表达某一个结构形式的数据:元组中的每个元素都表示结构中的一个字段,元组使用位置而不是名字区分不同的字段。在之前的章节 中看到了 Python 的位置参数和关键字参数,它们恰好对应了元组和字典的两种使用思维:元组只关键数据的位置,而字典只关心数据的名称。
从这个角度上说,元组和列表在使用时的差别很大:元组通常视为由多个字段组成的一条数据,而列表通常视为多条数据构成的组。例如,以下是一个既含有列表又含有元组的典型数据:
所以,虽然 Python 列表可以存储各种类型的数值,但在使用时一般只存储某一特定类型的值,并且并不关系这些值的具体位置,处理列表的方式一般使用 for
循环处理所有元素,而较少使用索引与切片单独获取某一个或某一部分元素;元组则相反,对元组的遍历一般是没有意义的,但更关注每个位置对应值的含义。
既然说到了数据的处理,接下来介绍一种元组常用的处理方式。
元组解包
如果需要赋值的变量构成一个元组,而赋予的值是一个序列,这时赋值语句会将其中包含的元素分配给对应位置的变量,例如:
这种操作称为元组的解包(unpack),也称平行赋值。在解包时,变量的个数要和序列的元素个数一致,否则会导致错误。
因为元组是定长的序列,并且每个位置的元素都有特定含义,因此元组解包很适合处理一个元组中的元素。元组解包时,往往会省略最外层的括号,例如:
在解包时,并不总需要使用元组内的所有数据,这时可以使用一个单独的下划线 _
作为占位:
元组解包可以用于交换变量,可以不用使用第三个变量,并且写起来非常方便:
元组解包也能用到 for
循环内,直接处理每次迭代的值:
元组解包可以应用到任何可迭代对象上,但被迭代的对象的元素数量必须要跟接受这些元素的元组长度一致:
因此元组解包有时也称为可迭代元素解包。
如果解包时,只需要用到其中的几个元素,那么可以在某一个变量前加上星号 *
,表示将解包后剩下的值塞进该变量中,组成一个列表:
即便序列中只有两个元素,*rest
也可以正常工作,这时它的接收结果为空列表:
星号 *
前缀在解包时,可以作用于任意一个变量,但是一个解包中只能出现一个星号变量:
总之,星号变量会接收解包后剩余的所有值。
元组解包还可以处理嵌套表达式,只要元组的嵌套关系符合序列的嵌套关系:
嵌套表达式内也可以采用星号变量处理剩余参数。
命名元组
使用命名元组
以上使用元组来表达结构形式的数据,但这种结构只能通过索引来访问其元素。如果元组中的元素较多,不仅操作起来很麻烦,而且比较容易出错。
命名元组是一种特殊的元组,命名元组的元素既可以使用名称访问,也可以使用索引值访问,大大增加了元组的可读性。
命名元组并不是内置的数据类型,而是标准库的一部分,因此首先需要导入命名元组:
使用 namedtuple
的构造函数可以定义一个子类命名元组,其构造函数的完整形式为:
其中各参数的含义为:
typename
是返回的命名元组子类的类名,创建命名元组相当于创建了一个新类field_names
是命名元组各元素的名称,是一个由字符串组成的列表,其中的字符串必须为合法的标识符;或者field_names
也可以是一个字符串,各元素名称使用逗号或空格隔开- 当
rename
设置为True
时,如果field_names
中包含保留关键字或重复的变量名,则会自动重命名为_1
、_2
等 - 如果给定
defaults
值,则它应该是一个由默认值组成的可迭代对象。由于具有默认值的参数必须位于没有默认值的任何参数之后,如果默认值少于命名元组的元素个数,默认值将应用于最右边的元素。如果默认值多于命名元组的元素个数,将引起错误 - 如果设置了
module
参数,那么该类将位于该模块下,因此该自定义类的__module__
属性将被设置为该参数值
以下是一个简单的示例,构造了一个类型的命名元组,并由该类型的命名元组生成具体的元组对象:
命名元组对象可以通过访问元素的字段名称来访问其属性,这更加准确、方便:
命名元组创建的类继承于元组,并且包含以下额外属性和方法:
._fields
类属性返回其所有字段名称,以下给出了一个示例,并注意 rename
参数的作用效果:
如果没有设置 rename
参数,则会直接发生错误。
._field_defaults
属性返回所有有默认值的字段及其默认值组成的字典。如果没有默认值,那么返回空字典。
._make(iterable)
用于从指定可迭代对象构建命名元组对象。这里需要注意的是,得到的命名元组参数和字段名是对应的,可以使用关键字参数的形式生成命名元组:
因此,调用该方法相当于在可迭代对象前加上星号将其变为一系列参数。
_replace(**kwargs)
方法可以用于从另一个命名元组得到其部分值被替换后的副本:
最后,._asdict()
方法用于把命名元组对象转换为 OrderedDict ,即一种字典对象。
改进的命名元组
Python 中可以使用另一种方式得到命名元组,这种方式得到的命名元组更简洁,并且可以保存类型信息。
如果对以下涉及的 Python 语法有疑问,也可以暂时忽略。
这种类型的命名元组通过继承创建,并且使用类属性表示各个字段,代码为:
类型注解是必须的,否则无法通过语法检查。Python 中的类型注解都不会实际检查类型是否匹配,不过它会为编写和阅读代码提供一定方便。
它的使用方法也和前一种命名元组一致:
这种命名元组也具有以上所使用的一些特别的属性和方法。在实际使用时可以任意选取一种命名元组。个人比较推荐这一种,因为它的定义比较简洁易懂。