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