propertyについて

pythonのpropertyについて頭を整理しようと思ったのでまとめてみる。

agenda

  • propertyって何?
    • 困ったこと(propertyって読み込み専用の属性を定義するための機能なの?)
  • propertyの使い方
  • propertyの利用例
    • getterを定義する場合
    • setterを定義する場合
    • deleterを定義する場合
  • まとめ


と書く予定だったけれど、途中で力尽きてしまった。利用例をまったく書いていない。でも、propertyとは何かということについて分かれば、利用例なんてすぐに思いつけると思う。


propertyって何?

pythonで自分で定義したオブジェクトに、独自のアクセサを持った属性を追加できる機能のこと。
アクセサというのは、オブジェクトの状態に触るために用意された機能のこと。
具体的には、状態の取得、状態の設定(更新)、状態の削除の機能を持つものを指すことが多い。

慣例的にgetter,setter,deleter?*1とか言う。


pythonドキュメントでは、

fget is a function for getting an attribute value, likewise fset is a function for setting, and fdel a function for del’ing, an attribute. Typical use is to define a managed attribute x:

とかかれている。ドキュメント中ではgetter,setter,deleter?はそれぞれ、fget,fset,fdelという名前で呼ばれている。

困ったこと(propertyって読み込み専用の属性を定義するための機能なの?)

propertyの機能は誤解されやすいような形で紹介されていることが多い気がする。例えば以下の通り。

  • propertyを使えば読み込み専用の属性を手軽に定義できる
  • propertyで定義した属性へのアクセスには()が必要ない。

これらは確かに間違っていない。。。けれど、どこか腑に落ちないところがある。


ひとつめの例については、pythonのドキュメントを参考にした日記から始まっているのかもしれない。


上で紹介したpythonのドキュメントのPropertyの項をそのまま項を読み進めて行くと、「propertyをデコレータとして使うと、読み込み専用の属性が簡単に定義できる」と書かれている。もう少し丁寧に見ていく。

  1. pythonにはpropertyという機能がある。
  2. pythonのpropertyをデコレータとして使うこともできる
  3. pythonのpropertyをデコレータとして使うと、読み込み専用の属性を簡単に定義できる


と言っている。よく読んでみると、propertyの機能の利用する一例として書かれているように読める。
propertyは読み込み専用属性を定義することにも使えるのであって、読み込み専用属性を定義するための機能じゃない。



にもかかわらず、"python property"で検索してみると、以下のような内容のブログが多い。気がする。

  • propertyというものが存在する
  • 読み取り専用の属性を定義する方法に触れる
  • property定義した属性へのアクセスには、()がいらないことに触れる

これらは間違っていない。でも、これだけでは、読んでも「propertyをどういう時に使うのか」全然わかんないんじゃないかと思う。
(もちろん、分かっている人はわざわざpropertyについて日記やブログなどに書くことが無いのかもしれないけど。
本などを買えば、そこに詳しくpropertyの説明が載っているのかもしれないけれど。)

利用方法しか書かれていない。どういうときに使うと良いか書かれていない。

propertyって何?(propertyの機能について)


ここからは具体的に、pythnoのコードを交えてpropertyについて書いてみることにする。

propertyには2通りの書き方がある。

  1. propertyを関数として実際に実行するように書く方法
  2. propertyをデコレータとして利用する方法


前者は古く、後者の方が新しいらしい。とりあえず、古い方の書き方で書いてみる。

class UsePropertyDirectly(object):
    def __init__(self, x):
        self._x = x

    def _get_x(self):
        return self._x
    def _set_x(self, v):
        self._x = v
    def _delete_x(self):
        del self._x 
    x = property(_get_x, _set_x, _delete_x)

upd = UsePropertyDirectly(10)
print upd.x # getter
upd.x = 20 # setter
del upd.x # deleter?


記述の仕方については触れない。
getter,setter,deleterを定義したxという属性を持ったオブジェクトを定義しているだけだ。


これは、他と何が違うのか?既存の方法、propertyを使わない方法で書いてみる。

class NoProperty(object):
    def __init__(self, x):
        self._x = x

    def get_x(self):
        return self._x
    def set_x(self, v):
        self._x = v
    def delete_x(self):
        del self._x 
        
np = NoProperty(10)
print np.get_x()
np.set_x(20)
np.delete_x()

もちろん、実行の仕方が違うだけだ。オブジェクトの状態xについてのアクセス用のメソッドをpropertyに渡すことで、定義した状態に対して、変数へのアクセスをするpythonの既存の構文と同様の方法でアクセスすることができる。これはうれしい。。。それで本当に納得できる?



もう少し落ち着いて考えてみる。すると以下のようになる。


  1. しばしば状態にアクセサを定義したいときがある。(?)
  2. propertyを利用すると、定義したアクセサを簡便に使えるようになる。

propertyを使って、定義したアクセサを簡便に使えるようになった。ところで、アクセサってどういう時に定義すれば良いんだろうか?このことについての理解はあやふやなままのように思う。ただ、「propertyを使ってアクセサを定義する方法」については分かっている段階で納得してしまってたりしない?。


後で考えること


実際にpropertyを便利に使おうと思えば、「アクセサを使う方法(アクセサを簡便に使う方法)」が分かっていてもあまり意味がない。
アクセサが簡便に使えるようになったとして、何時アクセサを使うんだろう?それが分かっていなければ使えない。

そんなわけで、「アクセサを定義するとどういうことができるか?」ということについて考える必要があるように思う。
(このことについては後で触れることにする)


読み取り専用の属性の定義について(デコレータを使う方法)

propertyの書き方について古い方法は分かった。でも、それとは別に新しい方法も存在するらしい。それはpropertyをデコレータとして使う方法なようだ。
それについて考えてみることにする。

デコレータは単なるsyntax sugar

なので、以下のコードはほとんど同じもの

# これと
@deco
def f(x):
	return x

# これ
def g(x):
	return x
g = deco(g)

このことから、propertyをデコレータとして使った時のことを考えるには、propertyの戻り値について調査してみれば良さそう。
色々、いじってみる。


from pprint import pprint
_n = 10
n = property(lambda : [_n,_n])
pprint( n.__class__) # <type 'property'>
pprint( n.__class__.__dict__.keys()) 
#> ['fset', '__new__', 'setter', '__set__', '__getattribute__',
#  '__doc__', 'getter', 'deleter', '__get__', 'fget',
#  'fdel', '__init__', '__delete__']
print globals()["_n"]
print n.fget() #[10, 10]

def set_n(x, v):
	# xがselfなら、状態を保存できる。
	# self._n = vというように
    globals()["_n"] = ["pre", x, "after", v]

n = n.setter(set_n)
n.fset(_n, 100)

print n.fget() #[['pre', 10, 'after', 100], ['pre', 10, 'after', 100]]

n = n.getter(lambda : _n)
print n.fget() #['pre', 10, 'after', 100]


propertyのオブジェクトが持っているメソッド/メンバーを覗いてみると以下のような対応関係になっているらしい。


役割  slot wrapper 関数 アクセサを定義するデコレータ
getter __get__ fget getter
settter __set__ fset setter
deleter __delete__ fdel deleter


propertyを実行すると、property typeのオブジェクトがが返される。このオブジェクトは、アクセサを定義するデコレータを持っているということが分かる。
setterを例に取ると


  1. x = property(...) => xはproperty type => x はsetterを持つ
  2. x.setter(func) => x.fset()で setterに渡した関数funcが実行される。
  3. __set__ => obj.foo = val 形式の時呼び出される

なので、propertyをデコレータとして使うと、デコレータを付与した関数はproperty typeになる。
property typeはgetter,setter,deleterというデコレータを持つ。
これを他の関数にデコレートすることで、fget,fset,fdelが定義される。



それと一緒に__get__,__set__,__delete__にfget,fset,fdelを対応付けている。
これらは、pythonの構文にしたがったアクセス時に呼び出されるフック。
(最後の__set__については説明していないけれど => 追記に書いた。)


ということで、新しい方法でプロパティが定義できる。これをクラス定義に利用すると以下のように使えるということが分かる。

class UsePropertyAsDecorator(object):
    def __init__(self, x):
        self._x = x

    # x = property(func, func, func)のsyntax sugar
    @property
    def x(self):
        return None

    @x.setter
    def x(self, v):
        self._x = abs(v)

    @x.deleter
    def x(self):
        self._x = None
        
    @x.getter
    def x(self):
        return [self._x, self._x]

upad = UsePropertyAsDecorator(100)
print upad.x # [100, 100]
upad.x = -200
print upad.x # [200, 200]
del upad.x
print upad.x # [None, None]


最初のdef xはpropertyを呼び出して、property typeにするもの。
(もちろん、propertyの第一引数はgetterになるのでここでgetterを定義するのが普通)
その後、property typeが持つデコレータを利用して、setter,deleter,getterを定義している。


実際の利用例

力尽きた。用例は後で書く。
ただ、以下のことが分かっていれば、用例は簡単に思いつくんじゃないかと思う。

  • アクセサ = 「オブジェクトの状態を触るための機能」
  • 自分でアクセサを定義 = 「状態の取得、更新、削除の前後に何らかの処理をはさめる」
  • propertyの利用 = 上の機能を満たすために最大3つの語彙を節約できる。

参考

追記(__get__,__set__について)

実際、__get__,__set__を定義したオブジェクトは以下のように振る舞う。

class Item(object):
    def __init__(self, x):
        self.x = x

    def __get__(self, obj, kind):
        return [self.__class__.__name__,
                obj.__class__.__name__, 
                kind.__name__,
                self.x]

    def __set__(self, obj, v):
        self.x = v


class Contener(object):
    x = Item(10)
    y = 20

c =  Contener()
print c.x
c.x = 100
print c.x

# ['Item', 'Contener', 'Contener', 10]
# ['Item', 'NoneType', 'Contener', 100]

追記2

言いたいことを一言でまとめられてしまった><。
kiwanamiさん

@podhmo OOPでの「プロパティ」はオブジェクトの「ふるまい」ではないものだと考えるとしっくり来る気がします。そのクラスのユーザーにとって値(状態)のように見せたいものという感じです。実装方法が言語によってバラバラなので、実装上の単なるアクセサと混同されてることが多いですね

*1:正しいか自信無い