きっかけ
Java言語で学ぶデザインパターン入門 (著作:結城浩) で MementoのコードをJava → Python に移行を試みた時に発見してしまいました。どうやらループ内で単純にインスタンスを生成すると、ガーページコレクションが上手に働いていない可能性があります。コード例を出して、解決策を見つけようと思います。
問題のコード
プレイヤーが10枚のカードを引いて合計scoreを出す
といったとても簡単なゲーム設定で例題を出します。
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")
がまだ残っています。そのため、古いインスタンスが残ったままカードが追加されているので合計スコアも累積した値になってしまいます。
もしC++だったら...
#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
でも要素の操作が出来ることとなり、[]
がa
とb
で実質的にシェアしている状況になっています。
で、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
はオブジェクトが生きているかどうかを示していて、破棄されると.alive
がFalse
になります。
この状態で結果をみます。
[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作成ですが
- listに要素を追加するときは変数で追加する
- sleepで少し間隔を置いて、自動ガーベージコレクションを働かせる
- ガーベージコレクションを起動させるために
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は取得しないと思います。
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を空にすると爆発しません。
def __del__(self):
self._name = ""
self._scoreList.clear()
このオブジェクトIDは実体に付与されているわけではなくて、C/C++風にいうとアドレスに近いみたいです。
例題が悪かったみたいです
ここで、
- 上記の例で一意のObject IDを付与したい場面があるか?
- Object ID手動で設定したい場合はc/c++にて直接codinigか?
- 上記の書き方でクラス内fieldの値が保持されたままのは何故?
と色々と疑問が出てきました。
謎が深まりました。
知ってる人コメントください
→ To Be Continued |
---|
参考にした読み物
-
fluent python 第8章 オブジェクト参照、可変性、リサイクル pp.229-255
- weakrefには
WeakValueDictionary
、WeakKeyDictionary
クラスがあるみたい使いたいときに調べてみよう - 章末Soapboxに*"オブジェクトを直接削除するメカニズムはありません。そうした機能を無くしているのは実にすばらしいことです"*と書いているのですがメリットあるの?と思ってしまう
- weakrefには
- Pythonでweakref(弱参照)モジュールを使いこなす
- プログラミング FAQ
-
Python よくある質問
- 読んでて意外にオモロイ