はじめに
Twitterで一時期流行していた 100 Days Of Code なるものを先日知りました。本記事は、初学者である私が100日の学習を通してどの程度成長できるか記録を残すこと、アウトプットすることを目的とします。誤っている点、読みにくい点多々あると思います。ご指摘いただけると幸いです!
今回学習する教材
-
- 8章構成
- 本章216ページ
今日の進捗
- 進行状況: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
を何度も使うことなく、簡潔にコードを書くことができました。
参考サイト