1. namedtuple的优点

Python有专门的namedtuple容器类型,但似乎没有得到应有的重视。这是Python中那些缺乏关注但又令人惊叹的特性之一。
利用namedtuple可以手动定义类(class),除此之外,本节还会介绍namedtuple中其他有趣的特性。
那么namedtuple是什么,有什么特别之处呢?理解namedtuple的一个好方法是将其视为内置元组数据类型的扩展。
Python的元组是用于对任意对象进行分组的简单数据结构。元组也是不可变的,创建后就不能修改。来看一个简单的例子:

  1. >>> tup = ('hello', object(), 42)
  2. >>> tup
  3. ('hello', <object object at 0x105e76b70>, 42)
  4. >>> tup[2]
  5. 42
  6. >>> tup[2] = 23
  7. TypeError:
  8. "'tuple' object does not support item assignment"

简单元组有一个缺点,那就是存储在其中的数据只能通过整数索引来访问。无法给存储在元组中的单个属性赋予名称,因而代码的可读性不高。
另外,元组是一种具有单例性质的数据结构,很难保证两个元组存有相同数量的字段和相同的属性,因此很容易因为不同元组之间的字段顺序不同而引入难以意识到的bug。

1.1 namedtuple上场

namedtuple旨在解决两个问题。
首先,与普通元组一样,namedtuple是不可变容器。一旦将数据存储在namedtuple的顶层属性中,就不能更新属性了。namedtuple对象上的所有属性都遵循“一次写入,多次读取”的原则。
其次,namedtuple就是具有名称的元组 。存储在其中的每个对象都可以通过唯一的(人类可读的)标识符来访问。因此不必记住整数索引,也无须采用其他变通方法,如将整数常量定义为索引的助记符。
下面来看看namedtuple:

  1. >>> from collections import namedtuple
  2. >>> Car = namedtuple('Car' , 'color mileage')

namedtuple在Python 2.6被首次添加到标准库中。使用时需要导入collections 模块。上面的例子中定义了一个简单的Car 数据类型,含有colormileage 两个字段。
你可能想知道为什么本例中将字符串'Car' 作为第一个参数传递给namedtuple 工厂函数。
这个参数在Python文档中被称为typename,在调用namedtuple 函数时作为新创建的类名称。
由于namedtuple 并不知道创建的类最后会赋给哪个变量,因此需要明确告诉它需要使用的类名。namedtuple 会自动生成文档字符串和__repr__ ,其中的实现中会用到类名。
在这个例子中还有另外一个奇特的语法:为什么将字段作为'color mileage' 这样的字符串整体传递?
答案是namedtuple的工厂函数会对字段名称字符串调用split() ,将其解析为字段名称列表。分开来就是下面这两步:

  1. >>> 'color mileage'.split()
  2. ['color', 'mileage']
  3. >>> Car = namedtuple('Car', ['color', 'mileage'])

当然,如果更倾向于分开写的话,也可以直接传入带有字符串字段名称的列表。使用列表的好处是,在需要拆分成多行时可以更轻松地重新格式化代码:

  1. >>> Car = namedtuple('Car', [
  2. ... 'color',
  3. ... 'mileage',
  4. ... ])

无论以什么方式初始化,现在都可以使用Car 工厂函数创建新的“汽车”对象,其效果和手动定义Car 类并提供一个接受colormileage 值的构造函数相同:

  1. >>> my_car = Car('red', 3812.4)
  2. >>> my_car.color
  3. 'red'
  4. >>> my_car.mileage
  5. 3812.4

除了通过标识符来访问存储在namedtuple中的值之外,索引访问仍然可用。因此namedtuple可以用作普通元组的替代品:

  1. >>> my_car[0]
  2. 'red'
  3. >>> tuple(my_car)
  4. ('red', 3812.4)

元组解包和用于函数参数解包的* 操作符也能正常工作:

  1. >>> color, mileage = my_car
  2. >>> print(color, mileage)
  3. red 3812.4
  4. >>> print(*my_car)
  5. red 3812.4

自动得到的namedtuple对象字符串形式也挺不错的,不用自己编写相关函数了:

  1. >>> my_car
  2. Car(color='red' , mileage=3812.4)

与元组一样,namedtuple是不可变的。试图覆盖某个字段时会得到一个AttributeError 异常:

  1. >>> my_car.color = 'blue'
  2. AttributeError: "can't set attribute"

namedtuple对象在内部是以普通的Python类实现的。当涉及内存使用时,namedtuple比普通类“更好”,它和普通元组的内存占用都比较少。
可以这么看:namedtuple适合在Python中以节省内存的方式快速手动定义一个不可变的类。

1.2 子类化namedtuple

因为namedtuple建立在普通Python类之上,所以还可以向namedtuple对象添加方法。例如,可以像其他类一样扩展namedtuple定义的类,为其添加方法和新属性。来看一个例子:

  1. Car = namedtuple('Car', 'color mileage')
  2. class MyCarWithMethods(Car):
  3. def hexcolor(self):
  4. if self.color == 'red':
  5. return '#ff0000'
  6. else:
  7. return '#000000'

现在能够创建MyCarWithMethods 对象并调用hexcolor() 方法了,就像预期的那样:

  1. >>> c = MyCarWithMethods('red', 1234)
  2. >>> c.hexcolor()
  3. '#ff0000'

这种方式可能有点笨拙,但适合构建具有不可变属性的类,不过也很容易带来其他问题。
例如,由于namedtuple内部的结构比较特殊,因此很难添加新的不可变 字段。另外,创建namedtuple类层次的最简单方法是使用基类元组的_fields 属性:

  1. >>> Car = namedtuple('Car', 'color mileage')
  2. >>> ElectricCar = namedtuple(
  3. ... 'ElectricCar', Car._fields + ('charge',))

结果符合预期:

  1. >>> ElectricCar('red', 1234, 45.0)
  2. ElectricCar(color='red', mileage=1234, charge=45.0)

1.3 内置的辅助方法

除了_fields 属性,每个namedtuple实例还提供了其他一些有用的辅助方法。这些方法都以单下划线(_ )开头。单下划线通常表示方法或属性是“私有”的,不是类或模块的稳定公共接口的一部分。
然而在namedtuple中,下划线具有不同的含义。这些辅助方法和属性是namedtuple公共接口的一部分,以单下划线开头只是为了避免与用户定义的元组字段发生命名冲突。所以在需要时就尽管使用吧。
下面会介绍一些能用到这些namedtuple辅助方法的情形。我们从_asdict() 辅助方法开始,该方法将namedtuple的内容以字典形式返回:

  1. >>> my_car._asdict()
  2. OrderedDict([('color', 'red'), ('mileage', 3812.4)])

这样在生成JSON输出时可以避免拼错字段名称:

  1. >>> json.dumps(my_car._asdict())
  2. '{"color": "red", "mileage": 3812.4}'

另一个有用的辅助函数是_replace() 。该方法用于创建一个元组的浅副本,并能够选择替换其中的一些字段:

  1. >>> my_car._replace(color='blue')
  2. Car(color='blue', mileage=3812.4)

最后介绍的是_make() 类方法,用来从序列或迭代对象中创建namedtuple的新实例:

  1. >>> Car._make(['red', 999])
  2. Car(color='red', mileage=999)

1.4 何时使用namedtuple

namedtuple能够更好地组织数据的结构,让代码更整洁、更易读。
例如,将格式固定、针对特定场景的数据类型(比如字典)替换为namedtuple能更清楚地表达开发者的意图。通常,当我尝试以这种方式重构时,就会神奇地为眼前的问题想出一个更好的解决方案。
用namedtuple替换非结构化的元组和字典还可以减轻同事的负担,因为namedtuple让数据在传递时在某种程度上做到了自说明(self documenting)。
另一方面,如果namedtuple不能帮我编写更整洁、更易维护的代码,那么我会尽量避免使用。像本书介绍的许多其他技术一样,滥用namedtuple会带来负面影响。
但只要仔细使用,namedtuples无疑能让Python代码更好、更可读。

1.5 关键要点

  • collection.namedtuple 能够方便地在Python中手动定义一个内存占用较少的不可变类。
  • 使用namedtuple能按照更易理解的结构组织数据,进而简化了代码。
  • namedtuple提供了一些有用的辅助方法,虽然这些方法以单下划线开头,但实际上是公共接口的一部分,可以正常使用。