Doge log

Abby CTO 雑賀 力王のオフィシャルサイトです

namedtupleの話

python2.6から導入されたnamedtupleについてちょっくら書いておくか。

namedtupleって?

namedtupleは名前の通り名前付きでアクセスできるtupleを返す。

>>> from collections import namedtuple
>>> p = namedtuple('Point', 'x y')
>>> p1 = p(11, 22)
>>> p1[0]
11
>>> p1[1]
22
>>> p1.x
11
>>> p1.y
22
>>> p2 = p(x = 33, y = 44)
>>> p2[0]
33
>>> p2[1]
44
>>> p2.x
33
>>> p2.y
44
>>>

とまあこんな感じ。
namedtuple関数に名前とフィールドを渡すと名前付きでアクセスできるものを
作成するものを返す。

仕組み

実際にはnametuple関数はクラスを返している。

>>> namedtuple('Point', 'x y')

このクラスはtupleのサブクラスである。
詳細はverboseオプションで確認できる

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

__slots__ = ()

_fields = ('x', 'y')

def __new__(cls, x, y):
return tuple.__new__(cls, (x, y))

@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) != 2:
raise TypeError('Expected 2 arguments, got %d' % len(result))
return result

def __repr__(self):
return 'Point(x=%r, y=%r)' % self

def _asdict(t):
'Return a new dict which maps field names to their values'
return {'x': t[0], 'y': t[1]}

def _replace(self, **kwds):
'Return a new Point object replacing specified fields with new values'
result = self._make(map(kwds.pop, ('x', 'y'), self))
if kwds:
raise ValueError('Got unexpected field names: %r' % kwds.keys())
return result

def __getnewargs__(self):
return tuple(self)

x = property(itemgetter(0))
y = property(itemgetter(1))


>>>

とまあどんなクラスを生成したか見る事ができる。

で気になったのはこいつの作り方。正直この手のコードが標準ライブラリに
入るとはあんまり思ってなかったんだが。

    numfields = len(field_names)
    argtxt = repr(field_names).replace("'", "")[1:-1]   # tuple repr without parens or quotes
    reprtxt = ', '.join('%s=%%r' % name for name in field_names)
    dicttxt = ', '.join('%r: t[%d]' % (name, pos) for pos, name in enumerate(field_names))
    template = '''class %(typename)s(tuple):
        '%(typename)s(%(argtxt)s)' \n
        __slots__ = () \n
        _fields = %(field_names)r \n
        def __new__(cls, %(argtxt)s):
            return tuple.__new__(cls, (%(argtxt)s)) \n
        @classmethod
        def _make(cls, iterable, new=tuple.__new__, len=len):
            'Make a new %(typename)s object from a sequence or iterable'
            result = new(cls, iterable)
            if len(result) != %(numfields)d:
                raise TypeError('Expected %(numfields)d arguments, got %%d' %% len(result))
            return result \n
        def __repr__(self):
            return '%(typename)s(%(reprtxt)s)' %% self \n
        def _asdict(t):
            'Return a new dict which maps field names to their values'
            return {%(dicttxt)s} \n
        def _replace(self, **kwds):
            'Return a new %(typename)s 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' %% kwds.keys())
            return result \n
        def __getnewargs__(self):
            return tuple(self) \n\n''' % locals()
    for i, name in enumerate(field_names):
        template += '        %s = property(itemgetter(%d))\n' % (name, i)
    if verbose:
        print template

    # Execute the template string in a temporary namespace and
    # support tracing utilities by setting a value for frame.f_globals['__name__']
    namespace = dict(itemgetter=_itemgetter, __name__='namedtuple_%s' % typename)
    try:
        exec template in namespace
    except SyntaxError, e:
        raise SyntaxError(e.message + ':\n' + template)
    result = namespace[typename]
    # For pickling to work, the __module__ variable needs to be set to the frame
    # where the named tuple is created.  Bypass this step in enviroments where
    # sys._getframe is not defined (Jython for example).
    if hasattr(_sys, '_getframe'):
        result.__module__ = _sys._getframe(1).f_globals['__name__']

    return result

とまあ定番のexecとかsys._getframeとかやってますな。
これはなんだか黒魔術的な気がするんだけどどうなんだろう?
明示的な文化を持つPythonでは受け入れられない気がしてたのだが、最近では
緩くなったのかも知れないという話でした。
(namedtupleの実装は自動生成しまくりの真っ黒いメタクソプログラミングしたい人は参考にするといいかも知れない。)

まあでも便利は便利よね。
例えばフィールドの定義がだるいのでこんな感じでクラス作るとか

class Point(namedtuple('Point', 'x y')):
...

DBからのデータをクラスにMappingするのとかにも重宝する。

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