Pythonのpropertyデコレータの使い方やメリットについて解説する

スポンサーリンク

今回は、Pythonのプロパティ・デコレーターについて見ていきましょう。

Pythonにはデコレーターという非常に便利な機能があり、これは関数ラッパーの構文上の糖分に過ぎません。

ここでは、デコレータの特殊なタイプであるプロパティ・デコレータに焦点を当てます。

この話題は少しわかりにくいかもしれませんので、例を挙げながら順を追って説明します。

それでは、始めましょう。

スポンサーリンク

Python Property Decorator は何に基づいてるんですか?

Property デコレータは、組み込みの property() 関数に基づいています。

この関数は、特別な property オブジェクトを返します。

Python インタープリターでこれを呼び出して、見てみましょう。

>>> property()
<property object at 0x000002BBD7302BD8>

この property オブジェクトは、オブジェクトの値を取得したり設定したりするための、いくつかの特別なメソッドを持っています。

また、このオブジェクトを削除するためのメソッドも持っています。

メソッドの一覧は以下の通りです。

  • property().getter (プロパティゲッター
  • property().setter (プロパティセッター)
  • property().deleter (プロパティ().セッター)

しかし、これだけでは終わりません! これらのメソッドは他のオブジェクトでも使用することができ、それ自体がデコレータとして機能します!

例えば、 property().getter(obj) を使用すると、別のプロパティオブジェクトを取得することができます!

つまり、property デコレータはこの関数を使用し、オブジェクトへの読み書きのための特別なメソッドを持っているということです。

では、見てみましょう。

Python プロパティデコレータを使用する

プロパティデコレータを使うには、関数やメソッドにラップする必要があります。

以下は簡単な例です。

@property
def fun(a, b):
    return a + b

と同じです。

def fun(a, b):
    return a + b
 
fun = property(fun)

つまり、ここでは property()fun() のまわりにくるんでいるわけです。

それでは、クラスメソッドにプロパティ・デコレータを適用した簡単な例を見てみましょう。

次のようなクラスで、デコレータを使用していないメソッドを考えてみましょう。

class Student():
    def __init__(self, name, id):
        self.name = name
        self.id = id
        self.full_id = self.name + " - " + str(self.id)
 
    def get_name(self):
        return self.name
 
s = Student("Amit", 10)
print(s.name)
print(s.full_id)
 
# Change only the name
s.name = "Rahul"
print(s.name)
print(s.full_id)

結果は以下の通りです。

Amit
Amit - 10
Rahul
Amit - 10

ここで、オブジェクトの name 属性を変更しただけでは、 full_id 属性への参照はまだ更新されていません!

nameidが更新されたときにfull_id属性も更新されるようにするには、full_id` をメソッドにすることで解決できます。

class Student():
    def __init__(self, name, id):
        self.name = name
        self.id = id
 
    def get_name(self):
        return self.name
 
    # Convert full_id into a method
    def full_id(self):
        return self.name + " - " + str(self.id)
 
s = Student("Amit", 10)
print(s.name)
# Method call
print(s.full_id())
 
s.name = "Rahul"
print(s.name)
# Method call
print(s.full_id())

結果は以下の通りです。

Amit
Amit - 10
Rahul
Rahul - 10

ここでは、 full_id をメソッド full_id() に変換することで、問題を解決しました。

しかし、これはこの問題に取り組むベストな方法ではありません。

なぜなら、このような属性をすべてメソッドに変換し、属性をメソッド呼び出しに変更する必要がある場合があるからです。

この問題を解決するために、代わりに @property デコレータを使用することができます

このアイデアは、 full_id() をメソッド化し、それを @property で囲むことです。

この方法では、関数の呼び出しとして扱うことなく、 full_id を更新することができます

これを直接行うには、 s.full_id とします。

ここで、メソッドの呼び出しがないことに注目してください。

これは、プロパティデコレータを使用しているためです。

では、実際に試してみましょう。

class Student():
    def __init__(self, name, id):
        self.name = name
        self.id = id
 
    def get_name(self):
        return self.name
 
    @property
    def full_id(self):
        return self.name + " - " + str(self.id)
 
s = Student("Amit", 10)
print(s.name)
# No more method calls!
print(s.full_id)
 
s.name = "Rahul"
print(s.name)
# No more method calls!
print(s.full_id)

結果は以下の通りです。

Amit
Amit - 10
Rahul
Rahul - 10

確かに、これでうまくいきました! これで、括弧を使って full_id を呼び出す必要がなくなりました。

これはまだメソッドですが、プロパティデコレーターはそれを隠して、クラスのプロパティであるかのように扱います! これで名前が意味をなすようになったでしょう!

プロパティをセッターで使用する

上記の例では、full_id プロパティを明示的に直接変更しなかったので、このアプローチはうまくいきました。

デフォルトでは、 @property を使用すると、そのプロパティは読み取り専用になるだけです。

つまり、そのプロパティを明示的に変更することはできません。

s.full_id = "Kishore"
print(s.full_id)

結果は以下の通りです。

---> 21 s.full_id = "Kishore"
     22 print(s.full_id)
 
AttributeError: can't set attribute

明らかに、このプロパティは読み取り専用なので、私たちは権限を持っていません!

このプロパティを書き込み可能にするには、先ほど説明した property().setter メソッド (これはデコレーターでもあります) を思い出してください。

このメソッドはデコレーターでもあります。

そして、@full_id.setter を使用して、もうひとつの full_id プロパティを追加して書き込み可能にできることがわかりました。

この @full_id.setter は元の full_id プロパティをすべて継承しているので、直接追加することができます!しかし、 @full_id.setter を直接使用することはできません。

しかし、セッターのプロパティで full_id プロパティを直接使用することはできません。

これは無限の再帰的降下を引き起こすことになります。

class Student():
    def __init__(self, name, id):
        self.name = name
        self.id = id
 
    def get_name(self):
        return self.name
 
    @property
    def full_id(self):
        return self.name + " - " + str(self.id)
 
    @full_id.setter
    def full_id(self, value):
        # Infinite recursion depth!
        # Notice that you're calling the setter property of full_id() again and again
        self.full_id = value

これを避けるために、隠し属性 _full_id をクラスに追加します。

代わりにこの属性を返すように @property デコレーターを変更します。

更新されたコードは次のようになります。

class Student():
    def __init__(self, name, id):
        self.name = name
        self.id = id
        self._full_id = self.name + " - " + str(self.id)
 
    def get_name(self):
        return self.name
 
    @property
    def full_id(self):
        return self._full_id
 
    @full_id.setter
    def full_id(self, value):
        self._full_id = value
 
s = Student("Amit", 10)
print(s.name)
print(s.full_id)
 
s.name = "Rahul"
print(s.name)
print(s.full_id)
 
s.full_id = "Kishore - 20"
print(s.name)
print(s.id)
print(s.full_id)

結果は以下の通りです。

Amit
Amit - 10
Rahul
Amit - 10
Rahul
10
Kishore - 20

これで、full_id プロパティにゲッターとセッターの属性を持たせることに成功しました!

まとめ

これで、property デコレータについて、また、なぜ _full_id のようなクラス属性を使用する必要があるのかを理解していただけたかと思います。

これらの隠されたクラス属性 (_full_id など) を使用すると、外側の full_id プロパティを簡単に使用することができます!これは、現代のオープンソースでプロパティが多用されている理由です。

これこそが、最近のオープンソースプロジェクトでプロパティが多用される理由です。

エンドユーザーにとって非常に使いやすく、開発者にとっても隠し属性とそうでない属性を簡単に切り分けることができるのです!

参考文献

  • StackOverflow プロパティデコレータに関する質問
タイトルとURLをコピーしました