ディスクリプタを使うことで、@property メソッドの振る舞いを再利用する【16/100】

# はじめに

Twitterで一時期流行していた 100 Days Of Code なるものを先日知りました。本記事は、初学者である私が100日の学習を通してどの程度成長できるか記録を残すこと、アウトプットすることを目的とします。誤っている点、読みにくい点多々あると思います。ご指摘いただけると幸いです！

# 今回学習する教材

- [Effective Python](https://www.amazon.co.jp/Effective-Python-%E2%80%95Python%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%82%92%E6%94%B9%E8%89%AF%E3%81%99%E3%82%8B59%E9%A0%85%E7%9B%AE-Brett-Slatkin/dp/4873117569/ref=sr_1_3?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=Effective+python&qid=1609408832&sr=8-3)

    - 8章構成
    - 本章216ページ

# 今日の進捗

- 進行状況：95-101ページ
- 第4章：メタクラスと属性
- 本日学んだことの中で、よく忘れるところ、知らなかったところを書いていきます。

## 再利用可能な `@property` メソッドにディスクリプタを使う

### 前提知識

#### @staticmethod

メソッドを静的メソッドに変換します。
静的メソッドは暗黙の第一引数を受け取りません。
つまり、self を第一引数に持ちません。
静的メソッドをクラスやクラスインスタンスから取得すると、実際に返されるオブジェクトはラップされたオブジェクトになり、それ以上は変換の対象になりません。

#### `__get__`メソッド

`object.__get__(self, instance, owner)`

`__get__`メソッドは、所有クラスの属性またはそのクラスのインスタンスの属性が取得されるたびに呼び出されるメソッドです。


例を見てみます。

```python
class SampleGet(object):
    def __init__(self, x):
        self._x = x

    def __get__(self, instance, owner):
        print('Hello')
        print(instance)
        print(owner)
        return owner

class Sample(object):
    sample_get = SampleGet(100)

obj = Sample()
print('print', obj.sample_get)               # クラスのインスタンスの属性を取得したため、__get__メソッドを呼び出す
```

出力結果

```result
Hello
<__main__.Sample object at 0x00000289EDAB11C8>          # instance
<class '__main__.Sample'>                               # owner
print <class '__main__.Sample'>                         # obj.sample_get 
```

#### `__set__`メソッド

`object.__set__(self, instance, value)`

`__get__`メソッドはオーナークラスのインスタンス (instance) 上の属性を新たな値 (value)に設定する際に呼び出さるメソッドです。

class SampleGet

例を見てみましょう。

```python
class SampleSet(object):
    def __init__(self, x):
        self._x = x

    def __set__(self, instance, value):
        print('World!')
        print(instance)
        print(value)
        return self.value

class Sample(object):
    sample_set = SampleSet(100)

obj = Sample()                                      # Sampleクラスのインスタンス生成
obj.sample_set = 200                                # インスタンス上の属性を新たな値 (200) に設定
# World!                                            # インスタンス上の属性の値が変更されたため、__set__メソッドが呼び出され、World! と出力
# <__main__.Sample object at 0x000002069D921188>    # instance
# 200                                               # value
```

#### ディスクリプタ (descriptor)

ディスクリプタは、属性アクセスを言語でどのように解釈するかを定義します。
ディスクリプタのクラスは、`__get__`メソッドや`__set__`メソッドおよび、`__delete__`メソッドを使って、属性アクセスをオーバーライドするものです。
これらのメソッドのいずれかがオブジェクトに対して定義されている場合、オブジェクトはでスクリプタであるといいます。

#### weakref

Python 組み込みモジュールの一つで、メモリリーク対策をすることができます。
このモジュールは、実行時にプログラムでインスタンスの最後に残っている参照が、その辞書のキー集合で保持されているだけの場合、そこからインスタンスを削除します。

### 本題

`@property`は逐次的に必要な処理を追加していけるので、非常に便利なのですが、再利用できないことが欠点です。
デコレートするメソッドを同じクラスの複数の属性で再利用することができないのです。
これを解決するために、ディスクリプタを使います。
例として、学生の試験の科目ごとの評価を管理するクラスを作る場合を考えます。

```python
class Grade(object):
    # ディスクリプタ
    def __get__(self, instance, instance_type):
        pass
    
    def __set__(self, instance, value):
        pass

class Exam(object):
    # 各科目ごとにGradeインスタンスを生成
    math_grade = Grade()
    writing_grade = Grade()
    science_grade = Grade()

exam = Exam()
exam.writing_grade = 40
print(exam.writing_grade)

```

`exam.writing_grade = 40`は次のように解釈されます。
`Exam.__dict_['writing_grade'].__set__(exam, 40)`
つまり、`__set__(self, instance, value)`の instance に exam、value に 40 を引数として`__set__()`が呼び出されます。
`print(exam.writing_grade)`は次のように解釈されます。
`print(Exam.__dict__['writing_grade'].__get__(exam, Exam))`
つまり、`__get__(self, instance, value)`のinsntace に exam、Exam に value　を引数として`__get__()`が呼び出されます。

では、実装を進めていきます。

```python
from weakref import WeakKeyDictionary

class Grade(object):
    def __init__(self):
        # self._values = {}                  # これだとインスタンスの参照がゼロになることが無いので、ガーベージコレクションでメモリが回収できない
        self._values = WeakKeyDictionary()   # 辞書と置き換えられる、インスタンスの最後に残っている参照が、その辞書のキー集合のみの場合、Exam インスタンスを削除する

    def __get__(self, instance, instance_type):
        if instance is None:
            return self
        return self._values.get(instance, 0)

    def __set__(self, instance, value):
        if not (0 <= value <= 100):
            raise ValueError('Grade must be between 0 and 100')
        self._values[instance] = value

first_exam = Exam()
first_exam.writing_grade = 80
first_exam.science_grade = 99
print('Writing', first_exam.writing_grade)
print('Science', first_exam.science_grade)
second_exam = Exam()
second_exam.writing_grade = 75
print('Second', second_exam.writing_grade)
```

出力結果

```result
Writing 80
Science 99
Second 75
```

ディスクリプタを使うことで、`@property`を何度も使うことなく、簡潔にコードを書くことができました。

# 参考サイト

https://docs.python.org/ja/3.7/reference/datamodel.html#object