Python で immutable な値クラスを作るデコレータ
Python では tuple が immutable なオブジェクトなので、それを使った namedtuple
などの関数で簡単に値クラスを作ることができます。
更に、namedtupleの作った値クラスを継承したクラスでメソッドを追加することもできます。
しかし、この2ステップは冗長で、namedtupleの値クラスもフィールドが増えると面倒なので、デコレータやBuilderパターンなどで対応したいと思いました。
そこで、このような @value
デコレータを作りました。便利です。
from collections import namedtuple
def value(*fields):
def wrapper(class_):
name = class_.__name__
t = namedtuple(name, fields)
value_class = type(name, (class_, t), {})
class Builder(object):
def __init__(self, **kwargs):
self.kwargs = kwargs
for f in fields:
setattr(self, f, self.setter(f))
def ready(self):
return len(self.kwargs) == len(fields)
def build(self):
assert self.ready()
return value_class(**self.kwargs)
def setter(self, key):
def set_value(value):
self.kwargs[key] = value
return self
return set_value
def builder(*args, **kwargs):
if all(k in fields for k in kwargs.keys()):
valid = len(args) == 0 or len(kwargs) == 0 or \
len(args) <= min(fields.index(k) for k in kwargs.keys())
if valid:
if len(args) + len(kwargs) == len(fields):
return value_class(*args, **kwargs)
else:
for k, v in zip(fields, args):
kwargs[k] = v
return Builder(**kwargs)
raise RuntimeError("unsupported args {} and kwargs {}".format(args, kwargs))
return builder
return wrapper
# test
@value("x", "y")
class Point(object):
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.x == other.x and self.y == other.y
else:
return False
assert Point(1, 2) == Point().x(1).y(2).build()
assert Point(y=2, x=1) == Point(y=2).x(1).build()
assert Point(1, y=2) == Point(1).y(2).build()
ところで、動的に継承クラスを作るために、type
組み込み関数を使ったのですが、こいつは曲者です。
オブジェクトが渡されたときは生成元のクラスを返す一方で、
クラス名の文字列と継承元クラスが渡されたときは、継承させたクラスを返すのです。
なぜ、このような全く違う機能を一つの組み込み関数に持たせたのか意味がわかりません。
There should be one-- and preferably only one --obvious way to do it. -- The zen of python