Last Updated: 2/6/2024, 5:44:57 AM

# namedtuple ってなに?

# 1. 使い道

namedtuple を使うとイミュータブルなクラスを定義できます。

ただし、イミュータブルなクラスを定義する方法は、いくつか他にもあります。 また、個人的にイミュータブルなクラスを実装する意義は、低いかなと個人的に感じてします。 実装方法とその理由について、以下の記事の下の方に記述させて頂きました。

ここでは collections.namedtuple (opens new window) そのものについて、見て行きます。

# 2. 使い方

namedtuple 関数にクラス名と属性名のタプルを実引数に与えれば、 簡単に immutable なクラスを定義できます。

import collections
Point = collections.namedtuple('Point', ['x', 'y'])
point = Point(0, 1)
point
>>> point
Point(x=0, y=1)
>>> 

# 3. 動作確認

本当に immutable か、簡単に確認してみました。

import collections
import inspect

def msg(err):
    return err.__class__.__name__ + ': ' + str(err)


#
# immutable なクラスの定義
#
Point = collections.namedtuple('Point', ['x', 'y'])

#
# インスタンスオブジェクトの動作確認
#
point = Point(11, y=22)
print(point)

# 1) 属性の参照ができる
assert point.x  == 11

# 2) 属性の変更はできない
try:
    point.x = 33
except AttributeError as err:
    print(msg(err))

# 3) 属性の追加はできない
try:
    point.z = 44
except AttributeError as err:
    print(msg(err))


#
# クラスオブジェクトの動作確認
#

# 4) Point はクラスオブジェクト
assert inspect.isclass(Point)

# 5) tuple を継承したクラスです。
assert Point.__bases__ == (tuple, )
assert point.x is point[0]
assert point.y is point[1] 

# 6) メソッドを追加する。
Point.add = lambda self: self.x + self.y
assert point.add() == 33

# 4. 継承

直接継承させることもできます。

from collections import namedtuple

BaseImmutableRegion = namedtuple(
    'BaseImmutableRegion', 
    ('x1', 'y1', 'x2', 'y2',
     'is_rectangle', 'is_line', 'is_dot')
)

class ImmutableRegion(BaseImmutableRegion):
    def __new__(cls, x1, y1, x2, y2):
        # point 1. __init__ ではなく、__new__ を使う。
        width_0 = (x1 - x2 == 0)
        height_0 = (y1 - y2 == 0)
        is_rectangle = (not width_0 and not height_0)  # 0 0
        is_line = (width_0 != height_0)  # 0 1 or 1 0; xor
        is_dot = (width_0 and height_0)  # 1 1
        args = (x1, y1, x2, y2, is_rectangle, is_line, is_dot)
        
        # point 2. 必要なオブジェクトが揃ったところで
        #          オブジェクトを生成する。
        self = super().__new__(cls, *args)
        
        # point 3. 生成したオブジェクトを return
        return self

ImmutableRegion(0, 0, 1, 1)

# ◯ 注意したいこと

例えば namedtuple 関数を使って immutable なオブジェクトを生成した時、 __init__ メソッドでは、値を初期化するための処理が実行できません。

class Region(namedtuple('Region', 
      ('x1', 'y1', 'x2', 'y2',
       'is_rectangle', 'is_line', 'is_dot'))):
    def __init__(self, x1, y1, x2, y2):
        width_0 = (x1 - x2 == 0)
        height_0 = (y1 - y2 == 0)
        self.is_rectangle = (not width_0 and not height_0)  # 0 0
        self.is_line = (width_0 != height_0)  # 0 1 or 1 0; xor
        self.is_dot = (width_0 and height_0)  # 1 1

# TypeError
Region(0, 0, 10, 10)
>>> Region(0, 0, 10, 10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __new__() missing 3 required positional arguments: 'is_rectangle', 'is_line', and 'is_dot'
>>>

エラー文を読んでみると __new__ が呼ばれていることがわかります。 なぜ __new__ が使われているのでしょうか? それは self に代入されたオブジェクトは immutable なので、 属性に代入することができないからです。

# 5. 実装方法

namedtuple 動作原理の概要については、1つ前の以下の記事で見てきました。

namedtuple は point.x が参照されると point[0] を返すように property (opens new window) 関数を使って実装されています。 パッと見は凄そうに見えますが仕組みそのものは、とても単純です。

文字列で __new__ メソッドを定義して

def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):

    ...

    # vvvvv 文字列で __new__ が定義されています。
    s = f'def __new__(_cls, {arg_list}): return _tuple_new(_cls, ({arg_list}))'
    namespace = {'_tuple_new': tuple_new, '__name__': f'namedtuple_{typename}'}
    # Note: exec() has the side-effect of interning the field names
    exec(s, namespace)
    __new__ = namespace['__new__']

あとは属性参照できるように property を設定していきます。

def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):

    ...

    # Build-up the class namespace dictionary
    # and use type() to build the result class
    class_namespace = {
        ...
        '__new__': __new__,  # <--- 上で定義した __new__ がここに。
        ...
    }
    cache = _nt_itemgetters
    for index, name in enumerate(field_names):
        try:
            itemgetter_object, doc = cache[index]
        except KeyError:
            itemgetter_object = _itemgetter(index)
            doc = f'Alias for field number {index}'
            cache[index] = itemgetter_object, doc
        #      ここで tuple を参照できるように
        # vvvv property を設定しています。
        class_namespace[name] = property(itemgetter_object, doc=doc)

    result = type(typename, (tuple,), class_namespace)

気にするほどではないのですが、 このように property で属性参照を実装しているので namedtuple を使うと属性参照がすこし遅くなります。 時間は以下の記事で計測しました。

# おわりに

個人的な感想としては namedtuple は、ちょっと気持ち悪いなと言う感じがします。 以下の記事に、とても共感してしまいました笑

次は、クラスデコレータを使ってもうちょっとオシャレに namedtuple を使う方法を見て行きたいと思います。

@immutable(
    'x1', 'y1', 'x2', 'y2',
    'is_rectangle', 'is_line', 'is_dot')
class ImmutableRegion:
    ...