Python函数式编程06 元组与命名元组

元组的基本概念

元组的基本操作

在 Python 中,使用圆括号括起逗号分隔的不同的数据项

(elem1, elem2, ..., elemn)

会被视为一个元组(tuple)。在不引起歧义的情况下,最外层的括号可以省略,例如:

>>> t1 = (1, 3, 4, 6) >>> type(t1) <class 'tuple'> >>> t2 = 2, ['1', 'a'] >>> type(t2) <class 'tuple'>

因此,要注意有些时候,一个元素后面不小心多加了一个逗号,它会被当做只有一个元素的元组处理,可能会导致类型错误等问题。

也可以使用 tuple 类从一个可迭代对象中构造元组。

给定一个元组,可以使用赋值语句将其中包含的元素分配给一系列变量,例如:

>>> x1, x2, x3, x4 = t1 >>> x1 1 >>> x3 4

这种操作称为元组的解包(unpack)。在解包时,变量的个数要和元组的元素个数一致,否则会导致错误。

如果解包时,只需要前面的几个元素,那么可以在最后一个变量前加上星号 * ,将解包后剩下的值塞进该变量中:

>>> x1, x2, *y = t1 >>> x1 1 >>> y [4, 6]

元组是一种不可变类型,任何尝试对元组元素的修改都会发生错误。元组仅支持 .count().index() 方法,分别用于统计某个元素出现的次数以及查找某个元素的位置。

实际上以上元组的特性,列表也能做到。看起来元组就是一个不能修改的列表,那为何 Python 要引入这种类型呢?

元组最大的特性也就是其不可变,而有些时候(例如在函数式编程中)并不需要改变元素的值,此时为了防止元素意外变动,就需要使用元组。例如在某些大型工程中函数众多,可能会有某个排序函数意外改变原有序列,如果使用元组就能提前发现问题所在。

元组这种不可变的特点使得它可以作为字典的键:

{(3, 8, 1): 'hello'}

而列表则不行。

在使用使用中,常用元组来表达某一个结构形式的数据,例如以下是一个列表元组,列表中的每一个元组在同一索引值处,表达的都是同一含义的数据:

[('a', 1, True), ('c', 2, False), ('k', 3, True)]

使用命名元组

以上使用元组来表达结构形式的数据,但这种结构只能通过索引来访问其元素。如果元组中的元素较多,不仅操作起来很麻烦,而且比较容易出错。

命名元组是一种特殊的元组,命名元组的元素既可以使用名称访问,也可以使用索引值访问,大大增加了元组的可读性。

命名元组并不是内置的数据类型,而是标准库的一部分,因此首先需要导入命名元组:

from collections import namedtuple

使用 namedtuple 的构造函数可以定义一个子类命名元组,其构造函数的完整形式为:

namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)

其中各参数的含义为:

  • typename 是返回的命名元组子类的类名,创建命名元组相当于创建了一个新类
  • field_names 是命名元组各元素的名称,是一个由字符串组成的列表,其中的字符串必须为合法的标识符;或者 field_names 也可以是一个字符串,各元素名称使用逗号或空格隔开
  • rename 设置为 True 时,如果 field_names 中包含保留关键字或重复的变量名,则会自动重命名为 _1_2
  • 如果给定 defaults 值,则它应该是一个由默认值组成的可迭代对象。由于具有默认值的参数必须位于没有默认值的任何参数之后,如果默认值少于命名元组的元素个数,默认值将应用于最右边的元素。如果默认值多于命名元组的元素个数,将引起错误
  • 如果设置了 module 参数,那么该类将位于该模块下,因此该自定义类的 __module__ 属性将被设置为该参数值

以下是一个简单的示例,构造了一个类型的命名元组,并由该类型的命名元组生成具体的元组对象:

Point = namedtuple('Point', ['x', 'y', 'z'])
p01 = Point(3, 4, 8)
print(p01)

命名元组对象可以通过访问元素的字段名称来访问其属性,这更加准确、方便:

>>>p01[0] 3 >>> p01.y 4

命名元组创建的类继承于元组,并且包含以下额外属性和方法:

._fields 类属性返回其所有字段名称,以下给出了一个示例,并注意 rename 参数的作用效果:

>>> SomeStruct = namedtuple('OneTuple', ['abc', 'def', 'ghi', 'abc'], defaults=['1', '2', '3'], rename=True) >>> SomeStruct._fields ('abc', '_1', 'ghi', '_3')

如果没有设置 rename 参数,则会直接发生错误。

._field_defaults 属性返回所有有默认值的字段及其默认值组成的字典。如果没有默认值,那么返回空字典。

._make(iterable) 用于从指定可迭代对象构建命名元组对象。这里需要注意的是,得到的命名元组参数和字段名是对应的,可以使用关键字参数的形式生成命名元组:

>>> Point(1, 2, z=3) Point(x=1, y=2, z=3)

因此,调用该方法相当于在可迭代对象前加上星号将其变为一系列参数。

_replace(**kwargs) 方法可以用于从另一个命名元组得到其部分值被替换后的副本:

>>> p02 = p01._replace(x=10, z=12) >>> p02 Point(x=10, y=4, z=12)

最后,._asdict() 方法用于把命名元组对象转换为 OrderedDict ,即一种字典对象。

改进的命名元组

Python 中可以使用另一种方式得到命名元组,这种方式得到的命名元组更简洁,并且可以保存类型信息。

如果对以下涉及的 Python 语法有疑问,也可以暂时忽略。

这种类型的命名元组通过继承创建,并且使用类属性表示各个字段,代码为:

from typing import NamedTuple

class Student(NamedTuple):
    name: str
    age: int
    score: float

类型注解是必须的,否则无法通过语法检查。Python 中的类型注解都不会实际检查类型是否匹配,不过它会为编写和阅读代码提供一定方便。

它的使用方法也和前一种命名元组一致:

>>> s = Student('Peter', 21, 62.0) >>> s Student(name='Peter', age=21, score=62.0)

这种命名元组也具有以上所使用的一些特别的属性和方法。在实际使用时可以任意选取一种命名元组。个人比较推荐这一种,因为它的定义比较简洁易懂。

京ICP备2021034974号
contact me by hello@frozencandles.fun