Manjusaka

Manjusaka

Pythonを使えると聞きましたか?

前言#

最近、Python が「簡単すぎる」と感じたので、師匠の川爷の前で大胆に言いました:「私は Python が世界で最も簡単な言語だと思います!」。すると川爷の口元に軽蔑の微笑みが浮かびました(内心 OS:Naive!Python 開発者として、あなたに少し人生経験を教えなければ、天高く地厚いことを知らないでしょう!)。それで川爷は私に満点 100 点の問題を出しました。そしてこの記事は、その問題を解く過程での失敗を記録したものです。

1. リスト内包表記#

説明#

以下のコードはエラーになります。なぜでしょう?

class A(object):
    x = 1
    gen = (x for _ in xrange(10))  # gen=(x for _ in range(10))


if __name__ == "__main__":
    print(list(A.gen))

答え#

この問題は変数のスコープの問題です。gen=(x for _ in xrange(10))の中で、gengeneratorであり、generator内の変数は独自のスコープを持ち、他のスコープとは隔離されています。したがって、NameError: name 'x' is not definedという問題が発生します。では、解決策は何でしょうか?答えは:lambda を使うことです。

class A(object):
    x = 1
    gen = (lambda x: (x for _ in xrange(10)))(x)  # gen=(x for _ in range(10))


if __name__ == "__main__":
    print(list(A.gen))

またはこうすることもできます。

class A(object):
    x = 1
    gen = (A.x for _ in xrange(10))  # gen=(x for _ in range(10))


if __name__ == "__main__":
    print(list(A.gen))

補足#

コメント欄で提案してくれた方々に感謝します。ここで公式文書の説明を示します:
クラスブロック内で定義された名前のスコープはクラスブロックに限定されており、メソッドのコードブロックには拡張されません。これには、関数スコープを使用して実装されている内包表記やジェネレーター式も含まれます。つまり、以下のコードは失敗します:

class A:
    a = 42
    b = list(a + i for i in range(10))

参考リンク Python2 Execution-ModelPython3 Execution-Model。これは PEP 227 で追加された提案だと言われています。後でさらに詳しく調査します。再度、コメント欄の @没头脑很着急 @涂伟忠 @Cholerae の 3 名に感謝します。

2. デコレーター#

説明#

関数 / メソッドの実行時間を測定するためのクラスデコレーターを書きたいです。

import time

class Timeit(object):
    def __init__(self, func):
        self._wrapped = func

    def __call__(self, *args, **kws):
        start_time = time.time()
        result = self._wrapped(*args, **kws)
        print("経過時間は %s です " % (time.time() - start_time))
        return result

このデコレーターは通常の関数で動作します:

@Timeit
def func():
    time.sleep(1)
    return "関数funcを呼び出しています"


if __name__ == '__main__':
    func()  # 出力: 経過時間は 1.00044410133

しかし、メソッドで実行するとエラーになります。なぜでしょう?

class A(object):
    @Timeit
    def func(self):
        time.sleep(1)
        return 'メソッドfuncを呼び出しています'


if __name__ == '__main__':
    a = A()
    a.func()  # エラー!

もし私がクラスデコレーターを使い続けるなら、どう修正すればよいでしょうか?

答え#

クラスデコレーターを使用すると、func関数を呼び出す過程で、その対応するインスタンスが__call__メソッドに渡されないため、method unboundが発生します。では、解決策は何でしょうか?デスクリプタが最高です。

class Timeit(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print('タイマーを呼び出しています')

    def __get__(self, instance, owner):
        return lambda *args, **kwargs: self.func(instance, *args, **kwargs)

3.Python 呼び出しメカニズム#

説明#

私たちは__call__メソッドが括弧の呼び出しをオーバーロードするために使用できることを知っています。さて、問題はそんなに簡単だと思いましたか?Naive!

class A(object):
    def __call__(self):
        print("Aから__call__を呼び出しています!")


if __name__ == "__main__":
    a = A()
    a()  # 出力: Aから__call__を呼び出しています

今、私たちはa()a.__call__()と等価であるように見えることがわかります。簡単そうですね。さて、私はまたしても危険なことを考え、以下のコードを書きました。

a.__call__ = lambda: "lambdaから__call__を呼び出しています"
a.__call__()
# 出力: lambdaから__call__を呼び出しています
a()


# 出力: Aから__call__を呼び出しています!

皆さん、なぜa()a.__call__()を呼び出さなかったのか説明してください(この問題は USTC の王子博先輩が提起しました)。

答え#

理由は、Python では新しいスタイルのクラス(new class)の組み込み特殊メソッドとインスタンスの属性辞書が相互に隔離されているためです。具体的には、Python 公式の文書でこの状況についての説明があります。

新しいスタイルのクラスの場合、特殊メソッドの暗黙の呼び出しは、オブジェクトの型に定義されている場合にのみ正しく動作することが保証されており、オブジェクトのインスタンス辞書ではありません。この動作が、以下のコードが例外を発生させる理由です(古いスタイルのクラスの同等の例とは異なります)。

公式も以下の例を示しています:

class C(object):
    pass


c = C()
c.__len__ = lambda: 5
len(c)


# トレースバック(最も最近の呼び出しの最後):
#  File "<stdin>", line 1, in <module>
# TypeError: object of type 'C' has no len()

私たちの例に戻ると、a.__call__=lambda:"lambdaから__call__を呼び出しています"を実行したとき、確かにa.__dict____call__というキーを持つアイテムが新たに追加されましたが、a()を実行すると、特殊メソッドの呼び出しが関与するため、呼び出しプロセスはa.__dict__から属性を探すのではなく、type(a).__dict__から属性を探すことになります。したがって、上記のような状況が発生します。

4. デスクリプタ#

説明#

私は Exam クラスを作成したいと思っています。その属性 math は [0,100] の整数であり、値を割り当てるときにこの範囲外であれば例外をスローします。この要件をデスクリプタを使用して実装することに決めました。

class Grade(object):
    def __init__(self):
        self._score = 0

    def __get__(self, instance, owner):
        return self._score

    def __set__(self, instance, value):
        if 0 <= value <= 100:
            self._score = value
        else:
            raise ValueError('成績は0から100の間でなければなりません')


class Exam(object):
    math = Grade()

    def __init__(self, math):
        self.math = math


if __name__ == '__main__':
    niche = Exam(math=90)
    print(niche.math)
    # 出力 : 90
    snake = Exam(math=75)
    print(snake.math)
    # 出力 : 75
    snake.math = 120
    # 出力: ValueError:成績は0から100の間でなければなりません!

すべてが正常に見えます。しかし、ここには大きな問題があります。その問題を説明してください。
この問題を解決するために、Grade デスクリプタを次のように書き直しました:

class Grad(object):
    def __init__(self):
        self._grade_pool = {}

    def __get__(self, instance, owner):
        return self._grade_pool.get(instance, None)

    def __set__(self, instance, value):
        if 0 <= value <= 100:
            _grade_pool = self.__dict__.setdefault('_grade_pool', {})
            _grade_pool[instance] = value
        else:
            raise ValueError("ふざけるな")

しかし、これによりさらに大きな問題が発生します。どう解決すればよいでしょうか?

答え#

1. 最初の問題は実際には非常に簡単です。print(niche.math)をもう一度実行すると、出力値が75であることがわかります。これはなぜでしょうか?これは Python の呼び出しメカニズムから始まります。属性を呼び出す場合、その順序は最初にインスタンスの__dict__を検索し、次に見つからない場合はクラス辞書、親クラス辞書を順に検索し、完全に見つからないまで続きます。さて、私たちの問題に戻ると、Examクラスの中で、self.mathの呼び出しプロセスは、まずインスタンス化されたインスタンスの__dict__で検索し、見つからない場合、次にExamクラスで検索します。見つかりましたので、返します。つまり、self.mathに対するすべての操作はクラス変数mathに対する操作です。したがって、変数汚染の問題が発生します。では、どうすれば解決できるでしょうか?多くの人が言うかもしれませんが、__set__関数内で値を具体的なインスタンス辞書に設定すればいいのではないかと。
これが可能かどうかというと、明らかに不可能です。なぜなら、それは Python のデスクリプタのメカニズムに関係しているからです。デスクリプタとは、デスクリプタプロトコルを実装した特別なクラスであり、3 つのデスクリプタプロトコルは__get____set____delete__および Python 3.6 で新たに追加された__set_name__メソッドです。__get____set__/__delete__/__set_name__を実装したものがデータデスクリプタであり、__get__のみを実装したものが非データデスクリプタです。では、違いは何でしょうか?前述のように、** 属性を呼び出す場合、その順序は最初にインスタンスの__dict__を検索し、次にクラス辞書、親クラス辞書を順に検索し、完全に見つからないまで続きます。このとき、クラスインスタンス辞書にその属性がデータデスクリプタである場合、インスタンス辞書にその属性が存在するかどうかに関係なく、無条件にデスクリプタプロトコルを呼び出します。クラスインスタンス辞書にその属性が非データデスクリプタである場合、優先的にインスタンス辞書の属性値を呼び出し、デスクリプタプロトコルをトリガーしません。インスタンス辞書にその属性値が存在しない場合、非データデスクリプタのデスクリプタプロトコルがトリガーされます。** 以前の問題に戻ると、__set__で具体的な属性をインスタンス辞書に書き込んでも、クラス辞書にデータデスクリプタが存在するため、math属性を呼び出すと、依然としてデスクリプタプロトコルがトリガーされます。

2. 改良されたアプローチでは、dictのキーの一意性を利用して、具体的な値をインスタンスにバインドしますが、同時にメモリリークの問題が発生します。なぜメモリリークが発生するのでしょうか?まず、dictの特性を復習しましょう。dictの最も重要な特性は、ハッシュ可能なオブジェクトはすべてキーになれることです。dictはハッシュ値の一意性を利用して(厳密には一意ではなく、ハッシュ値の衝突の確率が非常に低いため、ほぼ一意と見なされます)、キーの重複を防ぎます。同様に(重要なポイントです)、dictのキーの参照は強い参照タイプであり、対応するオブジェクトの参照カウントを増加させる可能性があるため、オブジェクトがガベージコレクションされないことがあり、メモリリークが発生します。では、これをどう解決すればよいでしょうか?2 つの方法があります。
最初の方法:

class Grad(object):
    def __init__(self):
        import weakref
        self._grade_pool = weakref.WeakKeyDictionary()

    def __get__(self, instance, owner):
        return self._grade_pool.get(instance, None)

    def __set__(self, instance, value):
        if 0 <= value <= 100:
            _grade_pool = self.__dict__.setdefault('_grade_pool', {})
            _grade_pool[instance] = value
        else:
            raise ValueError("ふざけるな")

weakref ライブラリのWeakKeyDictionaryによって生成された辞書のキーはオブジェクトへの弱い参照タイプであり、メモリ参照カウントの増加を引き起こさないため、メモリリークを防ぎます。同様に、値がオブジェクトへの強い参照を引き起こさないようにするために、WeakValueDictionaryを使用できます。
2 番目の方法:Python 3.6 では、PEP 487 提案が実装され、デスクリプタに新しいプロトコルが追加され、対応するオブジェクトをバインドするために使用できます:

class Grad(object):
    def __get__(self, instance, owner):
        return instance.__dict__[self.key]

    def __set__(self, instance, value):
        if 0 <= value <= 100:
            instance.__dict__[self.key] = value
        else:
            raise ValueError("ふざけるな")

    def __set_name__(self, owner, name):
        self.key = name

この問題は多くのことを含んでいます。ここでいくつかの参考リンクを示します。invoking-descriptorsDescriptor HowTo GuidePEP 487Python 3.6 の新機能

5.Python 継承メカニズム#

説明#

以下のコードの出力結果を求めてください。

class Init(object):
    def __init__(self, value):
        self.val = value


class Add2(Init):
    def __init__(self, val):
        super(Add2, self).__init__(val)
        self.val += 2


class Mul5(Init):
    def __init__(self, val):
        super(Mul5, self).__init__(val)
        self.val *= 5


class Pro(Mul5, Add2):
    pass


class Incr(Pro):
    csup = super(Pro)

    def __init__(self, val):
        self.csup.__init__(val)
        self.val += 1


p = Incr(5)
print(p.val)

答え#

出力は 36 です。具体的にはNew-style Classesmultiple-inheritanceを参照してください。

6. Python 特殊メソッド#

説明#

私は__new__メソッドをオーバーロードしてシングルトンパターンを実装するクラスを書きました。

class Singleton(object):
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance:
            return cls._instance
        cls._isntance = cv = object.__new__(cls, *args, **kwargs)
        return cv


sin1 = Singleton()
sin2 = Singleton()
print(sin1 is sin2)
# 出力: True

今、私はシングルトンパターンを実装するために多くのクラスを持っているので、メタクラスを使ってコードを再利用しようと考えています:

class SingleMeta(type):
    def __init__(cls, name, bases, dict):
        cls._instance = None
        __new__o = cls.__new__

        def __new__(cls, *args, **kwargs):
            if cls._instance:
                return cls._instance
            cls._instance = cv = __new__o(cls, *args, **kwargs)
            return cv

        cls.__new__ = __new__


class A(object):
    __metaclass__ = SingleMeta


a1 = A()  # 何が起こるのか

ああ、イライラします。なぜこれがエラーになるのですか?私は以前、この方法で__getattribute__をパッチして成功したのに、以下のコードはすべての属性呼び出しをキャッチしてパラメータを印刷できます。

class TraceAttribute(type):
    def __init__(cls, name, bases, dict):
        __getattribute__o = cls.__getattribute__

        def __getattribute__(self, *args, **kwargs):
            print('__getattribute__:', args, kwargs)
            return __getattribute__o(self, *args, **kwargs)

        cls.__getattribute__ = __getattribute__


class A(object):  # Python 3では class A(object,metaclass=TraceAttribute):
    __metaclass__ = TraceAttribute
    a = 1
    b = 2


a = A()
a.a
# 出力: __getattribute__:('a',){}
a.b

なぜ__getattribute__をパッチするのが成功し、__new__をパッチするのが失敗したのか説明してください。
もし私がメタクラスを使って__new__をパッチしてシングルトンパターンを実装し続けるなら、どう修正すればよいでしょうか?

答え#

実際には最もイライラする点は、クラス内の__new__staticmethodであるため、置き換える際にはstaticmethodとして置き換える必要があります。答えは以下の通りです:

class SingleMeta(type):
    def __init__(cls, name, bases, dict):
        cls._instance = None
        __new__o = cls.__new__

        @staticmethod
        def __new__(cls, *args, **kwargs):
            if cls._instance:
                return cls._instance
            cls._instance = cv = __new__o(cls, *args, **kwargs)
            return cv

        cls.__new__ = __new__


class A(object):
    __metaclass__ = SingleMeta


print(A() is A())  # 出力: True

结语#

師匠の一連の問題に感謝します。新しい世界の扉を開いてくれました。ええ、ブログではエイリアスを使えないので、気持ちを伝えるしかありません。正直なところ、Python の動的特性は、さまざまな「黒魔法」を使って非常に快適な機能を実現できますが、同時に言語の特性や落とし穴を理解することがより厳格になります。皆さんが公式文書を読むことを願っています。早く装逼如風、常伴吾身の境地に達することを願っています。

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