昨晚在阅读 Flask-Principal 文档的时候发现了一段代码不是很懂什么意思,而且发现还很有趣,源代码是这样的:

from collections import namedtuple
from functools import partial
... ...

BlogPostNeed = namedtuple('blog_post', ['method', 'value'])
EditBlogPostNeed = partial(BlogPostNeed, 'edit')

... ...
need = EditBlogPostNeed(unicode(post_id))


中间省略了很多无关主题的代码,这里我觉得有意思的地方有两个:

好似以前见过,但是一时居然说不上来什么意思,所以秉着好学的态度去 google (去年换工作之后就很少用x度了) 了一下。原来这两个函数这么有意思啊,partial 还好,我觉得更有意思的是 namedtuple。最先发现的资料是 python 的文档,毕竟是标准库中存在的函数,文档中的介绍是:

collections.namedtuple(typename, field_names[, verbose=False][, rename=False])


Returns a new tuple subclass named typename. The new subclass is used to create tuple-like objects that have fields accessible by attribute lookup as well as being indexable and iterable. Instances of the subclass also have a helpful docstring (with typename and field_names) and a helpful repr() method which lists the tuple contents in aname=value format.

翻译大意就是:

调用这个函数会返回一个命名为 'typename' 的元组子类。这个子类可以用来创建类似于元组的对象,这个对象可以索引访问,可以被迭代,也可以通过属性的方式访问;这还不够,对象还有一个有用的 docstring 和可以以 name=value 的形式列出元组内容的 repr 函数

哦,看完文档之后,发现原来就是一个快捷创建 POJO 的方式啊,很好,所以我马上就写了一段测试代码尝试了一下,结果就崩了

from collections import namedtuple

Person = namedtuple('Person', ['name', 'age', 'height', 'weight'])
person = Person()


运行一下,发现就报错了:

TypeError                                 Traceback (most recent call last)
<ipython-input-5-f3379562d574> in <module>()
      2 
      3 Person = namedtuple('Person', ['name', 'age', 'height', 'weight'])
----> 4 person = Person()

TypeError: __new__() takes exactly 5 arguments (1 given)


哦,原来初始化的时候一定要带满参数的,好,继续:

from collections import namedtuple

Person = namedtuple('Person', ['name', 'age', 'height', 'weight'])
person = Person(name='tyrael', age=25, height=182, weight=68)

for value in person:
    print value
for idx, value in enumerate(person):
    print idx, value
print "Hello, {}, after waste {} years foods you only get {} cm tall and {} KG".format(
    person.name, person.age, person.height, person.weight)


好,跑一下,好像没什么问题,继续探索,尝试改个名字:

from collections import namedtuple

Person = namedtuple('Person', ['name', 'age', 'height', 'weight'])
person = Person(name='tyrael', age=25, height=182, weight=68)

person.name = 'yylucifer'


欧耶,又搞砸了,问题很快就抛出来了:

AttributeError                            Traceback (most recent call last)
<ipython-input-13-e2c8e303bb55> in <module>()
      4 person = Person(name='tyrael', age=25, height=182, weight=68)
      5 
----> 6 person.name = 'yylucifer'

AttributeError: can't set attribute


看一下问题是不能设置属性,想一下,既然 namedtuple 是 tuple 的子类,那应该也像 tuple 一样一旦设值就不能修改了,很科学。

应用场景

好像没什么好玩了,于是就想了想,那这个 namedtuple 有什么用呢?除了用来快捷保存 key-value 之外。继续看完官网的文档,哇哦,不得了啊,人家给的场景简直了,用得非常得巧妙,将 map 和 namedtuple 结合起来之后,就可以实现非常 pythonic 的持久数据操作代码了。

先别说话,上个代码:

EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title, department, paygrade')

import csv
for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))):
    print emp.name, emp.title

import sqlite3
conn = sqlite3.connect('/companydata')
cursor = conn.cursor()
cursor.execute('SELECT name, age, title, department, paygrade FROM employees')
for emp in map(EmployeeRecord._make, cursor.fetchall()):
    print emp.name, emp.title


这两处都是读取持久化数据(csv/db),然后转化成 namedtuple,然后再通过属性来访问,非常得简洁。

默认方法

除了从 tuple 上继承的所有方法,namedtuple 还有 3 个额外的方法以及 1 个额外的属性:

classmethod somenamedtuple._make(iterable)

使用已有的序列或者迭代构建新的对象。

>>>
>>> t = [11, 22]
>>> Point._make(t)
Point(x=11, y=22)


somenamedtuple._asdict()

返回一个包含 属性名称和对应值 的有序字典

>>>
>>> p = Point(x=11, y=22)
>>> p._asdict()
OrderedDict([('x', 11), ('y', 22)])


somenamedtuple._replace(kwargs)

修改指定属性的值,返回修改后的新对象

>>>
>>> p = Point(x=11, y=22)
>>> p._replace(x=33)
Point(x=33, y=22)

>>> for partnum, record in inventory.items():
        inventory[partnum] = record._replace(price=newprices[partnum], timestamp=time.now())


somenamedtuple._fields

列举出所有属性名的字符串 tuple,在从已有的 namedtuple 创建新的 namedtuple 时特别有用。

>>> p._fields            # view the field names
('x', 'y')

>>> Color = namedtuple('Color', 'red green blue')
>>> Pixel = namedtuple('Pixel', Point._fields + Color._fields)
>>> Pixel(11, 22, 128, 255, 0)
Pixel(x=11, y=22, red=128, green=255, blue=0)


参考资料