【Python】propertyとついでにclassを完全に理解する

# はじめに

`property`を使った書き方を丸暗記だけしてよくわからないままclassを使っている人が多いですね。元をたどればclassをなんとなく使っているからです。ということで、中で何が起きているのかを徹底的に解説します！

#### 2021/12/31 編集

- インスタンスからクラス変数にアクセスするとき、コピーではなく参照を受け取っています。その点間違っていたので修正しました。


# 目次

1. [そもそもclassとは](#そもそもclassとは)
2. [`__get__`メソッドについて](#__get__メソッドについて)
3. [`__set__`メソッドについて](#__set__メソッドについて)
4. [`property`とは](#propertyとは)
5. [よく見る`property`の使い方](#よく見るpropertyの使い方)

# そもそもclassとは

`property`を理解するには、まずclassが何なのかを知る必要があります。
ほとんどの解説記事では、classは次のような構成で紹介されます。

```python
class A:
    def __init__(self, value=0):
        """インスタンスを作成"""
        self.value = value

    def show(self):
        """値を表示"""
        print(self.value)
```

これにより`show`関数は謎の「メソッド」というものとして定義され、classの定義はメソッドの集合体として認識されがちです。しかしこれはいったん忘れてください。

#### 名前空間としてのclass

classの定義もあくまで１行（または１ブロック）ずつ実行しているにすぎません。次の例を見てください。

```python
class A:
    a = 1
    def print_value(x):
        print("value =", x)
    print_value(a)
```

classの中であること以外、地の文と変わりません。これを実行すると、

```
value = 1
```

とちゃんとprintされます。違いは定義した変数`a`や関数`print_value`にアクセスするときに`A`を介する必要があることです。

```python
A.a  # [Out]: 1
A.print_value  # [Out]: <function __main__.A.print_value(x)>
```

このおかげで、classの外で`a = 10`のように同じ名前の変数を新しく定義しても混同されません。「ただの`a`」と「クラス`A`の`a`」で区別できるので「名前空間を分ける」というふうに言われるわけです。

#### インスタンスの作成

上で定義した`A`は関数呼び出し可能です。

```python
callable(A)  # [Out]: True
```

`A`を関数呼び出ししたときの返り値は「`A`のインスタンス」と呼ばれます。`type`で型を確認すると`A`になっているので、型を定義したと解釈することもできます。

```python
ins = A()
ins  # [Out]: <__main__.A at 0x...>
type(ins)  # [Out]: __main__.A
```

`A`のインスタンスは、ピリオド`.`を介して`A`の中で定義した変数・関数すべてにアクセス可能です。しかし、このとき、アクセス先の変数によって挙動が異なります。もう一度`A`の定義に戻りましょう。

```python
class A:
    a = 1
    def print_value(x):
        print("value =", x)
```

インスタンスからアクセスできるものは`a`と`print_value`です。前者は

```python
ins.a  # [Out]: 1
```

となり、とても直観的ですが、後者は

```python
ins.print_value(0)  # TypeError: print_value() takes 1 positional argument but 2 were given
ins.print_value()  # [Out]: value = <__main__.A object at 0x0000021CC6533DC0>
```

からわかるように、引数が１つ減っている代わりに、１つめの引数`x`に`ins`自身が渡されていることが分かります。このことはclassの基本として紹介されていますが、考えてみればPythonにおいては整数も文字列も関数もすべて`object`型に属します。

```python
isinstance(1, object)  # [Out]: True
isinstance(A.print_value, object)  # [Out]: True
```

なぜこのような違いが出てくるのでしょうか。

# `__get__`メソッドについて

class`A`内の変数に、`A`とは全く別物である、`A`のインスタンス`ins`からアクセスすることは未定義です。これがPythonでどう定義されているのかがポイントです。

`A`のインスタンス`ins`はどのように`A`から変数をもらっているのか。結論から言うと

1. デフォルトでは変数の参照をもらう
2. 変数に`__get__`メソッドがある場合は`__get__`メソッドを呼び出す

の２パターンです。

#### 1. デフォルトでは変数の参照をもらう

`int`は`__get__`メソッドを持ちません。

```python
hasattr(0, "__get__")  # [Out]: False
```

したがって、`ins.a`と`A.a`は同じオブジェクトを表します。

```python
ins.a is A.a  # [Out]: True
```

ただし、`ins.a`に新しい値を代入しても、`ins`側で別のオブジェクトに更新されるだけで、`A.a`に影響はありません。

```python
ins = A()
ins.a = 10
ins.a  # [Out]: 10
A.a  # [Out]: 1
```

ただし、参照渡しであるので、たとえば`list`の中身は共有されます。

```python
hasattr([], "__get__")  # [Out]: False

class B:
    x = [0]

ins_b = B()
ins_b.x  # [Out]: [0]
ins_b.x[0] = -1
ins_b.x  # [Out]: [-1]
B.x  # [Out]: [-1]  <-- 上書きされる！
```

#### 2. 変数に`__get__`メソッドがある場合は`__get__`メソッドを呼び出す

まずは`__get__`が何なのかを知る必要があります。最も大事な、関数を例に説明します。

###### (1) 関数の`__get__`メソッド

Pythonでの関数は`function`型です。関数は、どこで定義されようと`function`のインスタンスであり、`__get__`メソッドを持ちます。

```python
def f(x): ...
type(f)  # [Out]: function
hasattr(f, "__get__")  # [Out]: True

type(A.print_value)  # [Out]: function
hasattr(A.print_value, "__get__")  # [Out]: True
```

Python内部での関数の`__get__`メソッドはPython実装ではないですが、真似すれば次のようになっています。

```python
class function:
    def __get__(self, obj, objtype=None):
        def method(*args, **kwargs):
            return self(obj, *args, **kwargs) # selfが関数自身であることに注意
        return method
```
（:warning: これはいろいろな意味で完全ではありません。あくまで説明のためです）

少しわかりづらいですが、自身の１つめの引数のみ`obj`にした新しい関数`method`を作成して返しています。

`function`クラスを無理やり`type`で取得してこれを実行してみましょう。足し算する関数と`function`クラスを用意します。

```python
def add(x, y):
    return x + y

function = type(add)
```

整数`1`の`add`メソッド`1.add`のようなものを用意します。

```python
add_method = function.__get__(add, 1)
```

`add_method`は`add`の１つめの引数に`1`が入った状態ですので、もう片方を指定すれば足し算が計算されます。

```python
add_method(2)  # [Out]: 3
```

###### (2) class内での`__get__`の挙動

以上を念頭に考えます。さきほどから使っているclass`A`とそのインスタンスを使います。

```python
class A:
    a = 1
    def print_value(x):
        print("value =", x)

ins = A()
```

関数`print_value`がclass`A`で定義され、それが`A`のインスタンス`ins`から`ins.print_value`とアクセスされたとき、次のような作業が行われます。

1. `method = function.__get__(A.print_value, ins)`によりメソッド関数`method`が作成される。
2. `ins.print_value`の返り値がそのまま`method`になる。

つまり、`print_value`をそのままコピーする代わりに、`print_value`には`__get__`が定義されているので、`__get__`の返り値（すなわち１つめの引数が指定された関数）をコピーの代わりにしているということです。

###### (3) `__get__`を使ってみる

よりシンプルに、`"Hello!"`を常にコピーとして渡してくる無能な型を作ってみましょう。

```python
class Munou:
    def __get__(self, obj, objtype=None):
        return "Hello!"
```

この無能オブジェクトをclass内で定義すれば、アクセスしたときに常に`"Hello!"`が得られます。

```python
class B:
    m = Munou()

ins = B()
ins.m  # [Out]: 'Hello!'
```

値へのアクセスが完全に制御できました。

# `__set__`メソッドについて

`__get__`は値を受け取るときに呼び出されました。値を渡す（代入する）ときはどうでしょうか。これも同じような構成になっています。

1. デフォルトではそのまま値を上書きする。
2. 上書きしようとしている変数に`__set__`メソッドがある場合は`__set__`メソッドを呼び出す

`int`には`__set__`メソッドがないので、普通に上書きされます。

```python
class B:
    i = 0

ins = B()
ins.i  # [Out]: 0
ins.i = 1
ins.i  # [Out]: 1 <-- 上書きされている
```

先ほどの`Munou`も`__set__`を定義していないので、新しい値を与えると上書きされます。

```python
class B:
    m = Munou()

ins = B()
ins.m = 0
ins.m  # [Out]: 0 <-- 上書きされている
```

そこで、上書きしないように、エラーを返すように書き換えます。

```python
class Munou:
    def __get__(self, obj, objtype=None):
        return "Hello!"
    def __set__(self, obj, value):
        raise AttributeError
```

この無能オブジェクトを使うと、上書き`ins.m = 0`の際に`Munou.__set__(B.m, ins, 0)`が呼び出されます。この例では、無能なうえに上書きが阻止されます。

```python
class B:
    m = Munou()

ins = B()
ins.m = 0  # [Out]: AttributeError
ins.m  # [Out]: 'Hello!'
```

# `property`とは

以上のように`__get__`や`__set__`は値の取得・上書きを制御するのに使えることが分かりました。これをうまく使えば、たとえば

- 格納された値を使いやすい形で返してくれるように`__get__`を定義する。
- 値の更新の際、型や値をチェックしてから更新するように`__set__`を定義する。
- 値の更新を禁止するために`__set__`でエラーを吐くようにする。

のようなことができます。

しかし問題は、毎回新しいクラスを定義するのは面倒だということです。メインのクラスである`B`とは別に、`__get__`と`__set__`を有した`Munou`クラスを定義しましたが、そもそも`B`にしか使わないのにわざわざ`Munou`を別で用意するといった大掛かりな作業は避けたいです。

ここで出てくるのが、標準実装の`property`クラスです。

###### `property`クラスの実装

`property`クラスはPythonで真似れば次のような実装になっています。

```python
class property:
    def __init__(self, fget=None, fset=None):
        self.fget = fget # ゲッター関数（なくてもよい）
        self.fset = fset # セッター関数（なくてもよい）

    def getter(self, fget):
        """ゲッター関数を更新した新たなpropertyを返す"""
        return property(fget=fget, fset=self.fset)

    def __get__(self, obj, objtype=None):
        if self.fget is None:
            raise AttributeError("unreadable attribute") # ゲッター関数がなければエラー
        return self.fget(obj) # ゲッター関数を呼び出す

    def setter(self, fset):
        """セッター関数を更新した新たなpropertyを返す"""
        return property(fget=self.fget, fset=fset)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute") # セッター関数がなければエラー
        return self.fset(obj, value) # セッター関数を呼び出す
```
（:warning: これもいろいろな意味で完全ではありません。あくまで説明のためです）

つまり、`property`のインスタンス`prop`を何かのクラスに定義し、適宜`prop`にゲッター関数やセッター関数を与えているとすると、インスタンス`ins`から`prop`にアクセスするとき、

- `ins.prop`では、ゲッター関数があればそれを呼び出し、なければ書き込み専用ということでエラーを返す。
- `ins.prop = 0`では、セッター関数があればそれを呼び出し、なければ読み取り専用ということでエラーを返す。

というルールに従うということです。したがって「`__get__`や`__set__`を持ったクラスを定義する」代わりに「ゲッター関数とセッター関数を定義する」ことでほぼ同じ仕組みが実現できることになります。

###### `property`の利用

それでは、自己紹介と、セットしようとした値の説明をするだけの`property`を作ります。ゲッター関数とセッター関数を定義します。

```python
def getter_func(self):
    print(f"I am {self}")

def setter_func(self, value):
    print(f"This is {value}")
```

これらをもちいて`property`インスタンスを作成します。

```python
prop = property(getter_func, setter_func)
prop  # [Out]: <property at 0x...>
```

ゲッター関数とセッター関数を別々でセットするのであれば次のコードで同じ`prop`が得られます。

```python
prop0 = property(getter_func)
prop = prop0.setter(setter_func)
prop  # [Out]: <property at 0x...>
```

この`property`のインスタンス`prop`をclassに渡します。

```python
class C:
    prop = prop
```

すると値のアクセス/代入をするたびに対応する関数が呼び出されます。

```python
c = C()
c.prop  # [Out]: I am <__main__.C object at 0x...>
c.prop = 0  # [Out]: This is 0
```



# よく見る`property`の使い方

さて、ようやくいつもの使い方が出てきます。ポイントは

1. ゲッター関数・セッター関数はどうせクラス外部から使わないので、クラス内で定義する。
2. `property`のメソッド、`__init__`と`setter`は単一の関数を引数にとれるので、できるだけデコレータを使う。

の2点です。

#### デコレータのとは

関数またはクラスを引数にとり、引数１つで動く関数は、デコレータ表記ができます。たとえば`list`に関数を追加するとき、

```python
def func():
    return 0

function_list.append(func)
```

と

```python
@function_list.append
def func():
    return 0
```

はほぼ等価です。後者では`func`には`function_list.append(func)`の返り値が格納されるので、`func`が`None`になっているという違いだけです。

###### デコレータを使った`property`の定義

以上から、`property`の`__init__`, `getter`, `setter`関数はすべてデコレータ表記できます。したがって

```python
def getter_func(self):
    print(f"I am {self}")

def setter_func(self, value):
    print(f"This is {value}")

prop0 = property(getter_func)
prop = prop0.setter(setter_func)
```

と

```python
@property
def getter_func(self):
    print(f"I am {self}")

@getter_func.setter
def setter_func(self, value):
    print(f"This is {value}")
```

はほぼ等価です。そして、`getter_func`はゲッター関数だけ定義された中間産物で不要なので、すべて同じ変数名`prop`で問題ないです。

```python
@property
def prop(self):
    print(f"I am {self}")

@prop.setter
def prop(self, value):
    print(f"This is {value}")
```

このように、はじめの方法で出てきた不要な変数名`getter_func`, `setter_func`, `prop0`を書かずに`prop`を定義できたので、デコレータを使った方法が良いとされています。これをクラス内でやればOKです。

```python
class C:
    @property
    def prop(self):
        print(f"I am {self}")

    @prop.setter
    def prop(self, value):
        print(f"This is {value}")
```

# まとめ

以上のように`property`というのは、Pythonでのクラスとインスタンスのアクセス関係を利用して、読み取り専用・書き込み専用のような変数を可読性高く簡単に定義するためのものでした。この仕組みが分かっていれば無用なトラブルシューティングが避けられますね。

また、`__get__`/`__set__`はPythonで魔法のような（直観的で使いやすいが一見Pythonの範疇を超えているかのように動作する）クラスを作るときに非常に有用です。ぜひ使えるようにしたいメソッドです。