metaclass で immutable

随分と久しぶりな投稿です。

Python 2.x の自作クラスを immutable にする件。

お手軽、かつ多くの記事を見かけるのが namedtuple を使う方法。

import collections
Vector = collections.namedtuple('Vector', ('x', 'y'))

Vector(0,0)


値を保持するだけのオブジェクトならばこれで十分。

immutable にするクラスの役割はデータ保持が主だから、これで足りる場合が殆どなのかと。

ただ、メソッドを追加したいとなったら?

そこで見つけたのは、tuple を継承する方法。

import operator
class Vector(tuple):
    x = property(operator.itemgetter(0))
    y = property(operator.itemgetter(1))
    __slots__ = []
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))

Vector(0,0)


メソッドを追加することができるようになりました。

同様な方法として type を使う方法。

import operator
Vector = type(
    'Vector',
    (tuple,),
    {
         'x': property(operator.itemgetter(0)),
         'y': property(operator.itemgetter(1)),
         '__slots__': [],
         '__new__': lambda cls, *args: tuple.__new__(cls, args),
    })

Vector(0,0)


tuple を継承する方法とやってることは同じだと思っています。

さらに metaclass を使った方法。

metaclass を学習がてら作ってみた、という程度のものなのですが、、、

import operator

class ImmutableMeta(type):
    """immutable クラスを構築するためのメタクラス
    
    >>> class Vector(object):
    ...
    ...     __metaclass__ = ImmutableMeta
    ...
    ...     properties = 'x y'
    ...     
    ...     @classmethod
    ...     def new(cls, x=0, y=0):
    ...         return x, y
    
    >>> v = Vector(1,-1)
    >>> v
    (1, -1)
    >>> v.x
    1
    >>> v.y
    -1
    >>> Vector()
    (0, 0)
    >>> Vector(y=2)
    (0, 2)
    """
    
    def __new__(cls, cls_name, bases, attrs):
        attrs['__slots__'] = []
        if 'properties' in attrs:
            properties = attrs['properties'].split()
            # property を追加
            for i, name in enumerate(properties):
                attrs[name] = property(operator.itemgetter(i))
            # __new__ を置き換え
            attrs['__new__'] = cls.new_method(attrs, properties)
        # tuple を継承していなければ、基底クラスに tuple を追加
        if not list(c for c in bases if issubclass(c, tuple)):
            bases = (tuple, ) + bases
        return type.__new__(cls, cls_name, tuple(bases), attrs)

    @staticmethod
    def new_method(attrs, properties):
        def gen(new_func):
            def __new__(cls, *args, **kwargs):
                args = new_func(cls, *args, **kwargs)
                if len(args) != len(properties):
                    raise TypeError(
                        '__new__() takes {0} arguments ({1} given)'.format(
                            len(properties), len(args)))
                return tuple.__new__(cls, args)
            return __new__
        if 'new' in attrs:
            return gen(lambda cls, *args, **kwargs: cls.new(*args, **kwargs))
        else:
            return gen(lambda cls, *args, **kwargs: tuple(args))


metaclass を使った Vector は随分とスッキリしました。

色々な書き方ができますね。

参考: