久しぶりに Flask コードに関することを書いていないので、ちょっと恥ずかしいですが、今回は Flask に関することは書きません。文句があるならかかってこい(こんな感じで、やってみろよ
今回は Python の非常に重要なもの、つまり Descriptor(ディスクリプタ)について書きます。
ディスクリプタとの初対面#
いつものように、Talk is cheap, Show me the code. まずはコードを見てみましょう。
class Person(object):
""""""
#----------------------------------------------------------------------
def __init__(self, first_name, last_name):
"""Constructor"""
self.first_name = first_name
self.last_name = last_name
#----------------------------------------------------------------------
@property
def full_name(self):
"""
Return the full name
"""
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("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
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):
"""Constructor"""
self.first_name = first_name
self.last_name = last_name
#----------------------------------------------------------------------
@Property
def full_name(self):
"""
Return the full name
"""
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 the full name
"""
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 では、new class の使用において明示的に 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("Something went wrong")
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("unreadable attribute")
return self.fget(obj)
def __set__(self, obj, value=None):
if value is None:
raise TypeError("You can't set value as None")
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
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 の黒魔法》