Pythonの「有効期間が重ならない2つのオブジェクトは同じid()値を持つかもしれません。」の一例

# はじめに
掲題は、Pythonドキュメントに記載されている
**id()**の説明の一部ですが
最初、見た時にピンと来ませんでした。

有効期間とは？
持つかもしれない、とは？

Pythonを動かしていて
たまたま、それっぽい動きを見つけたので
備忘もかねて記録しておきます。

# Pythonドキュメントの説明
まず日本語版です。

>
id(object)
オブジェクトの"識別値"を返します。
この値は整数で、このオブジェクトの有効期間中は
一意かつ定数であることが保証されています。
**有効期間**が重ならない2つのオブジェクトは同じid()値を持つかもしれません。
>

「有効期間」は英語版の該当する単語から類推できます。

>
Return the "identity" of an object. 
This is an integer which is guaranteed to be unique and constant
for this object during its lifetime.
Two objects with non-overlapping **lifetimes** may have the same id() value.
>

**lifetimes**とあるので
一般的な「**変数のライフサイクル**」と考えてよさそうです。

# たまたまやってた事
仮引数として**可変長引数**が定義されている自前の関数を呼びだし
呼び出された先でその引数のIDを調べていました。

# コード
こんな感じです。
func関数は可変長引数を受け取り
その中身であるタプルとそのIDをprintするだけです。

```python:test.py
def func(*args):
    # print id(args)
    print(f'{"id(args) ":-<20}' + "> " + repr(id(args)))
    # print args
    print(args)


if __name__ == '__main__':
    # aとbで呼び出し
    a, b = 0, 1
    func(a, b)
    print()

    # 5と6で呼び出し
    func(5, 6)
    print()

    # 文字列とリストで呼び出し
    func('abc', [[], [], []])
    print()

    # リストをアンパックして呼び出し
    c = [i for i in range(5)]
    func(*c)
    print()

    # リストをアンパックして呼び出し
    d = [i for i in 'abcde']
    func(*d)
    print()

    # 色々入っている集合をアンパックして呼び出し
    e = {2, 3, (1,), 1, 'hoge'}
    func(*e)
    print()

    # 色々入っているリストをアンパックして呼び出し
    f = [{}, [], {a: 1, b: 2}, 3, (1, 2)]
    func(*f)
    print()
```

# 結果
タプルのIDは**まちまち**になりました。

```console:結果
id(args) -----------> 18147208
(0, 1)

id(args) -----------> 18147208
(5, 6)

id(args) -----------> 18147208
('abc', [[], [], []])

id(args) -----------> 17979408
(0, 1, 2, 3, 4)

id(args) -----------> 17979408
('a', 'b', 'c', 'd', 'e')

id(args) -----------> 17979408
(1, 2, 3, (1,), 'hoge')

id(args) -----------> 17979408
({}, [], {0: 1, 1: 2}, 3, (1, 2))
```

# クラスの場合
クラスの場合は全部**同じ**になりました。

```python:test2.py
class Test:
    def func(self, *args):
        print(f'{"id(args) ":-<20}' + "> " + repr(id(args)))
        print(args)


if __name__ == '__main__':
    t = Test()
    t.func(1)
    print()

    t.func('aaa')
    print()

    t.func([i for i in 'aaaaa'])
    print()

    t.func([i for i in range(10)])
    print()

    t.func({})
    print()
```

```console:結果
id(args) -----------> 57210192
(1,)

id(args) -----------> 57210192
('aaa',)

id(args) -----------> 57210192
(['a', 'a', 'a', 'a', 'a'],)

id(args) -----------> 57210192
([0, 1, 2, 3, 4, 5, 6, 7, 8, 9],)

id(args) -----------> 57210192
({},)
```

# 試しに
渡す実引数を１つにしてIDを確認してみます。

```python:test3.py
def func(*args):
    print(f'{"*args ":-<20}' + "> " + repr(*args))
    print(f'{"id(*args) ":-<20}' + "> " + repr(id(*args)))
    print(f'{"args ":-<20}' + "> " + repr(args))
    print(f'{"id(args) ":-<20}' + "> " + repr(id(args)))


if __name__ == '__main__':
    a = 1
    print(f'{"a ":-<20}' + "> " + repr(a))
    print(f'{"id(a) ":-<20}' + "> " + repr(id(a)))
    print()
    func(a)
    print()

    b = 2
    print(f'{"b ":-<20}' + "> " + repr(b))
    print(f'{"id(b) ":-<20}' + "> " + repr(id(b)))
    print()
    func(b)
    print()

    c = 'hoge'
    print(f'{"c ":-<20}' + "> " + repr(c))
    print(f'{"id(c) ":-<20}' + "> " + repr(id(c)))
    print()
    func(c)
```

```console:結果
a ------------------> 1
id(a) --------------> 1745410224

*args --------------> 1
id(*args) ----------> 1745410224
args ---------------> (1,)
id(args) -----------> 61142352

b ------------------> 2
id(b) --------------> 1745410240

*args --------------> 2
id(*args) ----------> 1745410240
args ---------------> (2,)
id(args) -----------> 61142352

c ------------------> 'hoge'
id(c) --------------> 61993280

*args --------------> 'hoge'
id(*args) ----------> 61993280
args ---------------> ('hoge',)
id(args) -----------> 61142352
```

それぞれの実引数のIDはバラバラに出力されていますが
パックされたタプルのIDはやっぱり同じです。

# まとめ
funcにおけるargsタプルのライフサイクルは
func関数の呼び出しからfunc関数の処理が終わるまでという認識です。
それが**有効期間**という意味なのだと解釈しました。

また「持つかもしれません」については
上記、クラスのコード例では全てのIDが同じになりましたが
関数では**全てのIDが同じになるとも限りませんでした**。

何故、このような挙動になるのかはよくわかっていませんが
恐らくは
「**ライフサイクル満了時には、オブジェクトも破棄されるので**
　**破棄されたオブジェクトのIDが使いまわされている**」
と推測します。

もしくはどこかにIDが退避されているとか…？（考えにくいですが）
ともあれ、やや疑問は残りますが、こういうものだと思う事にします。

結論としては、もし「id()による識別値を使いたい」場合は
「**一意かつ定数であることが保証されています**」というメリットだけに囚われず
そのオブジェクトのスコープやライフサイクルなどから
「有効期間」を意識する事が大事だと思いました。