Python バージョンによってtupleのis演算子の振る舞いが変わる


バージョンによってtupleのis演算子の振る舞いが変わります。

【確認に使用したコード】

[code]

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

【実行結果】  
Python 2.7.17の結果(x is yがFalse)

[code]

    140379358955392
    140379359012656
    False
    
[/code]

Python 3.6.9の結果(Python 2.7.17と同じくx is yがFalse)

[code]

    139911075819024
    139911075406400
    False
    
[/code]

Python 3.7.5の結果(x is yがTrue)

[code]

    140641147669392
    140641147669392
    True
    
[/code]

### 質問1

振る舞いが変わるのはどのバージョンからでしょうか？  
バージョンの異なるPythonで試してみた結果、3.6.9より新しく、3.7.5以下のバージョンで振る舞いが変わったようです。  
同様の問題がないかネットで調べてみたのですが、見つかりませんでした。

### 質問2

他にもこのような振る舞いの違いはありますか？  
基本的な演算で、他にもこのような違いがあるか心配になりました。  
報告されているもので影響が大きそうなものがあれば教えて欲しいです。




※ 以下は質問に対する直接の回答ではありません。

Python 3.8.6 でも同様の結果になりますので、Python 3.8.6 で話を進めます。  
まず最初に `is` と `id()` に関して少し説明します。

  * [is](https://github.com/python/cpython/blob/3.8/Python/ceval.c#L5070)

[code]

    static PyObject *
    cmp_outcome(PyThreadState *tstate, int op, PyObject *v, PyObject *w)
    {
      int res = 0;
      switch (op) {
      case PyCmp_IS:
        res = (v == w);
        break;
          :
    
[/code]

オブジェクト(クラス、インスタンス、メソッド、関数、etc.)のメモリアドレス(ポインタ値)を比較しています。

  * [id()](https://github.com/python/cpython/blob/3.8/Python/bltinmodule.c#L1170)

[code]

    static PyObject *
    builtin_id(PyModuleDef *self, PyObject *v)
    /*[clinic end generated code: output=0aa640785f697f65 input=5a534136419631f4]*/
    {
        PyObject *id = PyLong_FromVoidPtr(v);
                     :
    
[/code]

オブジェクトのメモリアドレスを Python の整数型オブジェクト(`PyLongObject`)に変換しています。

次に質問欄にあるコードですが、dis モジュールを使って disassemble を行ってみます。

[code]

    $ python3 --version
    Python 3.8.6
    $ python3 -m dis tuple_equivalency.py
      1           0 LOAD_CONST               0 ((1, 2, 2))
                  2 STORE_NAME               0 (x)
                       :
    
      3          16 LOAD_CONST               0 ((1, 2, 2))
                 18 STORE_NAME               3 (y)
                       :
    
[/code]

`LOAD_CONST`(opcode) の引数がどちらも `0` になっています。この値は `func_code.co_consts`
のインデックスですが、同じという事はその値(この場合は `func_code.co_consts[0]` == tuple
インスタンスのメモリアドレス)が同一であるという事になります。

それでは `x = (1, 2, 4)` に変更するとどうなるのでしょうか。以下の様に、`LOAD_CONST` の引数が異なっています(つまり別個の
tuple インスタンス)。

[code]

      1           0 LOAD_CONST               0 ((1, 2, 4))
                  2 STORE_NAME               0 (x)
                       :
    
      3          16 LOAD_CONST               1 ((1, 2, 2))
                 18 STORE_NAME               3 (y)
                       :
    
[/code]

tuple は immutable なインスタンスですが、mutable なインスタンス(たとえば list 型)に変更して(`x = [1, 2,
3]`, `y = [1, 2, 3]`) disassemble してみます。

[code]

      1           0 LOAD_CONST               0 (1)
                  2 LOAD_CONST               1 (2)
                  4 LOAD_CONST               2 (3)
                  6 BUILD_LIST               3
                  8 STORE_NAME               0 (x)
                       :
    
      3          22 LOAD_CONST               0 (1)
                 24 LOAD_CONST               1 (2)
                 26 LOAD_CONST               2 (3)
                 28 BUILD_LIST               3
                 30 STORE_NAME               3 (y)
                       :
    
[/code]

`BUILD_LIST` によって新たな list 型インスタンスが作成されますので、list としては別々になりますが、その要素(`1, 2, 3`)は
`func_code.co_consts` の同じ要素を指しています。つまり、`x is y` は `False` になりますが、`x[0] is
y[0]` は `True` になります(`x[1]`, `x[2]` も同様)。

### 余談

Tuple インスタンスを memory allocate
している箇所は[PyTuple_New()](https://github.com/python/cpython/blob/3.8/Objects/tupleobject.c#L80)になります。

`ifdef` が入っていますが、`PyTuple_MAXSAVESIZE`
は[以下の様に定義されています](https://github.com/python/cpython/blob/3.8/Objects/tupleobject.c#L18)。

[code]

    /* Speed optimization to avoid frequent malloc/free of small tuples */
    #ifndef PyTuple_MAXSAVESIZE
    #define PyTuple_MAXSAVESIZE     20  /* Largest tuple to save on free list */
    #endif
    #ifndef PyTuple_MAXFREELIST
    #define PyTuple_MAXFREELIST   2000  /* Maximum number of tuples of each size to save */
    #endif
    
    #if PyTuple_MAXSAVESIZE > 0
    /* Entries 1 up to PyTuple_MAXSAVESIZE are free lists, entry 0 is the empty
       tuple () of which at most one instance will be allocated.
    */
    static PyTupleObject *free_list[PyTuple_MAXSAVESIZE];
    static int numfree[PyTuple_MAXSAVESIZE];
    #endif
    
[/code]

ここで、コメントには "... to save on **free list** " とか "... **malloc/free** of small
tuples" と書かれています。`PyTuple_New()` は以下の様になっていて、`free_list`(unbind された tuple
インスタンス の link list)を使って tuple インスタンスを再利用しています。

[code]

      if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL) {
          free_list[size] = (PyTupleObject *) op->ob_item[0];
          numfree[size]--;
                       :
          _Py_NewReference((PyObject *)op);
      }
      else
    #endif
      {
                       :
          op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
          if (op == NULL)
            return NULL;
      }
    
[/code]

Tuple インスタンスが再利用される例としては以下になります。

[code]

    $ python3
    Python 3.8.6 (default, Sep 25 2020, 09:36:53) 
    [GCC 10.2.0] on linux
    >>> x = (1, 2, 3)
    >>> x_id = id(x)
    >>> x_id
    139730736978368
    >>> del x
    >>> y = (-1, -2, -3)
    >>> id(y)
    139730736978368
    >>> id(y) == x_id
    True
    
[/code]

