Help us understand the problem. What is going on with this article?

a = Class() をループしても独立したObject IDを持ったオブジェクトが生成される方法について

More than 1 year has passed since last update.

きっかけ

Java言語で学ぶデザインパターン入門 (著作:結城浩) で MementoのコードをJava → Python に移行を試みた時に発見してしまいました。どうやらループ内で単純にインスタンスを生成すると、ガーページコレクションが上手に働いていない可能性があります。コード例を出して、解決策を見つけようと思います。

問題のコード

       プレイヤーが10枚のカードを引いて合計scoreを出す
といったとても簡単なゲーム設定で例題を出します。

cardgame.py
import time
import random
import gc
import threading

class Toy:

    _name = ""
    _scoreList = []

    def __init__(self, name):
        self._name = name

    def addScore(self, item):
        self._scoreList.append(item)

    def totalScore(self):
        return sum(self._scoreList)

    def __str__(self):
        dst = "[name="
        dst += self._name + ","
        dst += "scores=" + str(self._scoreList) + ","
        dst += "id=" + str(id(self))
        dst += "]"
        return dst


if __name__ == '__main__':

    for i in range(10):

        #player を 生成
        t = Toy("ion")

        #10枚分のカードを取得
        for j in range(10):
            score = random.randrange(1,10)
            t.addScore(score)

        #出力
        print("[sum="+ str(t.totalScore()) +"]")
        print(t)

        time.sleep(1)

で、結果はどうなるかと言いますと、

[sum=63]
[name=ion,scores=[7, 3, 2, 8, 4, 9, 9, 9, 3, 9],id=4406957112]
[sum=113]
[name=ion,scores=[7, 3, 2, 8, 4, 9, 9, 9, 3, 9, 8, 5, 9, 5, 1, 6, 3, 1, 7, 5],id=4407047112]
[sum=162]
[name=ion,scores=[7, 3, 2, 8, 4, 9, 9, 9, 3, 9, 8, 5, 9, 5, 1, 6, 3, 1, 7, 5, 1, 7, 5, 4, 1, 8, 8, 5, 9, 1],id=4406957112]
[sum=203]
[name=ion,scores=[7, 3, 2, 8, 4, 9, 9, 9, 3, 9, 8, 5, 9, 5, 1, 6, 3, 1, 7, 5, 1, 7, 5, 4, 1, 8, 8, 5, 9, 1, 5, 7, 1, 1, 1, 7, 2, 6, 3, 8],id=4407047112]

Toy() でインスタンスを新しく生成しているつもりで書いているのに前のループで定義した t = Toy("ion") がまだ残っています。そのため、古いインスタンスが残ったままカードが追加されているので合計スコアも累積した値になってしまいます。:weary:

もしC++だったら...

sample.cpp
#include <iostream>
#include <vector>
#include <sstream>
#include <chrono>
#include <thread>
#include <random>
using namespace std;

class Toy
{

public:
    std::string name;
    std::vector<int> score;

    Toy();
    Toy(std::string name);

    std::string getName(){
        return name;
    }

    void addScore(int item){
        score.push_back(item);
    }

    std::string getListString(){

        std::string dst = "[";
        for(int i = 0; i < score.size() ;i++){
            std::ostringstream oss;
            if (i != 0){
                dst += ",";
            }
            oss << score[i];
            dst += oss.str();
        }
        dst += "]";

        return dst;
    }
};


Toy::Toy(){
    name = "na-na-shi";
}

Toy::Toy(std::string n){
   name = n;
}

/** 
 ランダム値を生成
  import random
  random.randRange(1,10)
 と同じ 
**/
int randomRange(int from, int to){
    std::random_device rd;
    std::mt19937 mt(rd());
    std::uniform_int_distribution<int> dist(from,to);
    return dist(mt);
}

/** 
 スリープ時間
  import time
  time.sleep(1)
 と同じ
**/
void sleepTime(int second){
    std::this_thread::sleep_for(std::chrono::seconds(second));
}

int main(void){

    for (int i = 0;i < 10; i++){
        Toy *t;
        t = new Toy("ion");
        for (int j = 0;j < 10; j++){
            t->addScore(randomRange(1,10));
            sleepTime(1);
        }
        std::cout << t->getName() << std::endl;
        std::cout << t->getListString() << std::endl;
        delete t;
    }

    return 0;
}

結果

ion
[5,9,6,8,1,8,6,4,5,4]
ion
[9,6,6,3,3,4,9,7,5,5]
ion
[2,6,7,6,7,1,6,10,4,1]
ion
[6,6,9,2,10,1,5,10,4,2]
ion
[3,9,1,6,6,4,4,4,9,10]
ion
[3,4,9,3,5,10,2,8,10,2]
ion
[6,5,5,3,4,9,8,8,1,2]
ion
[3,10,6,4,2,10,8,9,9,5]
ion
[1,3,1,5,10,5,9,7,6,8]
ion
[7,9,1,5,8,3,9,3,3,4]

といった具合に、インスタンスが生成された時に古いインスタンスが破棄されます。
こちらが、実装したい結果ですが、解決方法はないかといくつか考えてみました。

解決策1 del

案1 del を使う

for i in range(10):

        #player を 生成
        t = Toy("ion")

        #10枚分のカードを取得
        for j in range(10):
            score = random.randrange(1,10)
            t.addScore(score)

        #出力
        print("[sum="+ str(t.totalScore()) +"]")
        print(t)

        del t        
        time.sleep(1)

オブジェクトidを見ると結果は del で破棄しないのと同様に変数が使い回しをされてしまいます。失敗。

[sum=52]
[name=ion,scores=[1, 8, 9, 8, 9, 6, 1, 4, 1, 5],id=4341359672]
[sum=95]
[name=ion,scores=[1, 8, 9, 8, 9, 6, 1, 4, 1, 5, 8, 2, 8, 8, 1, 1, 3, 3, 1, 8],id=4341449672]
[sum=137]
[name=ion,scores=[1, 8, 9, 8, 9, 6, 1, 4, 1, 5, 8, 2, 8, 8, 1, 1, 3, 3, 1, 8, 1, 5, 1, 4, 6, 8, 4, 4, 5, 4],id=4341449672]
・・・

案2 weakref を使う

Python Document : なぜ list 'y' を変更すると list 'x' も変更されるのですか?には、
      変数とは、単にオブジェクトを参照するための名前に過ぎません。
とあります。

>>> a = []
>>> b = a
>>> b.append(1)
>>> a
[1]
>>> b
[1]
>>> 

fluent pythonでは変数は箱ではなく、付箋と考えるべきと書かれています。変数は箱という認識だとbの箱にaの箱に入るはずがないと思ったりします。documentなどの通り付箋で考えると a = b = [] が参照(ラベリング)していると考えるとb[]要素追加も出来るし、aでも要素の操作が出来ることとなり、[]abで実質的にシェアしている状況になっています。

で、delで a = [] の参照を削除するとbのみが残ります。

>>> del a
>>> b
[1]
>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined

で、b = []delすると[]がどの変数にも参照されることがないのでどこかのタイミングでガーベージコレクションされます。

となると、参照が解除されていることを明示的に示していればガーベージコレクションも自動的に働いて新しいインスタンスも生成されるのではないかと考えました。

で探してみると、
Pythonでweakref(弱参照)モジュールを使いこなすを参考にweakref --- 弱参照が使えそう。
変数を破棄すると参照しているかどうかを判定してくれるみたいです。
と思ったのでこんなコードを書いてみました。

import time
import random
import gc
import threading
import weakref

class Toy:

    _name = ""
    _scoreList = []

    def __init__(self, name):
        self._name = name

    def addScore(self, item):
        self._scoreList.append(item)

    def totalScore(self):
        return sum(self._scoreList)

    def __str__(self):
        dst = "[name="
        dst += self._name + ","
        dst += "scores=" + str(self._scoreList) + ","
        dst += "id=" + str(id(self))
        dst += "]"
        return dst

def isAliveMessage(boolean):
    message = "weak refarence is "
    if boolean:
        message += "alive"
    else :
        message += "dead"

    return message

if __name__ == '__main__':

    for i in range(10):

        #player を 生成
        t = Toy("ion")

        #10枚分のカードを取得
        for j in range(10):
            score = random.randrange(1,10)
            t.addScore(score)

        #出力
        print("[sum="+ str(t.totalScore()) +"]")
        print(t)

        wf = weakref.finalize(t, print, str(id(t)) + " is removed.")

        print(isAliveMessage(wf.alive))
        del t
        print(isAliveMessage(wf.alive))

        time.sleep(1)

ここでは、 class weakref.finalize(obj, func, *args, **kwargs) を使っています。
    obj がガベージコレクションで回収されるときに呼び出される、呼び出し可能なファイナライザオブジェクトを返します。
とあるので、オブジェクトが破棄(参照が解除された)時にcallされるみたいです。で、.aliveはオブジェクトが生きているかどうかを示していて、破棄されると.aliveFalseになります。

この状態で結果をみます。

[sum=55]
[name=ion,scores=[6, 2, 2, 2, 9, 6, 9, 2, 9, 8],id=4528084304]
weak refarence is alive
4528084304 is removed.
weak refarence is dead
[sum=107]
[name=ion,scores=[6, 2, 2, 2, 9, 6, 9, 2, 9, 8, 9, 2, 9, 9, 1, 7, 1, 5, 5, 4],id=4528084304]
weak refarence is alive
4528084304 is removed.
weak refarence is dead
[sum=162]
[name=ion,scores=[6, 2, 2, 2, 9, 6, 9, 2, 9, 8, 9, 2, 9, 9, 1, 7, 1, 5, 5, 4, 3, 8, 7, 9, 7, 3, 6, 8, 3, 1],id=4528174304]
weak refarence is alive
4528174304 is removed.
weak refarence is dead

参照は破棄されてますよーと明示的に示されるのですが、オブジェクトIDは古いものが付与されます。
参照が破棄された後にガーベージコレクションが起動しているはずなのですが、オブジェクトIDが固定化されるようです。失敗。

プログラミング FAQなぜ id() の結果は一意でないように見えるのですか?を見ると、
     オブジェクトがメモリから削除された後に、次に新しく生成されたオブジェクトはメモリの同じ場所にメモリ領域を確保されていることが、しばしば起きます。

と書いてあります。上記のことを試していると変数を参照するとメモリ領域が固定化されるようです。解決方法については以下のことも書いています。

     2 つの同じ値を持つ id は id() の実行の前に作られてすぐさま削除された異なる整数オブジェクトによるものです。id を調べたいオブジェクトがまだ生きてることを保証したいなら、オブジェクトへの別の参照を作ってください
どうやら別の変数を用意しないと異なるidを取得することは難しいみたいです。

案3 巨大なlist を使う

listにオブジェクトを追加した場合ですが、一意のオブジェクトIDが連番されます。

    itemList = []
    for j in range(10):
        itemList.append(Item())
    idList = [id(item) for item in itemList]
    print(idList)
[4409489224, 4409488832, 4414837872, 4414885504, 4414908456, 4414909072, 4414909184, 4414909016, 4414909128, 4414909352]

この特性を生かして変数に参照させます。

  • listにオブジェクトを大量に追加する
  • 変数に新しいオブジェクトを参照させる毎にlistも新規で作成する
    、、、とかなりゴリ押しなのですが、高確率で古いメモリ要域を利用しているので現段階ではオブジェクトIDが同一にならないように分散させるこの方法しかない?のかなと思います。

また、新しいインスタンスが呼び出された場合のlist作成ですが
1. listに要素を追加するときは変数で追加する
2. sleepで少し間隔を置いて、自動ガーベージコレクションを働かせる
3. ガーベージコレクションを起動させるためにimport gc gc.collect() を呼んでみる。
が必要です。

特に1ですが、

class Item:
    pass

if __name__ == '__main__':

    for i in range(10):
        itemList = []
        for j in range(10):
            itemList.append(Item())
        idList = [id(item) for item in itemList]
        print(idList)
        itemList.clear()
        del itemList

とすると、

[4451264328, 4451263936, 4456612976, 4456664704, 4456683560, 4456684176, 4456684288, 4456684120, 4456684232, 4456684456]
[4451264328, 4451263936, 4456612976, 4456664704, 4456683560, 4456684176, 4456684288, 4456684120, 4456684232, 4456684456]
[4451264328, 4451263936, 4456612976, 4456664704, 4456683560, 4456684176, 4456684288, 4456684120, 4456684232, 4456684456]
[4451264328, 4451263936, 4456612976, 4456664704, 4456683560, 4456684176, 4456684288, 4456684120, 4456684232, 4456684456]
[4451264328, 4451263936, 4456612976, 4456664704, 4456683560, 4456684176, 4456684288, 4456684120, 4456684232, 4456684456]
...

の具合に同一のリストが生成されます。

これを変数にして要素を渡すと、

class Item:
    pass

if __name__ == '__main__':
    for i in range(10):
        itemList = []
        for j in range(10):
            item = Item()
            itemList.append(item)
        idList = [id(item) for item in itemList]
        print(idList)
        itemList.clear()
        del itemList
[4300846920, 4300846528, 4306195568, 4306247296, 4306266152, 4306266768, 4306266880, 4306266712, 4306266824, 4306267048]
[4300846920, 4300846528, 4306195568, 4306247296, 4306267048, 4306266152, 4306266768, 4306266880, 4306266712, 4306266824]
[4300846920, 4300846528, 4306195568, 4306247296, 4306266824, 4306267048, 4306266152, 4306266768, 4306266880, 4306266712]
[4300846920, 4300846528, 4306195568, 4306247296, 4306266712, 4306266824, 4306267048, 4306266152, 4306266768, 4306266880]
[4300846920, 4300846528, 4306195568, 4306247296, 4306266880, 4306266712, 4306266824, 4306267048, 4306266152, 4306266768]
[4300846920, 4300846528, 4306195568, 4306247296, 4306266768, 4306266880, 4306266712, 4306266824, 4306267048, 4306266152]
[4300846920, 4300846528, 4306195568, 4306247296, 4306266152, 4306266768, 4306266880, 4306266712, 4306266824, 4306267048]
[4300846920, 4300846528, 4306195568, 4306247296, 4306267048, 4306266152, 4306266768, 4306266880, 4306266712, 4306266824]
[4300846920, 4300846528, 4306195568, 4306247296, 4306266824, 4306267048, 4306266152, 4306266768, 4306266880, 4306266712]
[4300846920, 4300846528, 4306195568, 4306247296, 4306266712, 4306266824, 4306267048, 4306266152, 4306266768, 4306266880]

少し改善されましたが、特に最初に追加した要素は同一のオブジェクトIDになっています。

そのため、sleep時間とガーベージコレクションを明示化します。

    for i in range(10):
        itemList = []
        for j in range(10):
            item = Item()
            itemList.append(item)
        idList = [id(item) for item in itemList]
        print(idList)
        itemList.clear()
        del itemList
        gc.collect(2)
        time.sleep(1)
[4539090312, 4539089696, 4544439408, 4544487040, 4544509992, 4544510608, 4544510720, 4544510552, 4544510664, 4544510888]
[4548224560, 4548223664, 4548223608, 4548223440, 4548223496, 4548223552, 4548223328, 4548223384, 4548223216, 4548223104]
[4548224560, 4548223104, 4548223664, 4548223608, 4548223440, 4548223496, 4548223552, 4548223328, 4548223384, 4548223216]
[4548224560, 4548223216, 4548223104, 4548223664, 4548223608, 4548223440, 4548223496, 4548223552, 4548223328, 4548223384]
[4548224560, 4548223384, 4548223216, 4548223104, 4548223664, 4548223608, 4548223440, 4548223496, 4548223552, 4548223328]
[4548224560, 4548223328, 4548223384, 4548223216, 4548223104, 4548223664, 4548223608, 4548223440, 4548223496, 4548223552]
[4548224560, 4548223552, 4548223328, 4548223384, 4548223216, 4548223104, 4548223664, 4548223608, 4548223440, 4548223496]
[4548224560, 4548223496, 4548223552, 4548223328, 4548223384, 4548223216, 4548223104, 4548223664, 4548223608, 4548223440]
[4548224560, 4548223440, 4548223496, 4548223552, 4548223328, 4548223384, 4548223216, 4548223104, 4548223664, 4548223608]
[4548224560, 4548223608, 4548223440, 4548223496, 4548223552, 4548223328, 4548223384, 4548223216, 4548223104, 4548223664]

最初の要素は同一ですが、その他はある程度異なったIDが追加されていると思います。あとは、list自体を大きくして100回使うのであれば10000個の要素を用意すればだいたい重複するオブジェクトIDは取得しないと思います。

cardgame.py
import time
import random
import gc
import threading

class Toy:

    _name = ""
    _scoreList = []

    def __init__(self, name=""):
        self._name = name

    def setName(self, name):
        self._name = name

    def addScore(self, item):
        self._scoreList.append(item)

    def totalScore(self):
        return sum(self._scoreList)

    def clearScore(self):
        self._scoreList.clear()

    def __str__(self):
        dst = "[name="
        dst += self._name + ","
        dst += "scores=" + str(self._scoreList) + ","
        dst += "id=" + str(id(self))
        dst += "]"
        return dst

    def __del__(self):
        self._name = ""
        self._scoreList.clear()

def independToy(scale=10000):

    memoryRange = []
    it = None
    for i in range(scale):
        t = Toy()
        memoryRange.append(t)

    index = random.randrange(0,scale-1)
    it = memoryRange[index]
    memoryRange.clear()
    del memoryRange
    gc.collect(2)
    time.sleep(1)
    return it

if __name__ == '__main__':

    for i in range(10):
        t = independToy()
        t.setName("ion")

        #10枚分のカードを取得
        for j in range(10):
            score = random.randrange(1,10)
            t.addScore(score)

        print("[sum="+ str(t.totalScore()) +"]")
        print(t)

        del t
        gc.collect()
        time.sleep(1)

[sum=57]
[name=ion,scores=[8, 7, 1, 5, 5, 5, 6, 7, 7, 6],id=4429487352]
[sum=48]
[name=ion,scores=[7, 2, 1, 5, 1, 6, 8, 3, 7, 8],id=4428954144]
[sum=53]
[name=ion,scores=[6, 3, 8, 7, 4, 4, 5, 5, 4, 7],id=4428221744]
[sum=55]
[name=ion,scores=[7, 7, 8, 3, 4, 3, 1, 8, 9, 5],id=4429192328]
[sum=50]
[name=ion,scores=[7, 5, 2, 2, 6, 8, 9, 2, 4, 5],id=4429745512]
[sum=58]
[name=ion,scores=[5, 7, 3, 4, 7, 9, 5, 8, 3, 7],id=4429288224]
[sum=64]
[name=ion,scores=[1, 7, 8, 6, 9, 6, 4, 9, 7, 7],id=4428072664]
[sum=48]
[name=ion,scores=[4, 4, 9, 4, 8, 4, 6, 7, 1, 1],id=4429227120]
[sum=55]
[name=ion,scores=[7, 8, 4, 9, 3, 6, 1, 9, 6, 2],id=4429249000]
[sum=55]
[name=ion,scores=[8, 5, 4, 7, 9, 1, 2, 5, 7, 7],id=4429348936]

idもかぶってないしなんか良さげです。

おわりに

???
一番最初のコードでクラスに __del__メソッドを加えたら同じ結果じゃないの?
と思った人、正解です。オブジェクトIDは同一ですが、オブジェクト破棄時にlistを空にすると爆発しません。:scream:

def __del__(self):
        self._name = ""
        self._scoreList.clear()

このオブジェクトIDは実体に付与されているわけではなくて、C/C++風にいうとアドレスに近いみたいです。

例題が悪かったみたいです:tired_face:

ここで、
- 上記の例で一意のObject IDを付与したい場面があるか?
- Object ID手動で設定したい場合はc/c++にて直接codinigか?
- 上記の書き方でクラス内fieldの値が保持されたままのは何故?

と色々と疑問が出てきました。
謎が深まりました。

知ってる人コメントください:sweat_smile:

→ To Be Continued

参考にした読み物

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away