1. namedtuple的优点
Python有专门的namedtuple容器类型,但似乎没有得到应有的重视。这是Python中那些缺乏关注但又令人惊叹的特性之一。
利用namedtuple可以手动定义类(class),除此之外,本节还会介绍namedtuple中其他有趣的特性。
那么namedtuple是什么,有什么特别之处呢?理解namedtuple的一个好方法是将其视为内置元组数据类型的扩展。
Python的元组是用于对任意对象进行分组的简单数据结构。元组也是不可变的,创建后就不能修改。来看一个简单的例子:
>>> tup = ('hello', object(), 42)>>> tup('hello', <object object at 0x105e76b70>, 42)>>> tup[2]42>>> tup[2] = 23TypeError:"'tuple' object does not support item assignment"
简单元组有一个缺点,那就是存储在其中的数据只能通过整数索引来访问。无法给存储在元组中的单个属性赋予名称,因而代码的可读性不高。
另外,元组是一种具有单例性质的数据结构,很难保证两个元组存有相同数量的字段和相同的属性,因此很容易因为不同元组之间的字段顺序不同而引入难以意识到的bug。
1.1 namedtuple上场
namedtuple旨在解决两个问题。
首先,与普通元组一样,namedtuple是不可变容器。一旦将数据存储在namedtuple的顶层属性中,就不能更新属性了。namedtuple对象上的所有属性都遵循“一次写入,多次读取”的原则。
其次,namedtuple就是具有名称的元组 。存储在其中的每个对象都可以通过唯一的(人类可读的)标识符来访问。因此不必记住整数索引,也无须采用其他变通方法,如将整数常量定义为索引的助记符。
下面来看看namedtuple:
>>> from collections import namedtuple>>> Car = namedtuple('Car' , 'color mileage')
namedtuple在Python 2.6被首次添加到标准库中。使用时需要导入collections 模块。上面的例子中定义了一个简单的Car 数据类型,含有color 和mileage 两个字段。
你可能想知道为什么本例中将字符串'Car' 作为第一个参数传递给namedtuple 工厂函数。
这个参数在Python文档中被称为typename,在调用namedtuple 函数时作为新创建的类名称。
由于namedtuple 并不知道创建的类最后会赋给哪个变量,因此需要明确告诉它需要使用的类名。namedtuple 会自动生成文档字符串和__repr__ ,其中的实现中会用到类名。
在这个例子中还有另外一个奇特的语法:为什么将字段作为'color mileage' 这样的字符串整体传递?
答案是namedtuple的工厂函数会对字段名称字符串调用split() ,将其解析为字段名称列表。分开来就是下面这两步:
>>> 'color mileage'.split()['color', 'mileage']>>> Car = namedtuple('Car', ['color', 'mileage'])
当然,如果更倾向于分开写的话,也可以直接传入带有字符串字段名称的列表。使用列表的好处是,在需要拆分成多行时可以更轻松地重新格式化代码:
>>> Car = namedtuple('Car', [... 'color',... 'mileage',... ])
无论以什么方式初始化,现在都可以使用Car 工厂函数创建新的“汽车”对象,其效果和手动定义Car 类并提供一个接受color 和mileage 值的构造函数相同:
>>> my_car = Car('red', 3812.4)>>> my_car.color'red'>>> my_car.mileage3812.4
除了通过标识符来访问存储在namedtuple中的值之外,索引访问仍然可用。因此namedtuple可以用作普通元组的替代品:
>>> my_car[0]'red'>>> tuple(my_car)('red', 3812.4)
元组解包和用于函数参数解包的* 操作符也能正常工作:
>>> color, mileage = my_car>>> print(color, mileage)red 3812.4>>> print(*my_car)red 3812.4
自动得到的namedtuple对象字符串形式也挺不错的,不用自己编写相关函数了:
>>> my_carCar(color='red' , mileage=3812.4)
与元组一样,namedtuple是不可变的。试图覆盖某个字段时会得到一个AttributeError 异常:
>>> my_car.color = 'blue'AttributeError: "can't set attribute"
namedtuple对象在内部是以普通的Python类实现的。当涉及内存使用时,namedtuple比普通类“更好”,它和普通元组的内存占用都比较少。
可以这么看:namedtuple适合在Python中以节省内存的方式快速手动定义一个不可变的类。
1.2 子类化namedtuple
因为namedtuple建立在普通Python类之上,所以还可以向namedtuple对象添加方法。例如,可以像其他类一样扩展namedtuple定义的类,为其添加方法和新属性。来看一个例子:
Car = namedtuple('Car', 'color mileage')class MyCarWithMethods(Car):def hexcolor(self):if self.color == 'red':return '#ff0000'else:return '#000000'
现在能够创建MyCarWithMethods 对象并调用hexcolor() 方法了,就像预期的那样:
>>> c = MyCarWithMethods('red', 1234)>>> c.hexcolor()'#ff0000'
这种方式可能有点笨拙,但适合构建具有不可变属性的类,不过也很容易带来其他问题。
例如,由于namedtuple内部的结构比较特殊,因此很难添加新的不可变 字段。另外,创建namedtuple类层次的最简单方法是使用基类元组的_fields 属性:
>>> Car = namedtuple('Car', 'color mileage')>>> ElectricCar = namedtuple(... 'ElectricCar', Car._fields + ('charge',))
结果符合预期:
>>> ElectricCar('red', 1234, 45.0)ElectricCar(color='red', mileage=1234, charge=45.0)
1.3 内置的辅助方法
除了_fields 属性,每个namedtuple实例还提供了其他一些有用的辅助方法。这些方法都以单下划线(_ )开头。单下划线通常表示方法或属性是“私有”的,不是类或模块的稳定公共接口的一部分。
然而在namedtuple中,下划线具有不同的含义。这些辅助方法和属性是namedtuple公共接口的一部分,以单下划线开头只是为了避免与用户定义的元组字段发生命名冲突。所以在需要时就尽管使用吧。
下面会介绍一些能用到这些namedtuple辅助方法的情形。我们从_asdict() 辅助方法开始,该方法将namedtuple的内容以字典形式返回:
>>> my_car._asdict()OrderedDict([('color', 'red'), ('mileage', 3812.4)])
这样在生成JSON输出时可以避免拼错字段名称:
>>> json.dumps(my_car._asdict())'{"color": "red", "mileage": 3812.4}'
另一个有用的辅助函数是_replace() 。该方法用于创建一个元组的浅副本,并能够选择替换其中的一些字段:
>>> my_car._replace(color='blue')Car(color='blue', mileage=3812.4)
最后介绍的是_make() 类方法,用来从序列或迭代对象中创建namedtuple的新实例:
>>> Car._make(['red', 999])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提供了一些有用的辅助方法,虽然这些方法以单下划线开头,但实际上是公共接口的一部分,可以正常使用。
