Python namedtuple使用

1,808 阅读4分钟

文章的Python版本为3.5

namedtuple使用

这个函数用来创建tuple子类,可以用名称字段。例如下面的代码演示它的特效

  1. collections中导入,函数第一个参数为类名,第二个为要定义的名称字段,它们用逗号分割,放入一个字符串中。之后就能通过这个类来创建对象,代码演示中新建的类叫Someone
    >>> from collections import namedtuple
    >>> Person = namedtuple('Person', 'name, age')  # 此时Person这个类就有name,age两个字符
    >>> Someone = Person('ss', 29)  # 新建一个name为ss,Someone
    >>> Someone = Person(name='ss', age=29)  # 参数可以用key-value来表示
    
  2. __doc__为新类的描述文档,由namedtuple新建
    >>> Person.__doc__
    Person(name, age)
    
  3. 因为是tuple的子类,可以直接通过索引来访问属性,也可以通过字段名称来访问。同时能够像tuple那样解包赋值
    >>> Someone[0]      # 通过下标访问
    ss
    >>> Someone.name        # 通过字段名来访问
    ss
    >>> name, age = Someone
    ss 29
    
  4. 可以转换为dict,得到的是OrderedDict。同样通过一个dict来构造Someone这个我们定义的类
    >>> d = Someone._asdict()
    >>> d = {
        'name': 'ss',
        'age': 33
    }
    >>> anothor = Someone(**d)  # 将d解包后传入构造函数
    
  5. 使用类方法_make来生成一个Someone对象
    >>> ss = Someone._make(['myname', 28])
    >>> ss
    Person(name='myname', age=28)
    
  6. 使用_replace来通过替换特定的字段和值,返回一个新的对象
    >>> older = ss._replace(age=50)   # 上一个函数_make创建的ss
    >>> older
    Person(name='myname', age=50)
    

namedtuple实现

namedtuple函数使用的是元编程技术,通过传入namedtuple(cls_name, field_names)参数来定制类。

  1. 首先看namedtuple中对参数field_names的处理。如果参数是字符串,则将其分割成数组,比如'name, age'变为['name', 'age']。同时参数可以是可迭代对象,也将其转为字符串数组

    if isinstance(field_names, str):
        field_names = field_names.replace(',', ' ').split()
    field_names = list(map(str, field_names))
    typename = str(typename)
    
  2. 检查传入field_names是否符合要求,不能是Python关键词,也不能是运算符。

        for name in [typename] + field_names:
        if type(name) != str:       # 必须是字符串
            raise TypeError('Type names and field names must be strings')
        if not name.isidentifier():     # 不能是运算符
            raise ValueError('Type names and field names must be valid '
                             'identifiers: %r' % name)
        if _iskeyword(name):        # 不能是Python关键词
            raise ValueError('Type names and field names cannot be a '
                             'keyword: %r' % name)
    seen = set()
    for name in field_names:
        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)
    
  3. namedtuple定义了一个模板_class_template,里面包括前面提到的各种方法,下面是模板的代码:

    from builtins import property as _property, tuple as _tuple
    from operator import itemgetter as _itemgetter
    from collections import OrderedDict
    
    class {typename}(tuple):
        '{typename}({arg_list})'
    
        __slots__ = ()
    
        _fields = {field_names!r}
    
        def __new__(_cls, {arg_list}):
            'Create new instance of {typename}({arg_list})'
            return _tuple.__new__(_cls, ({arg_list}))
    
        @classmethod
        def _make(cls, iterable, new=tuple.__new__, len=len):
            'Make a new {typename} object from a sequence or iterable'
            result = new(cls, iterable)
            if len(result) != {num_fields:d}:
                raise TypeError('Expected {num_fields:d} arguments, got %d' % len(result))
            return result
    
        def _replace(_self, **kwds):
            'Return a new {typename} object replacing specified fields with new values'
            result = _self._make(map(kwds.pop, {field_names!r}, _self))
            if kwds:
                raise ValueError('Got unexpected field names: %r' % list(kwds))
            return result
    
        def __repr__(self):
            'Return a nicely formatted representation string'
            return self.__class__.__name__ + '({repr_fmt})' % self
    
        def _asdict(self):
            'Return a new OrderedDict which maps field names to their values.'
            return OrderedDict(zip(self._fields, self))
    
        def __getnewargs__(self):
            'Return self as a plain tuple.  Used by copy and pickle.'
            return tuple(self)
    {field_defs}
    

    举一个例子:比如Person = namedtuple('Person', 'name, age')这个代码创建了Person类,其中包括name、age字段,则上面的模板对应的字段如下

    • typename: 'Person'
    • arg_list: name, age
    • field_names: 其中!r表示调用repr()。相当于repr(("name", "age"))得到('name', 'age')
    • num_fields: len(field_names) = len(['name', 'age']) 为2
    • repr_fmt: (name, age)
    • field_defs: 后面介绍 可以看到namedtuple继承于tuple,这样就能像tuple那样作为可迭代对象。 在__new__函数中调用tuple.__new__返回tuple实例,在__new__函数中第一个参数是类本身,余下的参数与__init__方法一样,返回的实例作为__init__方法的第一个参数self
  4. 那像ss.name这样的取值操作是怎么实现的呢,还是使用模板,不过是另一个模板_field_template,定义了property,其中_property从from builtins import property as _property导入

    {name} = _property(_itemgetter({index:d}), doc='Alias for field number {index:d}')
    

    上面这个在模板被填充后变为:

    name = _property(_itemgetter(0), doc='Alias for field number 0')
    age = _property(_itemgetter(1), doc='Alias for field number 1')
    

    property类原型为property(fget=None, fset=None, fdel=None, doc=None),通过itemgetter定义了fget,其他的方法都没定义,这样就只能是一个只读字段。itemgetter(0)返回一个获取第1个元素的选择器。比如下面:

    >>> from operator import itemgetter
    >>> get = itemgetter(0)
    >>> get([1,2,3])
    1
    

    因此当我们调用ss.name时,实际是运行为ss[0]

  5. 需要将填充好的模板运行获得真实的类

    exec(class_definition, namespace)
    

    之后将其从namespace中得到

    result = namespace[typename]
    result._source = class_definition
    

    最终得到result就是我们定制得到的类