Python 中 NamedTuple 的理解

2021-12-04

namedtuple(具名元组)和与 tuple 通过坐标位置辨认属性相比,可读性更好。

namedtuple(具名元组)自带属性名称,对比下面两个:

t1 = ('Alice', 12500, 2)
t2 = Employee(name='Alice', salary=12500, grade=2)

namedtuple 可以使用在 tuple 的任何地方。

你可以这么理解:namedtuple 之于 tuple 就像 dict 之于 list,为每个元素起了名字,使用名字操作元素,更加方便、好记。

使用场景

当你打算抽象出一个类,而你设计的这个类只有属性而没有方法,仅仅为了保存数据,而且不做进一步修改。那么你可以考虑使用 namedtuple,将大大减少你的代码量。

简单示例

下面的几行代码简单展示了 namedtuple 的基本用法:

t 变量指向一个新生成的类,类名叫做 T

>>> t=collections.namedtuple('T',["a","b","c","d"])

实例化出 T 的对象,叫 t1

>>> t1=t(a=1,b=2,c=3,d=4)
>>> print(t1)
T(a=1, b=2, c=3, d=4)

参数不够就报错

>>> t1=t(a=1, b=2, c=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() missing 1 required positional argument: 'd'

如何访问实例的成员

>>> t1.a   # 点号表示法
1
>>> t1[1]  # 可索引
2
>>> for item in t1: print(item)  # 可遍历
...
1
2
3
4
>>> A,B,C,D = t1   # 可拆包
>>> A
1
>>>

因为 namedtuple 从 tuple 派生而来,所以继承了 tuple ,乃至 sequence 的特性。

上面例子里的点号表示法,十分方便,因为 dict 类型并不支持这种访问成员的方法。

工厂函数 namedtuple()

之所以叫工厂函数,是因为它不返回具体的实例,而是动态生成一个新的类,可以用这个新类来创建实例。

为什么要使用工厂函数而不是直接提供一个类?因为 namedtuple 要让用户自定义类型的名称,这个名称不能预先定义,所以要先动态生成一个类,再用这个类生成自定义类型的tuple实例。

与之相比,其他数据类型都从同一个类,实例化而来:

>>> d = dict(a=1, b=2)
>>> d
{'a': 1, 'b': 2}

namedtuple() 的参数列表

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

下面逐个解释参数(单独星号 * 的含义,见 这篇文章)。

类名 typename

必选参数。定义了生产出的类的名称,也就是新生成 tuple 的 type,所以叫 typename. 看下面的例子:

>>> import collections
>>> new_class = collections.namedtuple('aa',['a'])
>>> new_class.__class__
<class 'type'>           # 从type派生出来的
>>> new_class.__name__   # 类的名称
'aa'

这里创建了 aa 这个新类,但是这个新类被 new_class 变量所引用。就像

x=2

在使用 x 这个变量时,就在指代 2 这个字面量。当我使用 new_class 变量时,实际上在指代 aa 这个新类。下面我们用这个新类来创建对象。

>>> aa_object = new_class(a=1)
>>> aa_object.__class__
<class '__main__.aa'>      # 从 aa 实例化
>>> type(aa_object)
<class '__main__.aa'>      # aa 类型
>>> isinstance(aa_object,new_class)
True
>>>

很多关于 namedtuple 的教程里,喜欢把工厂函数 namedtuple()返回的类赋给同名的变量,比如

Card = collections.namedtuple('Card', ['rank', 'suit'])

或者

Employee = collections.namedtuple('Employee', ['name', 'city', 'salary'])

这样会引起误解,会以为对变量名又什么要求。其实并没有, 只是指向新类的一个变量而已。比如下面的例子中,更换了变量名称。

>>> new_2_class = new_class
>>> new_2_class(a=1).__class__
<class '__main__.aa'>
>>>

我们用新的变量指代,仍然指向同一个类。对于 Python 变量的赋值,可以参考 这篇文章

字段列表 field_names

定义 tuple 的字段名称,字段列表有序列和字符串两种形式:

字符串的序列

sequence of string 即可,比如

namedtuple('T',["a","b","c","d"])
namedtuple('T',("a","b","c","d"))

都可以

空格或逗号分隔

为了方便,还可以将所有字段名称放在同一个长的字符串里,用空格或者逗号分隔。比如:

namedtuple('T',"a b c d"])
namedtuple('T',"a,b,c,d")

非法的字段名称

python 的预留关键字不能用作字段名称:

>>> collections.namedtuple('T',"for,b,c,d")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "xx.py", line 393, in namedtuple
    raise ValueError('Type names and field names cannot be a '
ValueError: Type names and field names cannot be a keyword: 'for'

字段名称不能以下划线开头 (_),因为下划线有其他预留的用途。

rename

非必选参数,布尔值。True 表示允许自动重命名,(上节)[#invalid] 中非法字段名会被自动改成下划线开头的字段名(原因在 这一节 中有解释)。还是用 (上节)[#invalid] 的例子:

>>> collections.namedtuple('T',"for,b,c,d",rename=True)._fields
('_0', 'b', 'c', 'd')

# for 被自动更名为 _0

defaults

注意末尾有个"s",非必选参数,各字段的默认值。接受的取值是可迭代对象(iterable)。比如

>>> t=collections.namedtuple('T',"a,b,c,d")   # 没有定义默认值
>>> t()       #报错
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() missing 4 required positional arguments: 'a', 'b', 'c', and 'd'
>>> t=collections.namedtuple('T',"a,b,c,d",defaults=[1,2,3,4])  # 定义了默认值
>>> t()    #自动赋值
T(a=1, b=2, c=3, d=4)
>>>

如果 defaults 提供了 dict 类型,那么默认值取自 key :

>>> t=collections.namedtuple('T',"a b c d",defaults={"a":4,"b":3,"c":2,"d":1})
>>> t()
T(a='a', b='b', c='c', d='d')
>>>

如果 defaults 提供了 str 类型,是每个字母分别赋值:

>>> t=collections.namedtuple('T',"a b c d",defaults="1234")
>>> t()
T(a='1', b='2', c='3', d='4')

对不上的情况

如果 defaults 提供的数量和前面 fields 的数量对不上,那么一定有些字段是需要在实例化的时候靠位置定义的,比如说:

>>> t=collections.namedtuple('T',"a b c d",defaults="34")

default 提供的数值少两个,那么实例化的时候,必然有两个参数需要用位置来赋值、

又因为位置参数必须先于关键字参数,所以 defaults 值是从右往左一一赋值。

>>> t=collections.namedtuple('T',"a b c d",defaults="34")
>>> t(1,2)
T(a=1, b=2, c='3', d='4')
>>>

自带函数

namedtuple 是派生自 tuple 的子类,除了继承了 tuple 的所有特性外,还自带了一些特有的方法方便使用。

为了避免和用户自定义的属性冲突,自带的方法名都以下划线开头,这也是为什么 fields 字段不能使用下划线开头的字段名的原因。

_make()

输入一个可迭代对象,可以实例化一个对象。我觉得没什么用,直接用类名实例化即可。如果数据保存在序列里,那么用星号 unpack 即可。

>>> t=collections.namedtuple('T',"a b c d")
>>> t(*[1,2,3,4])
T(a=1, b=2, c=3, d=4)
>>>

_asdict()

输出一个 dict 形式:

>>> t1
T(a=1, b=2, c=3, d=4)
>>> t1._asdict()
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
>>>

捐助本站

为了保证阅读体验,本站不安放广告。但是,租用服务器和编写文章需要个人资金和时间的投入。

如果您觉得文章对您有用,请考虑捐助小站(金额不限),以期待更多原创文章。捐助记录

本站是个人网站,若无特别说明,文章均为原创,并采用 署名协议 CC-BY-NC 授权。
欢迎转载,惟请保留原文链接,且不得用于商业用途。