Pythonの二元算術演算を詳しく解くと、なぜ減算法は文法糖にすぎないのか.


原題|Unravelling binary arithmetic operations in Python
作者|Brett Cannon
訳者|エンドウの花の下の猫(「Python猫」公衆号作者)
声明|本翻訳は交流学習の目的で、CC BY-NC-SA 4.0授権協定に基づいている.読みやすいように、内容が少し変更されました.
皆さんは私の属性アクセスのブログを解読した文章に熱烈に反応して、Pythonがどれだけ文法が実際に文法糖にすぎないかについての文章を書くことを啓発しました.本文では、二元算術演算についてお話ししたいと思います.
具体的には、減算の動作原理a - bを解読したいと思います.スワップはできないので、わざと減算を選びました.これは操作順序の重要性を強調することができ、加算操作に比べて、実現時にaとbを誤って反転する可能性がありますが、同じ結果が得られます.
Cコードの表示
慣例に従って、CPython解釈器がコンパイルしたバイトコードを表示することから始めます.
>>> def sub(): a - b
... 
>>> import dis
>>> dis.dis(sub)
  1           0 LOAD_GLOBAL              0 (a)
              2 LOAD_GLOBAL              1 (b)
              4 BINARY_SUBTRACT
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

BINARY_SUBTRACTのオペレーティングコードを深く研究する必要があるようです.Python/ceval.cファイルは、この操作コードを実現するCコードが以下のように見える.
case TARGET(BINARY_SUBTRACT): {
    PyObject *right = POP();
    PyObject *left = TOP();
    PyObject *diff = PyNumber_Subtract(left, right);
    Py_DECREF(right);
    Py_DECREF(left);
    SET_TOP(diff);
    if (diff == NULL)
    goto error;
    DISPATCH();
}

ソース:https://github.com/python/cpy...
ここでのキーコードはPyNumber_Subtract()であり,減算の実際の意味を実現している.関数の一部のマクロを引き続き表示すると、binary_op1()関数が表示されます.二元操作を管理する一般的な方法を提供します.
しかし、実装の参考にするのではなく、Pythonのデータモデルを使用します.公式ドキュメントはよく、減算に使用される意味を明確に紹介しています.
データモデルから学ぶ
データモデルのドキュメントを読むと、減算を実現する際に、__sub____rsub__の2つの方法が重要な役割を果たしていることがわかります.
1、__sub__()メソッドa - bが実行されると、aのタイプで__が検索される.sub__()、そしてbをそのパラメータとする.これは私が書いた属性アクセスの記事のようなものです.getattribute__()は、特殊/マジックメソッドは、オブジェクトのタイプに応じて解析され、性能目的のためにオブジェクト自体を解析するものではない.次のサンプルコードでは、_を使用します.mro_getattr()は、このプロシージャを表します.
したがって、定義されている場合sub__()、ならtype(a)._sub__(a,b)は減算操作に用いられる.(注:マジックメソッドはオブジェクトのタイプに属し、オブジェクトに属しません)
これは本質的に、減算はメソッド呼び出しにすぎないことを意味します!標準ライブラリのoperator.sub()関数として理解することもできます.
この関数を模倣して独自のモデルを実装し,lhsとrhsの2つの名前でa−bの左側と右側をそれぞれ表し,サンプルコードをより理解しやすくする.
#     __sub__()     
def sub(lhs: Any, rhs: Any, /) -> Any:
    """Implement the binary operation `a - b`."""
    lhs_type = type(lhs)
    try:
        subtract = _mro_getattr(lhs_type, "__sub__")
    except AttributeError:
        msg = f"unsupported operand type(s) for -: {lhs_type!r} and {type(rhs)!r}"
        raise TypeError(msg)
    else:
        return subtract(lhs, rhs)

2、右側に使用させる_rsub__()
しかし、aが実現しなかったら_sub__()どうしましょうか.aとbが異なるタイプであれば、bの__を呼び出してみます.rsub__()(_rsub__の中の「r」は「右」を表し、オペレータの右側を表す).
操作の両方が異なるタイプである場合、式を有効にしようとする機会があることを確認できます.それらが同時になると、私たちは_sub__()うまく処理できます.ただし、両方のインプリメンテーションが同じであっても、__を呼び出す必要があります.rsub__()は、1つのオブジェクトが他の(サブ)クラスでないようにします.
3、無関心タイプ
これで、式の両方が演算に参加できます.ただし、何らかの理由でオブジェクトのタイプが減算をサポートしていない場合はどうしますか(たとえば4-「stuff」はサポートされていません).この場合、_sub__ または_rsub__ できることはNotImplementedに戻ることです.
これはPythonに返される信号で、コードを正常に動作させるために次の操作を継続する必要があります.我々のコードでは、メソッドの戻り値を確認してから、それが機能すると仮定する必要があることを意味します.
#      ,                 
_MISSING = object()

def sub(lhs: Any, rhs: Any, /) -> Any:
        # lhs.__sub__
        lhs_type = type(lhs)
        try:
            lhs_method = debuiltins._mro_getattr(lhs_type, "__sub__")
        except AttributeError:
            lhs_method = _MISSING

        # lhs.__rsub__ (for knowing if rhs.__rub__ should be called first)
        try:
            lhs_rmethod = debuiltins._mro_getattr(lhs_type, "__rsub__")
        except AttributeError:
            lhs_rmethod = _MISSING

        # rhs.__rsub__
        rhs_type = type(rhs)
        try:
            rhs_method = debuiltins._mro_getattr(rhs_type, "__rsub__")
        except AttributeError:
            rhs_method = _MISSING

        call_lhs = lhs, lhs_method, rhs
        call_rhs = rhs, rhs_method, lhs

        if lhs_type is not rhs_type:
            calls = call_lhs, call_rhs
        else:
            calls = (call_lhs,)

        for first_obj, meth, second_obj in calls:
            if meth is _MISSING:
                continue
            value = meth(first_obj, second_obj)
            if value is not NotImplemented:
                return value
        else:
            raise TypeError(
                f"unsupported operand type(s) for -: {lhs_type!r} and {rhs_type!r}"
            )

4、子は親より優先
__rsub__()のドキュメントを見ると、コメントに気づきます.減算式の右側が左側のサブクラス(真のサブクラス、同じクラスは計算されません)であり、2つのオブジェクトの_rsub__()メソッドが異なる場合は、__を呼び出します.sub__()前に呼び出されます_rsub__().すなわち,bがaのサブクラスであると呼び出しの順序が逆転する.
これは奇妙な特例のようだが、その背後には原因がある.サブクラスを作成すると、親が提供する操作に新しい論理を注入することを意味します.この論理は必ずしも親に加える必要はありません.そうしないと、親は子に対して操作するときに、子が実現したい操作を上書きしやすくなります.
具体的には、Spam()-Spam()を実行すると、LessSpamのインスタンスが得られるSpamというクラスがあるとします.次に、Spamのサブクラス名をBaconとして作成しました.このように、SpamでBaconを減らすと、VeggieSpamが得られます.
上記のルールがなければ、Spam()-Bacon()はLessSpamを得る.SpamはBaconを減らすにはVeggieSpamを出すべきだとは知らないからだ.
しかし、上記のルールがあれば、Bacon._rsub__()は、まず式で呼び出されます(Bacon()-Spam()が計算されている場合は、Bacon._が最初に呼び出されるため、正しい結果が得られます.sub__()なので、issubclass()で判断されたサブクラスだけでなく、2つのクラスの異なる方法に違いがあるとルールで言います.
# Python        
_MISSING = object()

def sub(lhs: Any, rhs: Any, /) -> Any:
        # lhs.__sub__
        lhs_type = type(lhs)
        try:
            lhs_method = debuiltins._mro_getattr(lhs_type, "__sub__")
        except AttributeError:
            lhs_method = _MISSING

        # lhs.__rsub__ (for knowing if rhs.__rub__ should be called first)
        try:
            lhs_rmethod = debuiltins._mro_getattr(lhs_type, "__rsub__")
        except AttributeError:
            lhs_rmethod = _MISSING

        # rhs.__rsub__
        rhs_type = type(rhs)
        try:
            rhs_method = debuiltins._mro_getattr(rhs_type, "__rsub__")
        except AttributeError:
            rhs_method = _MISSING

        call_lhs = lhs, lhs_method, rhs
        call_rhs = rhs, rhs_method, lhs

        if (
            rhs_type is not _MISSING  # Do we care?
            and rhs_type is not lhs_type  # Could RHS be a subclass?
            and issubclass(rhs_type, lhs_type)  # RHS is a subclass!
            and lhs_rmethod is not rhs_method  # Is __r*__ actually different?
        ):
            calls = call_rhs, call_lhs
        elif lhs_type is not rhs_type:
            calls = call_lhs, call_rhs
        else:
            calls = (call_lhs,)

        for first_obj, meth, second_obj in calls:
            if meth is _MISSING:
                continue
            value = meth(first_obj, second_obj)
            if value is not NotImplemented:
                return value
        else:
            raise TypeError(
                f"unsupported operand type(s) for -: {lhs_type!r} and {rhs_type!r}"
            )

他の二元演算に拡張
減算を解決したら、他の二元演算はどうなるのでしょうか.はい、事実はそれらの操作が同じであることを証明していますが、たまたま異なる特殊/マジックメソッド名を使用しています.
したがって、この方法を普及させることができれば、+、-、*、@、/、//、%、**、<>、&、^、および|の13の操作の意味を実現することができます.
オブジェクトの自省における閉パッケージとPythonの柔軟性によりoperator関数の作成を抽出できます.
#          ,          
_MISSING = object()


def _create_binary_op(name: str, operator: str) -> Any:
    """Create a binary operation function.

    The `name` parameter specifies the name of the special method used for the
    binary operation (e.g. `sub` for `__sub__`). The `operator` name is the
    token representing the binary operation (e.g. `-` for subtraction).

    """

    lhs_method_name = f"__{name}__"

    def binary_op(lhs: Any, rhs: Any, /) -> Any:
        """A closure implementing a binary operation in Python."""
        rhs_method_name = f"__r{name}__"

        # lhs.__*__
        lhs_type = type(lhs)
        try:
            lhs_method = debuiltins._mro_getattr(lhs_type, lhs_method_name)
        except AttributeError:
            lhs_method = _MISSING

        # lhs.__r*__ (for knowing if rhs.__r*__ should be called first)
        try:
            lhs_rmethod = debuiltins._mro_getattr(lhs_type, rhs_method_name)
        except AttributeError:
            lhs_rmethod = _MISSING

        # rhs.__r*__
        rhs_type = type(rhs)
        try:
            rhs_method = debuiltins._mro_getattr(rhs_type, rhs_method_name)
        except AttributeError:
            rhs_method = _MISSING

        call_lhs = lhs, lhs_method, rhs
        call_rhs = rhs, rhs_method, lhs

        if (
            rhs_type is not _MISSING  # Do we care?
            and rhs_type is not lhs_type  # Could RHS be a subclass?
            and issubclass(rhs_type, lhs_type)  # RHS is a subclass!
            and lhs_rmethod is not rhs_method  # Is __r*__ actually different?
        ):
            calls = call_rhs, call_lhs
        elif lhs_type is not rhs_type:
            calls = call_lhs, call_rhs
        else:
            calls = (call_lhs,)

        for first_obj, meth, second_obj in calls:
            if meth is _MISSING:
                continue
            value = meth(first_obj, second_obj)
            if value is not NotImplemented:
                return value
        else:
            exc = TypeError(
                f"unsupported operand type(s) for {operator}: {lhs_type!r} and {rhs_type!r}"
            )
            exc._binary_op = operator
            raise exc

このコードがあれば、減算を_と定義できます.create_binary_op(「sub」,「-」)は、必要に応じて他の演算を繰り返し定義します.
詳細
このブログの「構文糖」タブを通じて、Pythonの文法をもっと詳しく理解する文章を見つけることができます.ソースコードはhttps://github.com/brettcannon/desugarで見つけることができます.
しゅうせい
  • 2020-08-19:修復当_rsub__()比_sub__()先呼び出し時のルール.
  • 2020-08-22:タイプが同じ場合に呼び出されないことを修正しました_rsub__ の質問移行コードも簡素化され、先頭と末尾のコードだけが保持され、簡単になりました.
  • 2020-08-23:多くの例でコンテンツが追加されています.