72931


### 現象

スクリプトで実行したときと、スクリプトの行を1行づつインタラクティブシェルに入力したときで、is演算子、id()の振る舞いが異なります。

この現象はPython3.7.5(3.9.0でも現象は同じ)で確認しています。Python3.6.9以前ではid()の具体値は別にして振る舞いは同じでした。

【コード】

[code]

    x = 1, 2, 3
    print(id(x))
    y = 1, 2, 3
    print(id(y))
    print(x is y)
    
[/code]

【スクリプトの実行結果】

[code]

    139783069416656
    139783069416656
    True
    
[/code]

【インタラクティブシェルでの実行結果】

[code]

    >>> x = 1, 2, 3
    >>> print(id(x))
    140299191938736
    >>> y = 1, 2, 3
    >>> print(id(y))
    140299191824624
    >>> print(x is y)
    False
    
[/code]

### 質問

インタラクティブシェルでの実行とスクリプト実行でis演算子の振る舞いが異なる理由は何でしょうか？  
tupleオブジェクトのメモリ割り当て方法が異なることは実行結果のとおりですが、何が違っているために現象が変わるのでしょうか。


CC BY-SA 4.0
2020-12-27T02:39:19.727
72916
2020-12-27T20:01:56.963
35558
question
python python3
Pythonのインタラクティブシェルとスクリプト実行でis演算子、id()の振る舞いが異なる


ふへへ…… 10 時間連続で CPython のコード読み続けて概ね予想は付きましたぜ……。

まず、repl で一行ごとに実行される関数がこちらになります。  
[cpython/pythonrun.c at 32bd68c839adb7b42af12366ab0892303115d1d1 ·
python/cpython](https://github.com/python/cpython/blob/32bd68c839adb7b42af12366ab0892303115d1d1/Python/pythonrun.c#L147-L236)  
重要なのはこの部分です！

[code]

    ...
        arena = PyArena_New();
        if (arena == NULL) {
            Py_XDECREF(v);
            Py_XDECREF(w);
            Py_XDECREF(oenc);
            return -1;
        }
    
        mod = PyParser_ASTFromFileObject(fp, filename, enc, Py_single_input,
                                         ps1, ps2, flags, &errcode, arena);
    
        Py_XDECREF(v);
        Py_XDECREF(w);
        Py_XDECREF(oenc);
        if (mod == NULL) {
            PyArena_Free(arena);
            if (errcode == E_EOF) {
                PyErr_Clear();
                return E_EOF;
            }
            return -1;
        }
        m = PyImport_AddModuleObject(mod_name);
        if (m == NULL) {
            PyArena_Free(arena);
            return -1;
        }
        d = PyModule_GetDict(m);
        v = run_mod(mod, filename, d, d, flags, arena);
        PyArena_Free(arena);
    ...
    
[/code]

`PyParser_ASTFromFileObject` はファイルハンドラと様々な実行文脈（モジュール名とか。repl なら
`__main__`）を渡してモジュールオブジェクトとして返す関数です。repl で打ったコードもモジュールとして変換されるのです。そしてそれを
`run_mod` に実行文脈とともに渡して実際に実行させます。値が返ってくればそれを repl に表示します（`>>> 1 + 2` ってやると `3`
って表示されるアレです）。

ここで `arena` というものを渡していると思うんですがこれが肝です。これはコンパイル・実行ともに Python
コードが使用するメモリを管理する大事な構造体です。ここにメモリが確保され、その中にコンパイルされたオブジェクトが配置され、実行結果が溜められていく、そんなイメージです。コマンドラインから実行された
Python コードはこの `arena` を再帰的に受け継ぎながら実行していくのでメモリ空間を共有します。それに加え Python
には「型付きオブジェクトはキャッシュされる」という仕組みがあります。例えば整数や文字列、そしてタプルなんかがそうです。これらによりオブジェクト（変数）の
ID （というかポインタの値）が一致するわけです。

さてソースコードをもう一度よく見てもらうと、始まりに `arena = PyArena_New();`、終わりに
`PyArena_Free(arena);` ってやってますね？これは `arena` のメモリを repl
ではいちいち開放し直していることになります。つまりメモリ空間が毎回違うのです！一行一行メモリの割当が違うのでポインタの値が違うので ID
も違うというわけなんですね。ちなみに「毎回メモリ解放しとったら前の変数参照できひんやんけ」と思うかもしれませんが、そのへんは弱参照という仕組みでうまくやってるぽいです。知らんけど。これ以上は書籍「ガベージコレクション」とかを読まないとわかんないやーつです。わたしも積んでます。

ちなみにいつ挙動が変わったかですがごめんなさい！そこまでは追いきれませんでした。ただ多分 `is`
の正しい使い方を知っていたらこう答えるでしょう。「誰が気にすんの？」と。もし気になったら GitHub で考古学に勤しむのも良いかもしれません。


CC BY-SA 4.0
2020-12-27T20:01:56.963
72931
2020-12-27T20:01:56.963
39645
72916
answer