【Python 高级特性】深入 NamedTuple 命名元组

介绍

和元组 tuple 一样,NamedTuple 也是不可变数据类型,创建之后就不能改变内容。

如其名,和 tuple 的区别在于“Named”,即"命名"。NamedTuple 不像数组那样使用下标读写,反而和类相似,使用 . 来读写。

基本语法

创建 NamedTuple 的函数定义

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

参数说明:

  • typename:新创建的类的名称。
  • field_names:字段名称列表。必须是有效的 Python 变量名称,且不能以下划线开头。
  • rename:是否自动转换无效字段名。
  • defaults:字段默认值列表。
  • module:__module__ 的值。

    使用教程

    创建

    首先看看如何创建命名元组。以 Point(代表二维坐标中的一个点)为例:

    # 导包
    from collections import namedtuple
    # 创建普通元组
    point = (22, 33)
    print(point) # 输出:(22, 33)
    # 创建命名元组
    Point = namedtuple('Point', 'x y')
    point_A = Point(22, 33)
    print(point_A) # 输出:Point(x=22, y=33)
    

    重点是这两句话

    Point = namedtuple('Point', 'x y')
    point_A = Point(22, 33)
    

    需要注意,namedtuple() 是用来创建类的,不是创建对象实例!

    我们先用 namedtuple 创建了一个名为 Point,有两个字段 x、y 的子类,然后将这个类赋给 Point 变量。

    然后 Point(22, 33) 就是普通的 new 的语法。

    类似于如下代码:

    class Point:
    	def __init__(self, x, y):
    		self.x = x
    		self.y = y
    point_A = Point(22, 33)
    

    创建命名元组对象时,也可以使用位置参数

    a = Point(1, 2)
    b = Point(y=2, x=1)
    a == b # >>> True
    

    field_names 参数用来设置命名元组字段名,有三种风格可以选择。

    下面几种都是等价写法:

    Point = namedtuple('Point', 'x y')
    Point = namedtuple('Point', 'x,y')
    Point = namedtuple('Point', ['x', 'y'])
    # 下面都是合法代码
    # 中间允许存在任意空白字符
    Point = namedtuple('Point', 'x,   \t\t\t\n\n y')
    Point = namedtuple('Point', 'x   \t\t\t\n\n y')
    # 元组也可以
    Point = namedtuple('Point', ('x', 'y'))
    # 事实上只要是可迭代都行
    def fields():
    	yield 'x'
    	yield 'y'
    Point = namedtuple('Point', fields())
    

    使用

    命名元组首先是一个元组,元组能怎么用,命名元组当然也可以。

    print(point_A[0])
    print(point_A[1])
    print(*point_A) # tuple unpack
    # 输出
    """
    22
    33
    22 33
    """
    

    然后是命名元组的特殊用法:

    print(point_A.x)
    print(point_A.y)
    # 输出
    """
    22
    33
    """
    

    常用方法

    namedtuple 创建的类还附赠了一些实用方法:

    Point._make(iterable) # 从某个序列创建命名元组
    point._asdict() # 转成字典
    point._replace(**kargs) # 返回一个新元组,新元组里的指定字段被替换为指定值
    point._fields # 列出字段名
    point._field_defaults # 列出字段默认值
    

    设置默认值

    可以为命名元组的字段设置默认值,只需要在创建类的时候传入 defaults 参数即可。

    # 四维向量
    # 默认值为 Vector4D(0, 0, 0, 0)
    Vector4 = namedtuple('Vector4D', 'x y z w', defaults=(0, 0, 0, 0))
    v1 = Vector4()
    v2 = Vector4(1)
    v3 = Vector4(1, 2, w=4)
    print(v1)
    print(v2)
    print(v3)
    # 输出
    """
    Vector4D(x=0, y=0, z=0, w=0)
    Vector4D(x=1, y=0, z=0, w=0)
    Vector4D(x=1, y=2, z=0, w=4)
    """
    

    默认值的数量可以小于字段数,表示为右边 n 个参数设置默认值。

    Foo = namedtuple('Foo', 'a b c d', defaults=(1, 2))
    print(Foo(22, 33))
    print(Foo())
    # 输出
    """
    Foo(a=22, b=33, c=1, d=2)
    Traceback (most recent call last):
      File "D:\TempCodeFiles\named_tuple.py", line 6, in  print(Foo())
    TypeError: Foo.__new__() missing 2 required positional arguments: 'a' and 'b'
    """
    

    更好的表示方式

    namedtuple() 的写法既不直观,也不优雅。Python 3.5 新增了一种更好的写法:

    # >= Python 3.5
    from typing import NamedTuple
    class PointA(NamedTuple):
    	x: int = 0
    	y: int = 0
    # >= Python 2
    from collections import namedtuple
    PointB = namedtuple('PointB', 'x y', defaults=(0, 0))
    print(PointA(2, 3) == PointB(2, 3)) # 输出:True
    

    继承并扩展 NamedTuple

    namedtuple() 返回的是一个正常的类。既然它是一个类,当然也可以被继承。

    创建一个 Point 命名元组,增加一个方法,求两点距离。

    # >= Python 3.5
    class Point(NamedTuple):
    	x: int = 0
    	y: int = 0
        
    	def distance(self, p) -> float:
    		return math.sqrt((self.x - p.x) ** 2 + (self.y - p.y) ** 2)
    # >= Python 2
    class Point(namedtuple('Point', 'x y', defaults=(0, 0))):
    	def distance(self, p) -> float:
    		return math.sqrt((self.x - p.x) ** 2 + (self.y - p.y) ** 2)
    a = Point()
    b = Point(3, 2)
    print(a, b)
    print(a.distance(b))
    

    应用

    读 csv 文件

    以读入一个储存英语单词的 csv 文件为例。

    import csv
    from collections import namedtuple
    # 定义命名元组
    # 按照 csv 列名来定义字段
    Word = namedtuple('Word', 'word, type, chs_def, eng_ch, context, example')
    file_path = r'C:\Users\ZhouXiaokang\Desktop\单词 Vol 1 Ch 1 Ep 2.csv'
    with open(file_path, 'r', encoding='utf-8') as f:
    	reader = csv.reader(f)
    	next(reader) # 跳过标题行
    	for word in map(Word._make, reader):
    		print(f'{word.word} {word.type}. {word.chs_def} | 例:{word.context}')
    

    输出

    chirp n&v. (鸟、昆虫)啾啾叫,发唧唧声 | 例:(*chirp* *chirp* *chirp*)
    screech v. (车辆、汽车轮胎)发出刺耳声 | 例:(*screech*)
    Shiroko term. 白子 | 例:
    mug v. 对…行凶抢劫 | 例:You didn't get mugged, did you?
    faint v. 晕厥;晕倒 | 例:What's that? You fainted from hunger?
    ......
    

    作为字典的代替品表示数据

    相对于字典的优势:

    1.快、小

    2..field 比 ['field'] 更清晰

    以下源码摘自 baidupcs_py 库:

    class PcsFile(NamedTuple):
        """
        A Baidu PCS file
        path: str  # remote absolute path
        is_dir: Optional[bool] = None
        is_file: Optional[bool] = None
        fs_id: Optional[int] = None  # file id
        size: Optional[int] = None
        md5: Optional[str] = None
        block_list: Optional[List[str]] = None  # block md5 list
        category: Optional[int] = None
        user_id: Optional[int] = None
        ctime: Optional[int] = None  # server created time
        mtime: Optional[int] = None  # server modifed time
        local_ctime: Optional[int] = None  # local created time
        local_mtime: Optional[int] = None  # local modifed time
        server_ctime: Optional[int] = None  # server created time
        server_mtime: Optional[int] = None  # server modifed time
        shared: Optional[bool] = None  # this file is shared if True
        """
        path: str  # remote absolute path
        is_dir: Optional[bool] = None
        is_file: Optional[bool] = None
        fs_id: Optional[int] = None  # file id
        size: Optional[int] = None
        md5: Optional[str] = None
        block_list: Optional[List[str]] = None  # block md5 list
        category: Optional[int] = None
        user_id: Optional[int] = None
        ctime: Optional[int] = None  # server created time
        mtime: Optional[int] = None  # server modifed time
        local_ctime: Optional[int] = None  # local created time
        local_mtime: Optional[int] = None  # local modifed time
        server_ctime: Optional[int] = None  # server created time
        server_mtime: Optional[int] = None  # server modifed time
        shared: Optional[bool] = None  # this file is shared if True
        rapid_upload_info: Optional[PcsRapidUploadInfo] = None
        dl_link: Optional[str] = None
        @staticmethod
        def from_(info) -> "PcsFile":
            return PcsFile(
                path=info.get("path"),
                is_dir=info.get("isdir") == 1,
                is_file=info.get("isdir") == 0,
                fs_id=info.get("fs_id"),
                size=info.get("size"),
                md5=info.get("md5"),
                block_list=info.get("block_list"),
                category=info.get("category"),
                user_id=info.get("user_id"),
                ctime=info.get("ctime"),
                mtime=info.get("mtime"),
                local_ctime=info.get("local_ctime"),
                local_mtime=info.get("local_mtime"),
                server_ctime=info.get("server_ctime"),
                server_mtime=info.get("server_mtime"),
                shared=info.get("shared"),
            )
    

    源码

    见 Github。

    关键部分在这里:

     # Build-up the class namespace dictionary
        # and use type() to build the result class
        # 收集类的方法、字段等
        class_namespace = { '__doc__': f'{typename}({arg_list})',
            '__slots__': (),
            '_fields': field_names,
            '_field_defaults': field_defaults,
            '__new__': __new__,
            '_make': _make,
            '__replace__': _replace,
            '_replace': _replace,
            '__repr__': __repr__,
            '_asdict': _asdict,
            '__getnewargs__': __getnewargs__,
            '__match_args__': field_names,
        }
        for index, name in enumerate(field_names):
            doc = _sys.intern(f'Alias for field number {index}')
            class_namespace[name] = _tuplegetter(index, doc)
        # 创建新类
        result = type(typename, (tuple,), class_namespace) 

    type() 函数传入一个参数,用来获取对象的类;如果传入三个参数,就变成了动态创建类,相当于 class 的动态写法。

    class Foo:
        def hello(self):
            print('Hello')
    # 等价于
    def hello(self):
        print('Hello')
    Foo = type('Foo', (object,), {'hello': hello})
    

    参考文章

    1. https://docs.python.org/zh-cn/3/library/collections.html#collections.namedtuple
    2. https://realpython.com/python-namedtuple/