41090


python 3.6.4で以下のコードを作成しました。

[code]

    class Number(object):
        list = []
        def extend(self,arg):
            self.list.extend(arg.list)
    
    def multi(arg):
        cls = Number()
        tmp = []
    
        file = open('data.txt', 'r')
        for line in file:
            tmp.append(int(line))
        file.close
        print(tmp)
    
        file = open('data.txt', 'w')
        file.write('')
        file.close
    
        for item in tmp:
            cls.extend(multi(item))
    
        cls.list.append(arg)
        print ('arg:',arg,' cls.list:',cls.list," id of cls:",id(cls))
        return cls
    
    def main():
        ans = multi(5)
        print('ans.list:',ans.list)
    
    if __name__ == '__main__':
        main()
    
[/code]

そして、data.txtには以下のデータを保存しておきます。

[code]

    5
    4
    3
    2
    1
    
[/code]

このコードへ期待する動作は、  
ans.listが[5,4,3,2,1]となっていることでした。  
しかし、実際は再帰呼び出しされたclsのidがすべて同じため、  
ans.listは期待とは違った値となっています。  
なぜclsのidは再帰呼び出しされても同じidのままなのでしょうか。

例えば以下のコードでは、再帰呼び出しされたクラスのidはすべて異なっており、  
期待通りの動作をします。

[code]

    class Number(object):
        num = 5
    
    def multi(arg):
        num_cls = Number()
        if arg > 1:
            num_cls.num = arg * multi(arg - 1)
        else:
            num_cls.num = 1
        print ('arg:',arg,' num:',num_cls.num," id of num_cls:",id(num_cls))
        return num_cls.num
    
    def main():
        ans = multi(5)
        print (ans)
    
    if __name__ == '__main__':
        main()
    
[/code]

pythonのどの仕様がそうさせているのか、お伺いできればと思います。


CC BY-SA 3.0
2018-01-19T09:23:17.927
41088
2018-01-19T19:35:11.247
27065
question
python python3
関数を再帰呼び出しした時の、関数内のクラスのidが同じことについて


Pythonのidはその変数のライフタイムの間でしかユニークであることを保障されず、今回の場合は初めに呼ばれるmulti(5)以外のmulti(5),
multi(4), multi(3),
...がそれぞれ再帰的な呼び出され方ではなく別々のスコープの範囲内となるため、idが再利用されているのではないかと思います。

<https://docs.python.org/2/library/functions.html#id>

このコードがおかしな挙動を見せているのは、fileがcloseされていないのと、list変数がNumberクラスの静的変数になってしまっている点によるのではないでしょうか。  
該当部分を書き換えたものを以下にあげます。

ただし、この書き方でもファイルから読んだ値以外で、初めの引数であるargの5がリストに追加されるので、ans.listの最終的な出力は[5,4,3,2,1,5]となります。

[code]

    class Number(object):
        def __init__(self):
            self.list = []
    
        def extend(self, arg):
            self.list.extend(arg.list)
    
    
    def multi(arg):
        cls = Number()
        tmp = []
    
        file = open('data.txt', 'r')
        for line in file:
            tmp.append(int(line))
        file.close()
        print(tmp)
    
        file = open('data.txt', 'w')
        file.write('')
        file.close()
    
        file = open('data.txt', 'r')
        for line in file:
            print(line)
        file.close()
    
        for item in tmp:
            cls.extend(multi(item))
    
        cls.list.append(arg)
        print('arg:', arg, ' cls.list:', cls.list,
              " id of cls:", id(cls))
        return cls
    
    
    def main():
        ans = multi(5)
        print('ans.list:', ans.list)
    
    
    if __name__ == '__main__':
        main()
    
[/code]


CC BY-SA 3.0
2018-01-19T10:22:27.963
41090
2018-01-19T10:22:27.963
27067
41088
answer


multiが初回に呼ばれた時、同一のファイルに読み書きしたあとは空のファイルになるのだから、2回目以降にmultiが呼び出された時tmpは空です。ですから次の部分は実行されていないことはお気付きですか？

[code]

    for item in tmp:
        cls.extend(multi(item))
    
[/code]

2回目以降のmultiの呼び出しでは再帰は起こらないので、最大でも深度1の再帰しか起きていません。オリジナルのコードを実行すると5回同じIDが表示されたあと、最後に1回別のIDが表示されるはずです。つまり再帰した時IDはちゃんと異なっています。

ではなぜ最初の5回は同じIDかというと、`cls = Number()`で作られたインスタンスは`return
cls`で関数を抜けたあと消滅します。作っては消えてを繰り返しているわけです。おそらくメモリの再利用がされていて、たまたま同一のidが割り当てられたわけです。新規のメモリの確保はコストがかかるので。

次のようにしてclsのリファレンスを維持するようにすればidは変わります。

[code]

    g = []
    def multi(arg):
        # 中略
        g.append(cls)
        return cls
    
[/code]

もっと簡単な例では次のようにすればidの再利用の現象が確認できます。Test1は同一スコープにあるときは変数名の再拘束が完了して元のオブジェクトへのリファレンスが消えてからidが再利用されていることがわかります。Test2では明示的にリファレンスを消しています。

[code]

    class Number(object):
        pass
    
    print("Test 1")
    cls = Number()
    print(id(cls))
    cls = Number()
    print(id(cls))
    cls = Number()
    print(id(cls))
    cls = Number()
    print(id(cls))
    
    print("Test 2")
    cls = Number()
    print(id(cls))
    del cls
    cls = Number()
    print(id(cls))
    del cls
    cls = Number()
    print(id(cls))
    del cls
    cls = Number()
    print(id(cls))
    
[/code]

出力

[code]

    Test 1
    4434900112
    4435036176
    4434900112
    4435036176
    Test 2
    4434900112
    4434900112
    4434900112
    4434900112
    
[/code]

delの代わりに`cls = None`としても同じです。リファレンスカウンタを強制的に0にして、ガベージコレクタにメモリの回収を促します。

### 蛇足

id()はインタープリタの実装依存なのであまり深読みしないほうがいいでしょう。  
またクラス変数でリストを初期化するのはバグの原因になりますので、やめた方がいいです。`def
func(x=[])`が危険なのと同じです。メモイズのように意図してやる場合もありますが、他人が読んだ時まずわかりません。

[code]

    class Number(object):
        l = []
        i = 123
    
    def main():
        n = Number()
        print("before", n.l, n.i)
        n.l.append('hello')
        n.i = 456
        print("after", n.l, n.i)
    
    main()
    main()
    
[/code]

実行結果

[code]

    before [] 123
    after ['hello'] 456
    before ['hello'] 123
    after ['hello', 'hello'] 456
    
[/code]


CC BY-SA 3.0
2018-01-19T18:36:28.253
41099
2018-01-19T19:35:11.247
2018-01-19T19:35:11.247
7837
7837
41088
answer