graphics.hatenablog.com

技術系テクニカルアーティストのあれこれ

Python のプロパティあれこれ。

PyMEL の utils.cachedProperty をみてふと思ったのでやってみたコネタ。

python/property.py at master · hal1932/python · GitHub

public フィールド

class Klass(object):
    def __init__(self):
        self.public = None

まずは基本。オブジェクト指向というものをどう捉えるかにもよるけど、「カプセル化」が効かないのであまり良い方法とは思われないことが多い。とはいえ Python では比較的ポピュラーな方法。

内部的には getattr/setattr が呼ばれていて、オーバーヘッドは一番少ない。ここがベースライン。

property をそのまま使う

class Klass(object):
    def __init__(self):
        self.__prop = 1
    
    def __get_prop(self):
        return self.__prop
    
    def __set_prop(self, value):
        self.__prop = value

    prop = property(__get_prop, __set_prop)

Python で「プロパティ」といったときにおそらく一番標準的な方法。public フィールドと比べると「getter のみ定義する」というのができるのが利点。加えて getter/setter の中で メモ化 なんかを含めたいろんなロジックが組めるので柔軟性も高い。ただ、getter/setter をいちいち別定義してやらないといけないのが面倒。

内部的にはプロパティにアクセスするたびに getter/setter メソッドが呼ばれるので、若干のオーバーヘッドはある。パフォーマンス的には、手許の実測値で「public フィールド」の 1/4 程度。

fget 内で hasattr してから setattr

def create_property(name, creator):
    def fget(obj):
        value = None
        if hasattr(obj, name):
            value = getattr(obj, name)
        
        if value is None:
            value = creator(obj)
            setattr(obj, name, value)
        
        return value
    
    def fset(obj, value):
        setattr(obj, name, value)
        
    return property(fget, fset)

class Klass(object):
    def init(self):
        self.prop = create_property('prop', lambda: 1)

「property そのまま」を動的にしたパターン。こちらは fget が呼ばれるまでフィールドが生成されないのでメモリに優しい。大量のインスタンスが生成される一方で、プロパティへのアクセス頻度が限定されるときに有効な方法。

パフォーマンス的には hasattr (辞書のキー検索)がどうしてもかかってしまうので、その分だけ遅くはなる。手許の実測値で「property そのまま」の 1/2、「public フィールド」の 1/8 程度。

property 作成時に setattr して getattr

def create_property(self, name, default_value):
    prop_name = '__{}'.format(name)

    def fget(obj):
        return getattr(obj, prop_name)
    
    def fset(obj, value):
        setattr(obj, prop_name, value)
        
    setattr(self, prop_name, default_value)
    return property(fget, fset)

class Klass(object):
    def init(self):
        self.prop = create_property(self, 'prop', lambda: 1, None)

hasattr の負荷を避けるために property 作成時に setattr してしまうパターン。ただまぁ、これだと「property そのまま」とほぼ変わらない。getter/setter を自分で定義する必要がないってのが利点か。

内部的には fget/fset がインライン化されるのか、パフォーマンス的にも「public フィールド」と変わらない。

property 作成時に setattr して getattr(値キャッシュ付き)

def create_property(self, name, creator, default_value):
    prop_name = '__{}'.format(name)

    def fget(obj):
        value = getattr(obj, prop_name)
        if value is None:
            value = creator(obj)
            setattr(obj, prop_name)
        return value
    
    def fset(obj, value):
        setattr(obj, prop_name, value)
        
    setattr(self, prop_name, default_value)
    return property(fget, fset)

class Klass(object):
    def init(self):
        self.prop = create_property(self, 'prop', lambda: 1, None)

インスタンスごとにフィールド変数が生成されるのを許容できる場合は、おそらくこれが使いやすい。fset を使わない場合、`self.__prop = None` でキャッシュをクリアできる。ただし __prop 自体が明示的に宣言されていないのが難点。create_property の引数に別途 invalid_value を渡してもいいんだけど、invalid_value == None の場合に if value == invalid_value で比較するわけにはいかないので、そこがちょっと悩みどころ。

パフォーマンス的には、creator の呼び出し頻度にもよるけど、それを除けば「public フィールド」と変わらない。