正小歪 BLOG

namedtuple —— 使用字符串动态生成类

Python 中的 namedtuple 是一个对 tuple 的加强机制,返回一个具有命名字段的 tuple 子类。

参数检查

def namedtuple(typename, filed_name, verbose=False, rename=False):
    pass

namedtuple 的参数由四个构成:

  • typename 是该元组的名字
  • field_name 是该元组成员的名字
  • verbose 是否输出构造的源码
  • rename 是否在 field_name 非法时候进行重命名
if isinstance(field_names, basestring):
    field_names = field_names.replace(',', ' ').split()
field_names = map(str, field_names)
typename = str(typename)

调用 namedtuple 的第一步是处理 field_name 参数,该参数可以是 basestring 也就是说可以是 str 或者 unicode,再把 field_name 转化成列表。在使用 basestring 类型时,可以用 , 或者 隔开。

if rename:
    seen = set()
    for index, name in enumerate(field_names):
        if (not all(c.isalnum() or c=='_' for c in name)
            or _iskeyword(name)
            or not name
            or name[0].isdigit()
            or name.startswith('_')
            or name in seen):
            field_names[index] = '_%d' % index
        seen.add(name)

如果把 renameTrue 将会重命名非法的的成员名字为「下划线 + 位置」,如果不使用 rename ,当非法成员存在时候会抛出异常。

for name in [typename] + field_names:
    # 全部为 str 类型
	if type(name) != str:
		raise TypeError('Type names and field names must be strings')

    # 不能只由数字和下划线
	if not all(c.isalnum() or c=='_' for c in name):
		raise ValueError('Type names and field names can only contain '
						 'alphanumeric characters and underscores: %r' % name)
    # 不能是关键字
	if _iskeyword(name):
		raise ValueError('Type names and field names cannot be a '
						 'keyword: %r' % name)
    # 不能以数字开头
	if name[0].isdigit():
		raise ValueError('Type names and field names cannot start with '
						 'a number: %r' % name)
seen = set()
for name in field_names:
    # 在 rename 为 False 时候 name 不能以下划线开始
	if name.startswith('_') and not rename:
		raise ValueError('Field names cannot start with an underscore: '
						 '%r' % name)
    # 不能存在重复成员
	if name in seen:
		raise ValueError('Encountered duplicate field name: %r' % name)
	seen.add(name)

以上是对 typenamefield_name 的类型检查,可以看出在使用时候应该避免以下情况:

  • 非 str
  • 只用下划线和数字
  • 关键字
  • 数字开头
  • 下划线开头
  • 重复成员

填充「类模板」

namedtuple 是继承自 tuple 的子类,具有他的一切特性,每个子类的代码都是在调用时候生成的,使用的是 Python 的字符串模板。

使用 typenamefield_name 填充字符串模板,生成代码

class_definition = _class_template.format(
    typename = typename,
    field_names = tuple(field_names),
    num_fields = len(field_names),
    arg_list = repr(tuple(field_names)).replace("'", "")[1:-1],
    repr_fmt = ', '.join(_repr_template.format(name=name)
                         for name in field_names),
    field_defs = '\n'.join(_field_template.format(index=index, name=name)
                           for index, name in enumerate(field_names))
)

定义一个 Point 参数有 x, y, z,打开 verbose,查看输出构建类。

>>> Point = namedtuple('Point', 'x y z', verbose=True)
class Point(tuple):
    'Point(x, y, z)'

    __slots__ = ()

    # 定义的成员
    _fields = ('x', 'y', 'z')

    # 实例化 namedstuple 的 Point
    def __new__(_cls, x, y, z):
        'Create new instance of Point(x, y, z)'
        return _tuple.__new__(_cls, (x, y, z))

    # 生成一个 Point 的实例
    @classmethod
    def _make(cls, iterable, new=tuple.__new__, len=len):
        'Make a new Point object from a sequence or iterable'
        result = new(cls, iterable)
        if len(result) != 3:
            raise TypeError('Expected 3 arguments, got %d' % len(result))
        return result

    # 显示信息
    def __repr__(self):
        'Return a nicely formatted representation string'
        return 'Point(x=%r, y=%r, z=%r)' % self

    # 把 field 转化成字典
    def _asdict(self):
        'Return a new OrderedDict which maps field names to their values'
        return OrderedDict(zip(self._fields, self))

    # 替换成员的值,返回一个新的实例
    def _replace(_self, **kwds):
        'Return a new Point object replacing specified fields with new values'
        result = _self._make(map(kwds.pop, ('x', 'y', 'z'), _self))
        if kwds:
            raise ValueError('Got unexpected field names: %r' % kwds.keys())
        return result

    # 把整个 namedtuple 实例序列化
    def __getnewargs__(self):
        'Return self as a plain tuple.  Used by copy and pickle.'
        return tuple(self)

    # 构造类的各个成员成描述符
    __dict__ = _property(_asdict)

    # 清除 OrderedDict 对数据进行 picker 化时候返回的状态
    def __getstate__(self):
        'Exclude the OrderedDict from pickling'
        pass

    # 定义 field 的属性为类属性,itemgetter 定义取值操作
    x = _property(_itemgetter(0), doc='Alias for field number 0')

    y = _property(_itemgetter(1), doc='Alias for field number 1')

    z = _property(_itemgetter(2), doc='Alias for field number 2')

至此,构建 Point 的代码已经填充完成,但是还需要使用解释器执行该代码片段,才能真正 load 进内存中

# 执行 exec 时候临时的 namespace
namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename,
                 OrderedDict=OrderedDict, _property=property, _tuple=tuple)
try:
    exec class_definition in namespace
except SyntaxError as e:
    raise SyntaxError(e.message + ':\n' + class_definition)

# 返回生成的类
result = namespace[typename]

try:
    # 把当前调用帧中的全局变量 __name__ 添加到生成类的 __module__ 中
    result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
    pass

return result

class_definition 在使用 exec 执行的时候放在临时生成的 namespace 中,需要从 namespace 获取变量。

最后就可以使用 namedtuple 生成的类,每个成员都是类级别的描述符,实例化后和普通类一样使用,由于元组的不可修改的,所以 namedtuple 的属性也是不可以修改。