Python namedtuple使用

愛吃叉燒發表於2018-07-16

文章的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就是我們定製得到的類

相關文章