Python中的“命名元组”是什么?

1218 投票
14 回答
651644 浏览
提问于 2025-04-15 23:32
  • 什么是命名元组,它们怎么用?
  • 我什么时候应该用命名元组,而不是普通元组,或者反过来?
  • 有没有“命名列表”?(也就是可以改变的命名元组)

关于最后一个问题,具体可以参考 Python中可变命名元组的存在?

14 个回答

133

namedtuple 是一个工厂函数,用来创建一个元组类。通过这个类,我们可以创建可以通过名字来调用的元组。

import collections

#Create a namedtuple class with names "a" "b" "c"
Row = collections.namedtuple("Row", ["a", "b", "c"])   

row = Row(a=1,b=2,c=3) #Make a namedtuple from the Row class we created

print row    #Prints: Row(a=1, b=2, c=3)
print row.a  #Prints: 1
print row[0] #Prints: 1

row = Row._make([2, 3, 4]) #Make a namedtuple from a list of values

print row   #Prints: Row(a=2, b=3, c=4)
188

什么是命名元组?

命名元组就是一种元组。

它能做元组能做的所有事情。

但它不仅仅是一个普通的元组。

它是一个特定的元组子类,可以根据你的需求编程创建,具有命名的字段和固定的长度。

比如,这段代码创建了一个元组的子类,除了长度固定(在这个例子中是三),它可以在任何需要元组的地方使用而不会出错。这被称为里氏替换原则。

在Python 3.6中新增,我们可以用类定义和typing.NamedTuple来创建命名元组:

from typing import NamedTuple

class ANamedTuple(NamedTuple):
    """a docstring"""
    foo: int
    bar: str
    baz: list

上面的代码和collections.namedtuple是一样的,除了上面还有类型注解和文档字符串。下面的代码在Python 2及以上版本中可用:

>>> from collections import namedtuple
>>> class_name = 'ANamedTuple'
>>> fields = 'foo bar baz'
>>> ANamedTuple = namedtuple(class_name, fields)

这段代码实例化了它:

>>> ant = ANamedTuple(1, 'bar', [])

我们可以查看它并使用它的属性:

>>> ant
ANamedTuple(foo=1, bar='bar', baz=[])
>>> ant.foo
1
>>> ant.bar
'bar'
>>> ant.baz.append('anything')
>>> ant.baz
['anything']

更深入的解释

要理解命名元组,首先需要知道什么是元组。元组本质上是一个不可变的(不能在内存中直接修改)列表。

下面是你可能如何使用普通元组的例子:

>>> student_tuple = 'Lisa', 'Simpson', 'A'
>>> student_tuple
('Lisa', 'Simpson', 'A')
>>> student_tuple[0]
'Lisa'
>>> student_tuple[1]
'Simpson'
>>> student_tuple[2]
'A'

你可以通过可迭代解包来扩展元组:

>>> first, last, grade = student_tuple
>>> first
'Lisa'
>>> last
'Simpson'
>>> grade
'A'

命名元组是允许通过名称而不是索引来访问其元素的元组!

你可以这样创建一个命名元组:

>>> from collections import namedtuple
>>> Student = namedtuple('Student', ['first', 'last', 'grade'])

你也可以用一个字符串,名字之间用空格分开,这样更易读:

>>> Student = namedtuple('Student', 'first last grade')

如何使用命名元组?

你可以做所有元组能做的事情(见上文),还可以做以下事情:

>>> named_student_tuple = Student('Lisa', 'Simpson', 'A')
>>> named_student_tuple.first
'Lisa'
>>> named_student_tuple.last
'Simpson'
>>> named_student_tuple.grade
'A'
>>> named_student_tuple._asdict()
OrderedDict([('first', 'Lisa'), ('last', 'Simpson'), ('grade', 'A')])
>>> vars(named_student_tuple)
OrderedDict([('first', 'Lisa'), ('last', 'Simpson'), ('grade', 'A')])
>>> new_named_student_tuple = named_student_tuple._replace(first='Bart', grade='C')
>>> new_named_student_tuple
Student(first='Bart', last='Simpson', grade='C')

有位评论者问:

在一个大型脚本或程序中,通常在哪里定义命名元组?

namedtuple创建的类型基本上是你可以用简单的快捷方式创建的类。把它们当作类来对待。在模块级别定义它们,以便pickle和其他用户可以找到它们。

以下是一个在全局模块级别的工作示例:

>>> from collections import namedtuple
>>> NT = namedtuple('NT', 'foo bar')
>>> nt = NT('foo', 'bar')
>>> import pickle
>>> pickle.loads(pickle.dumps(nt))
NT(foo='foo', bar='bar')

这段代码演示了查找定义失败的情况:

>>> def foo():
...     LocalNT = namedtuple('LocalNT', 'foo bar')
...     return LocalNT('foo', 'bar')
... 
>>> pickle.loads(pickle.dumps(foo()))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
_pickle.PicklingError: Can't pickle <class '__main__.LocalNT'>: attribute lookup LocalNT on __main__ failed

为什么/什么时候应该使用命名元组而不是普通元组?

当用命名元组可以让你的代码更清晰时,就使用它们。

如果你本来会使用一个没有功能的对象,只有不变的数据属性,那么你可以用命名元组代替这个对象。

你也可以对它们进行子类化以添加功能,例如

class Point(namedtuple('Point', 'x y')):
    """adding functionality to a named tuple"""
        __slots__ = ()
        @property
        def hypot(self):
            return (self.x ** 2 + self.y ** 2) ** 0.5
        def __str__(self):
            return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

为什么/什么时候应该使用普通元组而不是命名元组?

从命名元组切换到普通元组可能会导致代码质量下降。最初的设计决策主要是考虑额外代码的成本是否值得在使用元组时提高可读性。

命名元组和普通元组在内存使用上没有区别。

有没有什么“命名列表”(命名元组的可变版本)?

你可能在寻找一种实现了静态大小列表所有功能的插槽对象,或者一种像命名元组一样工作的子类列表(并且以某种方式阻止列表改变大小)。

下面是一个扩展的例子,可能甚至符合里氏替换原则:

from collections import Sequence

class MutableTuple(Sequence): 
    """Abstract Base Class for objects that work like mutable
    namedtuples. Subclass and define your named fields with 
    __slots__ and away you go.
    """
    __slots__ = ()
    def __init__(self, *args):
        for slot, arg in zip(self.__slots__, args):
            setattr(self, slot, arg)
    def __repr__(self):
        return type(self).__name__ + repr(tuple(self))
    # more direct __iter__ than Sequence's
    def __iter__(self): 
        for name in self.__slots__:
            yield getattr(self, name)
    # Sequence requires __getitem__ & __len__:
    def __getitem__(self, index):
        return getattr(self, self.__slots__[index])
    def __len__(self):
        return len(self.__slots__)

使用时,只需子类化并定义__slots__

class Student(MutableTuple):
    __slots__ = 'first', 'last', 'grade' # customize 


>>> student = Student('Lisa', 'Simpson', 'A')
>>> student
Student('Lisa', 'Simpson', 'A')
>>> first, last, grade = student
>>> first
'Lisa'
>>> last
'Simpson'
>>> grade
'A'
>>> student[0]
'Lisa'
>>> student[2]
'A'
>>> len(student)
3
>>> 'Lisa' in student
True
>>> 'Bart' in student
False
>>> student.first = 'Bart'
>>> for i in student: print(i)
... 
Bart
Simpson
A
1506

命名元组其实是简单易用、轻量级的对象类型。你可以像使用对象一样通过变量来引用命名元组的实例,也可以用普通元组的方式来访问它们。它们的用法类似于 struct 或其他常见的记录类型,但不同的是,命名元组是不可变的。命名元组是在 Python 2.6 和 Python 3.0 中引入的,不过在 Python 2.4 也有一个实现的例子

举个例子,通常我们用一个元组 (x, y) 来表示一个点。这会导致代码像下面这样:

pt1 = (1.0, 5.0)
pt2 = (2.5, 1.5)

from math import sqrt
line_length = sqrt((pt1[0]-pt2[0])**2 + (pt1[1]-pt2[1])**2)

使用命名元组后,代码会变得更易读:

from collections import namedtuple
Point = namedtuple('Point', 'x y')
pt1 = Point(1.0, 5.0)
pt2 = Point(2.5, 1.5)

from math import sqrt
line_length = sqrt((pt1.x-pt2.x)**2 + (pt1.y-pt2.y)**2)

不过,命名元组仍然和普通元组兼容,所以下面的代码依然可以正常工作:

Point = namedtuple('Point', 'x y')
pt1 = Point(1.0, 5.0)
pt2 = Point(2.5, 1.5)

from math import sqrt
# use index referencing
line_length = sqrt((pt1[0]-pt2[0])**2 + (pt1[1]-pt2[1])**2)
 # use tuple unpacking
x1, y1 = pt1

因此,在你认为对象表示法能让代码更符合 Python 风格并更易读的地方,应该使用命名元组而不是普通元组。我个人开始用它们来表示非常简单的值类型,特别是在作为函数参数传递时。这样可以让函数更易读,而不需要看到元组打包的上下文。

此外,你还可以用命名元组替代那些没有函数、只有字段的普通 不可变。你甚至可以把命名元组类型用作基类:

class Point(namedtuple('Point', 'x y')):
    [...]

不过,和元组一样,命名元组的属性也是不可变的:

>>> Point = namedtuple('Point', 'x y')
>>> pt1 = Point(1.0, 5.0)
>>> pt1.x = 2.0
AttributeError: can't set attribute

如果你想要能够改变值,就需要使用其他类型。有一个很方便的可变记录类型的例子,可以让你给属性设置新值。

>>> from rcdtype import *
>>> Point = recordtype('Point', 'x y')
>>> pt1 = Point(1.0, 5.0)
>>> pt1 = Point(1.0, 5.0)
>>> pt1.x = 2.0
>>> print(pt1[0])
    2.0

不过,我不知道有没有“命名列表”这种形式可以让你添加新字段。在这种情况下,你可能更想用字典。命名元组可以通过 pt1._asdict() 转换为字典,返回的结果是 {'x': 1.0, 'y': 5.0},并且可以使用所有常见的字典操作。

如前所述,你应该查看文档,获取更多信息,这些例子就是从文档中提取的。

撰写回答