拡張算術賦値の詳細:「-=」操作はどのように実現されますか?

5505 ワード

花下猫语:本编翻译依然としてBrett大男の"Python文法糖"シリーズ、彼は"-="操作の実现をデモンストレーションする时、意外にもCPythonの1つのバグを発见して、ついでに修复して、これは大男です......
原題|Unravelling augmented arithmetic assignment
作者|Brett Cannon
訳者|エンドウの花の下の猫(「Python猫」公衆号作者)
声明|本翻訳は交流学習の目的で、CC BY-NC-SA 4.0ライセンス契約に基づいている.読みやすいように、内容が少し変更されました.
序文
本文はPython文法糖シリーズの文章の一つである.最新のソースコードはdesugarプロジェクトで入手できます(https://github.com/brettcannon/desugar).
紹介する
Pythonには (augmented arithmetic assignment)というものがあります.この呼び方に慣れていないかもしれませんが、実際には数学の演算をしながら賦値を行います.例えば、a-=bは減算の強化算術賦値です.
拡張付与はPython 2.0バージョンに組み込まれています.(注:PEP-203に導入)
剖析-=Pythonではオーバーライド付与は許可されていないため、他の特殊な/マジックメソッドの操作に比べて、付与を強化する方法は想像していたのとは異なる可能性があります.
まず、a -= bは意味的にa = a-bと同じであることを知っておく必要があります.しかし、a - bのブラインド操作よりも、オブジェクトを変数名に割り当てることを事前に知っていれば、より効率的になる可能性があります.
たとえば、最低限の利点は、新しいオブジェクトの作成を回避することです.オブジェクトをその場で変更できる場合は、selfを返すと、新しいオブジェクトを再構築するよりも効率的です.
そのため、Pythonは1つの__を提供しました.isub__()メソッド.割り当て操作の左側(通常はlvalue)に定義されている場合、右側の値(通常はrvalue)が呼び出されます.したがって、a -= bについては、a.__を呼び出してみます.isub__(b).
呼び出しの結果がNotImplementedであるか、結果がまったく存在しない場合、Pythonは通常の2元演算:a - bに戻ります.(訳注:著者の二元演算に関する文章、訳文はここにある)
最終的には、いずれの方法を用いても、戻り値はaに割り当てられる.
以下は単純な擬似コードであり、a -= bは以下のように分解される.
#    a -= b     
if hasattr(a, "__isub__"):
    _value = a.__isub__(b)
    if _value is not NotImplemented:
        a = _value
    else:
        a = a - b
    del _value
 else:
     a = a - b

これらの方法をまとめる
二元算術演算を実現したので,帰納増強算術演算はあまり複雑ではない.
二元算術演算関数を入力し、自省(および発生する可能性のあるTypeErrorを処理する)を行うことで、きれいにまとめることができます.
def _create_binary_inplace_op(binary_op: _BinaryOp) -> Callable[[Any, Any], Any]:

    binary_operation_name = binary_op.__name__[2:-2]
    method_name = f"__i{binary_operation_name}__"
    operator = f"{binary_op._operator}="

    def binary_inplace_op(lvalue: Any, rvalue: Any, /) -> Any:
        lvalue_type = type(lvalue)
        try:
            method = debuiltins._mro_getattr(lvalue_type, method_name)
        except AttributeError:
            pass
        else:
            value = method(lvalue, rvalue)
            if value is not NotImplemented:
                return value
        try:
            return binary_op(lvalue, rvalue)
        except TypeError as exc:
            # If the TypeError is due to the binary arithmetic operator, suppress
            # it so we can raise the appropriate one for the agumented assignment.
            if exc._binary_op != binary_op._operator:
                raise
        raise TypeError(
            f"unsupported operand type(s) for {operator}: {lvalue_type!r} and {type(rvalue)!r}"
        )

    binary_inplace_op.__name__ = binary_inplace_op.__qualname__ = method_name
    binary_inplace_op.__doc__ = (
        f"""Implement the augmented arithmetic assignment `a {operator} b`."""
    )
    return binary_inplace_op

これにより、定義された-=サポート_create_binary_inplace_op(__ sub__),その他の内容を推定することができる:関数名、何を呼び出すか_i*__ 関数と、二元演算で問題が発生した場合に呼び出す呼び出し可能なオブジェクトを指定します.**=を使う人はほとんどいないことに気づきました
本文のコードを書く時、私は**=の1つの奇妙なテストの間違いにぶつかりました.すべての保証_pow__ 適切に呼び出されるテストでは、Python標準ライブラリのoperatorモジュールに失敗したテスト例があります.
私のコードは通常問題ありません.もしコードとCPythonのコードの間に違いがあれば、通常は私がどこが間違っているかを意味します.
しかし、コードをどんなに詳しく調べても、なぜ私のテストが合格したのか、標準ライブラリが失敗したのかを特定できません.
CPython内部で何が起こっているのかを深く知ることにしました.逆アセンブリバイトコードから:
>>> def test(): a **= b
... 
>>> import dis
>>> dis.dis(test)
  1           0 LOAD_FAST                0 (a)
              2 LOAD_GLOBAL              0 (b)
              4 INPLACE_POWER
              6 STORE_FAST               0 (a)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

これによりevalサイクルのINPLACE_POWERを見つけました.
        case TARGET(INPLACE_POWER): {
            PyObject *exp = POP();
            PyObject *base = TOP();
            PyObject *res = PyNumber_InPlacePower(base, exp, Py_None);
            Py_DECREF(base);
            Py_DECREF(exp);
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }

出典:https://github.com/python/cpython/blob/v3.8.3/Python/ceval.c#L1677 PyNumber_InPlacePower()が見つかりました
PyObject *
PyNumber_InPlacePower(PyObject *v, PyObject *w, PyObject *z)
{
    if (v->ob_type->tp_as_number &&
        v->ob_type->tp_as_number->nb_inplace_power != NULL) {
        return ternary_op(v, w, z, NB_SLOT(nb_inplace_power), "**=");
    }
    else {
        return ternary_op(v, w, z, NB_SLOT(nb_power), "**=");
    }
}

出典:https://github.com/python/cpython/blob/v3.8.3/Objects/abstract.c#L1172
ほっと~コード表示定義したら_ipow__,呼び出されますが、__がない場合にのみ呼び出されます.ipow__ が呼び出されます.pow__.
ただし、正しい方法は、呼び出し_の場合です.ipow__ に問題が発生し、NotImplementedが返されたか、まったく返されなかった場合は、__を呼び出すべきです.pow__ および_rpow__.
言い換えれば、存在する場合ipow__ すると、上記のコードは意外にもa**bのバックアップ意味をスキップします!
実際、約11ヶ月前、この問題が部分的に発見され、バグが提出されました.この問題を修正しpython-devで説明しました.
これまでPython 3.10で修正されるようですが、3.8と3.9のドキュメントに**=バグに関する通知を追加する必要があります(この問題は以前からあったかもしれませんが、古いPythonバージョンはセキュリティ・メンテナンス・モードのみなので、ドキュメントは変更されません).
修復されたコードは、意味的な変化であり、問題のある意味に意図的に依存しているかどうかを判断するのが難しいため、移植されない可能性が高い.しかし、この問題に気づくのに長い時間がかかった.これは**=の使用が広くないことを示している.そうしないと、問題はとっくに発見されている.