0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

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

今回学習する教材

今日の進捗

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

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

前提知識

@staticmethod

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

__get__メソッド

object.__get__(self, instance, owner)

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

例を見てみます。

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__メソッドを呼び出す

出力結果

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

例を見てみましょう。

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

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__()が呼び出されます。

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

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)

出力結果

Writing 80
Science 99
Second 75

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

参考サイト

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?