实用的(named tuple)命名元组
Python的创始人Guido van Rossum曾经提过一个建议:“不要过度的自己去构建数据结构,尽量去使用命名元组 (named tuple) 而不是对象,尽量使用简单的属性域,因为内置数据类型是你最好的朋友。”
那么什么是命名元组呢,要解释什么是命名元组时我觉得很有必要说明下为什么我们需要它以及它可以提供什么功能,那就自然明白什么是命名元组了。
我们知道在一些编程语言里,都有struct结构体这类数据类型,当我们对结构体对象进行赋值或者取值的时候可以直接使用"."运算符来操作。
但是Python里是否也可以用自带的数据类型做到这样的效果呢?
在回答这个问题之前,我想先用一个例子再说的更清楚一些,假设我有一堆庞大的多类型数据,为了节省空间我使用元组来进行保存,但是要知道数据一旦保存为元组后,访问其中的内容只能通过下标索引的方式去访问,数据一旦多了通过记住每个下标那就会显得相当困难,到底是第[3]下标还是第[9]下标呢,于是命名元组 (named tuple)就出现了,通过命名元组我们就可以直接使用"."运算符跟上对应的属性名获得对应的内容。
named tuple语法规则定义的格式如下:
collections.namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)
typename: 定义这个元组的名称
field_names: 这个元组元素的名称,可以有多种表达方式,如:'name1 name2' 或 'name1, name2' 或 ['name1', 'name2']
defaults=None: 默认值设置可以是None或者是设置一个迭代的默认值。
module: 如果有定义module,那么设置module参数后,命名元组的__module__的属性就是被设定的值。
rename=False: 如果元素名称中含有python的关键字,或者有重复元素名称出现时,设置为rename=True时,那些不符合规则的元素名称就会被系统更改为下划线加数字。比如: ['abc', 'def', 'ghi', 'abc']
会被替换为['abc', '_1', 'ghi', '_3']
因为,其中def被认为是python保留关键字,而另一个abc重复出现了第二次,所以都被替换为了'_1','_3'。
下面这个例子,让我们看看如何定义和使用一个命名元组。
from collections import namedtuple
# 按照语法规则定义如下,'mobile_product'是元组名,为了方便记忆也可以和前面的赋值变量同名都定义为product。
product = namedtuple('mobile_product', 'name, color, price')
# product是之前创建的一个命名元组对象,接下来就可以给这个对象进行传参赋值。
obj = product(name = 'iPhone', color = 'gray', price = 7000)
# 可以使用命名元组的特性用"."运算符像往常访问类方法和属性一样来提取属性值。
print("Name:{}, Color:{}, Price:{}".format(obj.name, obj.color, obj.price))
# 输出如下:
# Name:iPhone, Color:gray, Price:7000
named tuple还有一个非常好的一点是它与tuple是完全兼容的。也就是说,我们依然可以用索引去访问一个named tuple。
print("Name:{}, Color:{}, Price:{}".format(obj[0], obj[1], obj[2]))
# 输出如下:
# Name:iPhone, Color:gray, Price:7000
所以,named tuple比普通tuple具有更好的可读性,可以使代码更易于维护。同时与字典相比,又更加的轻量和高效。但是有一点需要注意,既然都叫元组,那么元组的特性就是不可变,自然在named tuple中的属性也都是不可变的。任何尝试改变其属性值的操作都是非法的。
from collections import namedtuple
product = namedtuple('mobile_product', 'name, color, price')
obj = product(name = 'iPhone', color = 'gray', price = 7000)
obj.name = 'HUAWEI'
# 输出如下:
# AttributeError Traceback (most recent call last)
# AttributeError: can't set attribute
因为named tuple命名元组除了拥有继承自tuple元组的所有方法之外,在Python 3.7版本中,还提供了额外的三个方法和两个属性,为了防止命名冲突,这些方法都会以单个下划线开头,分别是: _make(iterable)、_replace(**kwargs)、_asdict()、_fields、_fields_defaults。
前面有提到无论元组还是命名元组都是不可以修改值的,如果实在想要更改属性值时怎么办?那就可以使用_replace(**kwargs)方法,根据传入的关键词参数,替换named tuple的相关参数,然后返回一个新的named tuple。
下面的例子中,我们使用_replace()方法成功修改了两个参数值。
from collections import namedtuple
product = namedtuple('mobile_product', 'name, color, price')
obj = product(name = 'iPhone', color = 'gray', price = 7000)
# 使用_replace方法修改name和price参数值。
obj._replace(name = 'HUAWEI', price = 8000)
# 输出如下:
# mobile_product(name='HUAWEI', color='gray', price=8000)
我们还可以使用_make()方法批量给named tuple的元素赋值。
from collections import namedtuple
product = namedtuple('mobile_product', 'name, color, price')
value = ['iPhone', 'gray', 7000]
# 这里直接使用_make()方法把之前的value列表中的值以迭代的方式传值给命名元组中的每个元素。
obj = product._make(value)
obj
# 输出如下:
# mobile_product(name='iPhone', color='gray', price=7000)
引申:使用场景
named tuple最常用的还是在处理csv或者数据库返回的数据上。比如使用_make()方法配合map()函数式编程可以快速从csv文件中倒入到我们的命名元组结构中。
from collections import namedtuple
import csv
product = namedtuple('mobile_product', 'name, color, price')
for prds in map(product._make, csv.reader(open("products.csv", "rb"))):
print(prds.name, prds.color, prds, price)
分析下上面这个场景案例是如何执行的,我们都知道map()是一个高阶函数,它的参数可以接受一个function和list也就是map(function, list)形式,它可以把每个list里的元素放入function中去执行。
那么在上面这个例子中,我们的list就是从csv里读取所有数据,并把这些数据传给我们的function也就是product._make,让他以迭代的方式传值给命名元组中的每个元素。这种方法在处理大批量数据时定义数据结构时非常有用。