Manjusaka

Manjusaka

Python デスクリプタ入門ガイド

久しぶりに 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 descriptorsnon data descriptors について説明しましょう。data descriptorsnon 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 属性が存在するため、クラス辞書内の areanon 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 黒魔法》

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。