起步

namedtuplecollections 模块下的一个功能,它是类工厂函数,能返回 tuple 子类,允许通过字段名向元组中取值,性能上接近于元组。

Person = namedtuple('Person', 'name age')p = Person(name='Tony', age=18)print(p.name)  # 'Tony'print(p.age)   # 18

我们来试着自己动手来实现这个 namedtuple 功能。因为这个功能需求明确,没什么模块依赖,通过自己的实现后再去看看真正在 cpython 里的源码就会比较清晰。

需求分析

对于代码 Person = namedtuple('Person', 'name age') 让它等价于:

class Person(tuple):    def __new__(cls, name, age):        return tuple.__new__(cls, (name, age))    @property    def name(self):        return self[0]    @property    def age(self):        return self[1]

那么有一种实现就是,拼凑成这样的代码块字符串,再通过 exec(code) 来创建类,旧版本(小于 3.7)的 namedtuple 在 cpython 中还真就是这么实现的。

PS: 重写了 __new__ 而不是 __init__ ,有一个原因是因为元组一旦创建就不可变。为了能够通过字段名取值,这里引入了 property 修饰符。

构造类的字符串模板创建类

基于这个思路一个简陋的就能写了出来:

# 类名称模板_class_template = '''class {typename}(tuple):    def __new__(_cls, {arg_list}):        return tuple.__new__(_cls, ({arg_list})){field_defs}'''# 属性模板_field_template = '''    @property    def {name}(self):        return self[{index}]'''def namedtuple(typename, field_names):    field_names = field_names.split()    class_definition = _class_template.format(        typename=typename,        arg_list=arg_list = repr(field_names).replace("'", "")[1:-1],        field_defs=''.join(_field_template.format(index=index, name=name)                               for index, name in enumerate(field_names))    )    namespace = {}    exec(class_definition, namespace)    return namespace[typename]# use demoPerson = namedtuple('Person', 'name age')p = Person(name='Tony', age=18)print(isinstance(p, tuple)) # Trueprint(p.name)               # Tonyprint(p.age)                # 18

cpython 中的 namedtuple 便是基于这个思路实现的,源码见:https://github.com/python/cpy...,这个实现方式一直沿用到了 3.6.x 。直到因为性能原因而进行了改版,PR见:https://github.com/python/cpy...

基于元类编程

改进后的 namedtuple 更能体现元类编程的思想,对性能也有明显的改善。我用简化的代码来展示改版后的 namedtuple 的工作内容:

def _tuplegetter(index):    @property    def _getter(self):        return self[index]    return _getterdef namedtuple(typename, field_names):    field_names = field_names.split()    arg_list = repr(field_names).replace("'", "")[1:-1]    s = f'def __new__(_cls, {arg_list}): return tuple.__new__(_cls, ({arg_list}))'    namespace = {}    exec(s, namespace)    __new__ = namespace['__new__']    class_namespace = {'__new__': __new__}    for index, name in enumerate(field_names):        class_namespace[name] = _tuplegetter(index)    result = type(typename, (tuple,), class_namespace)    return result

到此,一个简易版的 namedtuple 就结构就完成了,改进后的 exec 调用中只有一行代码,性能会更好,旧版本的还会额外 import 其他依赖。然后就是构造元类编程中类属性和方法了。属性的获取是通过写的 _getter 来完成,而实际上源码上会委托给 operator.itemgetter 函数。

总结

相信从本文中理解了 namedtuple 的设计和实现原理,再去阅读源代码,能更快的理解源码,起到事半功倍的效果。