Manjusaka

Manjusaka

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

久しぶりに 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 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("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 の黒魔法》

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