Pythonの@property
デコレータと@<name>.setter
デコレータを使うと同じ名前で属性(プロパティ)に対して取得と設定ができますよね。多くの方はなぜ「同じ名前で2つの挙動に変化するのか?」と疑問に思わず通り過ぎますが、探究心旺盛なあなたのために
- 「なぜsetterとgetterが同じ名前なのか?」
- 「内部ではどのように使い分けられるのか?」
を解説します。
=演算子が使われたときはgetterがそうでなければsetterが実行される
結論から端的に述べると、=
演算子が使われたときはgetterが、そうでなければsetterが実行されます。
例えば以下のコードでは同じage
という名前でHuman
クラスの_age
プロパティに値を設定・取得できます。=
演算子を使ったときは42という値を設定、print(ikuma.age)
のときは値を取得できます。
class Human: def __init__(self, name): self.name = name self._age = 0 @property def age(self): return self._age @age.setter def age(self, age): self._age = int(age) ikuma = Human("ikuma") ikuma.age = 42 print(ikuma.age) # 42
なぜこのように同じ名前で異なる挙動になるのでしょうか?
同じ名前で設定と取得できた方が読みやすく保守しやすい
そもそもの設計思想的には「設定と取得が同じ名前のほうがいいよね」、というものがあるのだと思います。その方がコーディングしやすく、コードも読みやすく、保守しやすいからです。
そのための実装が@<name>setter
デコレータを使ったsetterと@property
デコレータを使ったgetterです。公式ドキュメントに記載があります。
class property — Python 3.12.4 ドキュメント
興味深いことに、このドキュメントには@property
デコレータを使わずにgetter、setterと同じ機能を実装する方法が以下のように書いてあります。(一部省略)
class C: def __init__(self): self._x = None def getx(self): return self._x def setx(self, value): self._x = value x = property(getx, setx, "I'm the 'x' property.")
つまりproperty
関数にgetterたるgetx
メソッド、setterたるsetx
メソッドを渡すことでproperty
関数の戻り値が変数x
に格納され、x
を介して設定と取得を制御できます。
propertyクラスの実装を確認する方法
上でproperty
関数と述べましたが、実際はクラスです。PyCharmproperty
クラスで右クリックして「型宣言」を選択すると、builtins.pyiのclass property
に移動してそれが確認できます。
VSCodeではproperty
クラスで右クリックして「定義へ移動」するとbuiltins.pyiが表示され確認できます。
設定・取得の使い分けは__set__と__get__で行う
取得と設定をどのように使い分けるか? についてですが、このclass property
の中に__set__
メソッドと__get__
メソッドがあり、設定と取得に対応しています。
=
演算子が使われたら__set__
が呼ばれfset
メソッドを実行、そうでなければ__get__
が呼ばれfget
メソッドが実行されます。fset
とfget
はproperty
インスタンス生成時に設定します。もしくは追加の属性として後から設定します。
x = property(getx, setx, "I'm the 'x' property.") # 以下のように分けてもよい # x = property(getx, "I'm the 'x' property.") # x.setter(setx)
=
演算子を使ったときの挙動はクラスの__eq__
メソッド呼出しのようなものですね。(__eq__
メソッドは==
演算子で比較されたときに呼び出されるメソッドです)
class obj: def __init__(self, x): self.x = x def __eq__(self, y): print("==で比較されました") return self.x == y o = obj(1) print(o == 1) # True print(o == 2) # False
ここまでを冒頭のHuman
クラスに当てはめると以下のようになります。
class Human: def __init__(self, name): self.name = name self._age = 0 def _get_age(self): return self._age def _set_age(self, age): self._age = int(age) age = property(_get_age) age = age.setter(_set_age) # 以下のコードと等価 # class Human: # def __init__(self, name): # self.name = name # self._age = 0 # # @property # def age(self): # return self._age # # @age.setter # def age(self, age): # self._age = int(age)
age = property(get_age)
でゲッターを設定し、次にage.setter(set_age)
でセッターを設定しています。これでage
という名前でセッター、ゲッターを使い分けられるようになります。
@propertyデコレータでセッターとゲッターをPythonicに書く
これをよりスマートに、Pythonicにしたのが@property
デコレータを使った記法です。
class Human: def __init__(self, name, height, weight): self.name = name self._age = 0 @property def age(self): return self._age @age.setter def age(self, age): self._age = int(age)
デコレータの文法と挙動は以下のようになりますよね。つまりデコレータ関数
の引数func
に関数本体
が渡されます。
def デコレータ関数(func): def ラッパー関数(*args, **kwargs): # 前処理 result = func(*args, **kwargs) # 後処理 return result # 渡された関数の結果を返却 return ラッパー関数 @デコレータ関数 def 関数本体(): # 何らかの処理 result = 関数本体()
冒頭のHuman
クラスではage
メソッドに@property
が付いていますので、property
関数(実際はクラス)にはage
が渡されます。@age.setter
はproperty
インスタンスにセッターを設定しています。これで同じage
という名前で使い分けられるようになります。
class Human: def __init__(self, name): self.name = name self._age = 0 @property def age(self): return self._age @age.setter def age(self, age): self._age = int(age) ikuma = Human("ikuma") ikuma.age = 42 print(ikuma.age) # 42
つまり@property
デコレータを使うことで任意のメソッドがproperty
インスタンスになり、同じ名前のメソッドが=
演算子の有無によって__set__
と__get__
が使い分けられる仕組みです。
propertyクラスのCpythonの実装
ちなみにPyCharmで表示できるbuiltins.pyiでは以下のように__get__
や__set__
の実装がよく分かりませんよね。
Pythonの実装であるCPythonのdescrobject.cを見るともう少し詳しく書いてあります。
ただ、このあたり私も詳しくはよく分かりませんので雰囲気だけでもつかんでいただければと。
まとめ
この記事では
- なぜ
@property
デコレータを使うと同じ名前でプロパティに対して取得と設定ができるのか? - なぜsetterとgetterが同じ名前なのか?
- 内部ではどのように使い分けられるのか?
という疑問を解説しました。結論は@property
デコレータを使うことで任意のメソッドがproperty
インスタンスになり、=
演算子の有無によって同じ名前のメソッドが設定と取得に使い分けられるようになります。