# 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:
...