久しぶりに Flask コードに関することを書いていないので、ちょっと恥ずかしいですが、今回は Flask に関することは書きません。文句があるならかかってきなさい(こんな感じで、やるならかかってこい
今回は Python の非常に重要なもの、つまり Descriptor(ディスクリプタ)について書きます。
ディスクリプタとの初対面#
いつものように、Talk is cheap, Show me the code. まずはコードを見てみましょう。
class Person(object):
""""""
#----------------------------------------------------------------------
def __init__(self, first_name, last_name):
"""コンストラクタ"""
self.first_name = first_name
self.last_name = last_name
#----------------------------------------------------------------------
@property
def full_name(self):
"""
フルネームを返す
"""
return "%s %s" % (self.first_name, self.last_name)
if __name__=="__main__":
person = Person("Mike", "Driscoll")
print(person.full_name)
# 'Mike Driscoll'
print(person.first_name)
# 'Mike'
このコードは皆さんもよく知っているでしょう。ええ、property
ですから、誰でも知っていますが、property
の実装メカニズムを理解していますか?何がわからない?それなら Python を学ぶ意味がないですね。。。冗談です、次のコードを見てみましょう。
class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("読み取り不可の属性")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("属性を設定できません")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("属性を削除できません")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
見た目は複雑そうですが、大丈夫です、一歩ずつ見ていきましょう。ただし、まず結論を述べます:ディスクリプタは特別なオブジェクトであり、__get__
、__set__
、__delete__
の 3 つの特殊メソッドを実装しています。
ディスクリプタの詳細#
Property について#
前述の通り、Property の実装コードを示しました。では、これについて詳しく説明しましょう。
class Person(object):
""""""
#----------------------------------------------------------------------
def __init__(self, first_name, last_name):
"""コンストラクタ"""
self.first_name = first_name
self.last_name = last_name
#----------------------------------------------------------------------
@Property
def full_name(self):
"""
フルネームを返す
"""
return "%s %s" % (self.first_name, self.last_name)
if __name__=="__main__":
person = Person("Mike", "Driscoll")
print(person.full_name)
# 'Mike Driscoll'
print(person.first_name)
# 'Mike'
まず、デコレーターについて知らない場合は、こちらの記事を見てください。簡単に言うと、コードを実行する前に、インタプリタがコードをスキャンし、デコレーターに関わる部分を置き換えます。クラスデコレーターも同様です。前述のコードでは、
@Property
def full_name(self):
"""
フルネームを返す
"""
return "%s %s" % (self.first_name, self.last_name)
この部分が full_name=Property(full_name)
をトリガーします。そして、後でインスタンス化したオブジェクトで person.full_name
を呼び出すと、実際には person.full_name.__get__(person)
と等価になり、__get__()
メソッド内の return self.fget(obj)
が実行されます。つまり、元々書いた def full_name
内のコードが実行されることになります。
この時、皆さんは getter()
、setter()
、および deleter()
の具体的な動作メカニズムについて考えてみてください。もしまだ問題があれば、コメントで議論しましょう。
ディスクリプタについて#
以前に述べた定義を覚えていますか?ディスクリプタは特別なオブジェクトであり、__get__
、__set__
、__delete__
の 3 つの特殊メソッドを実装しています。 そして、Python の公式ドキュメントの説明には、ディスクリプタの重要性を示すために次のような一文があります。「それらはプロパティ、メソッド、静的メソッド、クラスメソッド、super () の背後にあるメカニズムです。Python 自体の新しいスタイルのクラスを実装するために使用されています。」簡単に言うと、ディスクリプタがあってこそ、天があり、地があり、空気があるということです。新しいスタイルのクラスでは、属性、メソッドの呼び出し、静的メソッド、クラスメソッドなどはすべてディスクリプタの特定の使用に基づいています。
さて、なぜディスクリプタがこれほど重要なのか、疑問に思うかもしれません。心配しないで、次を見ていきましょう。
ディスクリプタの使用#
まず、次のコードを見てください。
class A(object): #注:Python 3.x では、新しいクラスの使用において明示的に object クラスから継承する必要はありませんが、Python 2.X(x>2)の場合は必要です。
def a(self):
pass
if __name__=="__main__":
a=A()
a.a()
皆さんは a.a()
という文があることに気づいたでしょう。さて、メソッドを呼び出すときに何が起こるのか考えてみてください。
わかりましたか?思いつきましたか?わからない?それなら続けましょう。
まず、属性を呼び出すとき、メンバーであれメソッドであれ、必ず __getattribute__()
というメソッドが呼び出されます。私たちの __getattribute__()
メソッド内で、呼び出そうとしている属性がディスクリプタプロトコルを実装している場合、次のような呼び出しプロセスが発生します:type(a).__dict__['a'].__get__(b,type(b))
。ここで、優先順位の順序を示す必要があります。もし呼び出そうとしている属性が data descriptors
であれば、その属性がインスタンスの __dict__
辞書に存在するかどうかに関係なく、まずディスクリプタの __get__
メソッドが呼び出されます。もし呼び出そうとしている属性が non data descriptors
であれば、まずインスタンスの __dict__
に存在する属性が呼び出され、存在しない場合はクラスや親クラスの __dict__
に含まれる属性を上に向かって探します。一旦属性が存在すれば、__get__
メソッドが呼び出され、存在しなければ __getattr__()
メソッドが呼び出されます。理解するのは少し抽象的ですか?大丈夫です、すぐに説明しますが、まず data descriptors
と non data descriptors
について説明しましょう。data descriptors
と non data descriptors
とは何でしょうか?実はとても簡単です。ディスクリプタの中で __get__
と __set__
の両方のプロトコルを実装しているものが data descriptors
であり、__get__
のみを実装しているものが non data descriptors
です。では、例を見てみましょう。
import math
class lazyproperty:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
if instance is None:
return self
else:
value = self.func(instance)
setattr(instance, self.func.__name__, value)
return value
class Circle:
def __init__(self, radius):
self.radius = radius
pass
@lazyproperty
def area(self):
print("Com")
return math.pi * self.radius * 2
def test(self):
pass
if __name__=='__main__':
c=Circle(4)
print(c.area)
さて、このコードを詳しく見てみましょう。まず、クラスディスクリプタ @lazyproperty
の置き換えプロセスについては、前述の通り繰り返しません。次に、最初に c.area
を呼び出すと、まずインスタンス c
の __dict__
に area
ディスクリプタが存在するかどうかを確認します。すると、c
にはそのディスクリプタも属性も存在しないことがわかります。次に、Circle
の __dict__
を上に向かって確認し、area
という名前の属性を見つけます。これは non data descriptors
です。インスタンス辞書に area
属性が存在しないため、クラス辞書内の area
の __get__
メソッドが呼び出され、__get__
メソッド内で setattr
メソッドを使用してインスタンス辞書に area
属性が登録されます。次に、後続の c.area
の呼び出しでは、インスタンス辞書内に area
属性が存在するため、クラス辞書内の area
は non data descriptors
であり、__get__
メソッドはトリガーされず、インスタンスの辞書から直接属性値が取得されます。
ディスクリプタの使用#
ディスクリプタの使用範囲は広いですが、その主な目的は呼び出しプロセスを制御可能にすることです。したがって、呼び出しプロセスを細かく制御する必要がある場合にディスクリプタを使用します。例えば、前述の例のように、
class lazyproperty:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
if instance is None:
return self
else:
value = self.func(instance)
setattr(instance, self.func.__name__, value)
return value
def __set__(self, instance, value=0):
pass
import math
class Circle:
def __init__(self, radius):
self.radius = radius
pass
@lazyproperty
def area(self, value=0):
print("Com")
if value == 0 and self.radius == 0:
raise TypeError("何かがうまくいかなかった")
return math.pi * value * 2 if value != 0 else math.pi * self.radius * 2
def test(self):
pass
ディスクリプタの特性を利用して遅延読み込みを実現することができます。また、属性の値の設定を制御することもできます。
class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("読み取り不可の属性")
return self.fget(obj)
def __set__(self, obj, value=None):
if value is None:
raise TypeError("値を None に設定することはできません")
if self.fset is None:
raise AttributeError("属性を設定できません")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("属性を削除できません")
self.fdel(obj)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
class test():
def __init__(self, value):
self.value = value
@Property
def Value(self):
return self.value
@Value.setter
def test(self, x):
self.value = x
上記の例のように、渡された値が有効かどうかを判断することができます。
まとめ#
Python のディスクリプタは新しいスタイルのクラスの呼び出しチェーンの基盤であり、すべてのメソッド、メンバー、変数の呼び出しにはディスクリプタが介入します。また、ディスクリプタの特性を利用して、呼び出しプロセスをより制御可能にすることができます。この点は、多くの著名なフレームワークで見られます。
参考#
1.《Python Cookbook》 8.10 章 P271
2.《Descriptor HowTo Guid》
3.《Python 黒魔法》