Pythonのsetterとgetterで使う関数名が同じなのはなぜか? どうやって使い分けているのか?

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メソッドが実行されます。fsetfgetpropertyインスタンス生成時に設定します。もしくは追加の属性として後から設定します。

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.setterpropertyインスタンスにセッターを設定しています。これで同じ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インスタンスになり、=演算子の有無によって同じ名前のメソッドが設定と取得に使い分けられるようになります。