Python函数式编程06 元组处理技术

元组的基本概念

元组的基本操作

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

(elem1, elem2, ..., elemn)

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

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

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

>>> ls = [1, 2, 3, 4], >>> ls.append(5) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'tuple' object has no attribute 'append'

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

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

看起来元组只是一个不可变的列表,元组能实现的功能列表一般也能实现,那为何 Python 要引入这种类型呢?

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

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

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

而列表则不行。

在表示数据时,元组经常用于表达某一个结构形式的数据:元组中的每个元素都表示结构中的一个字段,元组使用位置而不是名字区分不同的字段。在之前的章节 中看到了 Python 的位置参数和关键字参数,它们恰好对应了元组和字典的两种使用思维:元组只关键数据的位置,而字典只关心数据的名称。

从这个角度上说,元组和列表在使用时的差别很大:元组通常视为由多个字段组成的一条数据,而列表通常视为多条数据构成的组。例如,以下是一个既含有列表又含有元组的典型数据:

city_coordinates = [
("Beijing", 39.9042, 116.4074),
("New York", 40.7128, -74.0060),
("Moscow", 55.7558, 37.6176),
("Paris", 48.8566, 2.3522),
("London", 51.5099, -0.1180),
("Tokyo", 35.6895, 139.6917),
]

所以,虽然 Python 列表可以存储各种类型的数值,但在使用时一般只存储某一特定类型的值,并且并不关系这些值的具体位置,处理列表的方式一般使用 for 循环处理所有元素,而较少使用索引与切片单独获取某一个或某一部分元素;元组则相反,对元组的遍历一般是没有意义的,但更关注每个位置对应值的含义。

既然说到了数据的处理,接下来介绍一种元组常用的处理方式。

元组解包

如果需要赋值的变量构成一个元组,而赋予的值是一个序列,这时赋值语句会将其中包含的元素分配给对应位置的变量,例如:

>>> t3 = (100, 200) >>> (x, y) = t3 >>> x 100 >>> y 200

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

因为元组是定长的序列,并且每个位置的元素都有特定含义,因此元组解包很适合处理一个元组中的元素。元组解包时,往往会省略最外层的括号,例如:

qut, rem = divmod(40, 3) import os dirname, filename = os.path.split(os.path.abspath(__file__))

在解包时,并不总需要使用元组内的所有数据,这时可以使用一个单独的下划线 _ 作为占位:

元组解包可以用于交换变量,可以不用使用第三个变量,并且写起来非常方便:

a, b = b, a

元组解包也能用到 for 循环内,直接处理每次迭代的值:

for i, e in enumerate(ls):
print(i, e)

元组解包可以应用到任何可迭代对象上,但被迭代的对象的元素数量必须要跟接受这些元素的元组长度一致:

a, b, c, d, e = map(lambda x: x ** 2, range(5))

因此元组解包有时也称为可迭代元素解包。

如果解包时,只需要用到其中的几个元素,那么可以在某一个变量前加上星号 * ,表示将解包后剩下的值塞进该变量中,组成一个列表:

first, second, *rest = ls

即便序列中只有两个元素,*rest 也可以正常工作,这时它的接收结果为空列表:

>>> first, second, *rest = range(10, 12) >>> first 10 >>> rest []

星号 * 前缀在解包时,可以作用于任意一个变量,但是一个解包中只能出现一个星号变量:

first, *res, last = ls

总之,星号变量会接收解包后剩余的所有值。

元组解包还可以处理嵌套表达式,只要元组的嵌套关系符合序列的嵌套关系:

students = [
("Alice", 25, (87, 92, 95)),
("Bob", 30, (72, 81, 86)),
("Charlie", 22, (92, 84, 78))
]

for name, age, (literature, science, art) in students:
print(f'{name} got {science} in sciece')

嵌套表达式内也可以采用星号变量处理剩余参数。

命名元组

使用命名元组

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

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

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

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