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("elapsed time is %s " % (time.time() - start_time))
        return result

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

@Timeit
def func():
    time.sleep(1)
    return "invoking function func"


if __name__ == '__main__':
    func()  # output: elapsed time is 1.00044410133

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

class A(object):
    @Timeit
    def func(self):
        time.sleep(1)
        return 'invoking method func'


if __name__ == '__main__':
    a = A()
    a.func()  # Boom!

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

答え#

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

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

    def __call__(self, *args, **kwargs):
        print('invoking Timer')

    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("invoking __call__ from A!")


if __name__ == "__main__":
    a = A()
    a()  # output: invoking __call__ from A

今、私たちはa()a.__call__()と等しいように見えることがわかります。簡単そうですね。さて、私はまた無謀なことをして、次のようなコードを書きました。

a.__call__ = lambda: "invoking __call__ from lambda"
a.__call__()
# output:invoking __call__ from lambda
a()


# output:invoking __call__ from A!

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

答え#

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

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

同様に、公式も次の例を示しています。

class C(object):
    pass


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


# Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
# TypeError: object of type 'C' has no len()

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

4. デスクリプタ#

説明#

私は、属性 math が [0,100] の整数であり、範囲外の値を割り当てると例外を投げる Exam クラスを書きたいと思います。この要件をデスクリプタを使って実現することにしました。

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('grade must be between 0 and 100')


class Exam(object):
    math = Grade()

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


if __name__ == '__main__':
    niche = Exam(math=90)
    print(niche.math)
    # output : 90
    snake = Exam(math=75)
    print(snake.math)
    # output : 75
    snake.math = 120
    # output: ValueError:grade must be between 0 and 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("fuck")

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

答え#

1. 最初の問題は非常に簡単です。print(niche.math)をもう一度実行すると、出力値が75であることがわかります。なぜでしょうか?これは Python の呼び出しメカニズムから説明できます。属性を呼び出す場合、その順序は最初にインスタンスの__dict__を検索し、見つからなければ、クラス辞書、親クラス辞書を順に検索し、最終的に見つからなくなるまで続きます。さて、私たちの問題に戻ると、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("fuck")

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("fuck")

    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)
# output: 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
# output: __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())  # output: True

结语#

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

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