【Python】ミューとイミューに首ったけ
今回は、Pythonの参照渡しと値渡しについて少しだけ知見を手にしたので書いておこうと思います。
別に、特別なことでもないですし
もしかしたら当たり前のことかも知れませんが、自分のメモとして。
0. はじめに
~~~ 環境 ~~~
Python 3.7 (Anaconda 2019.03)
~~~ ~~~
1. Pythonのクラスを作る
プログラムを書いているとクラスを作っていくことがあるかと思います。
この時に、クラス内部でのデータを配列として所持するようなことを考えます。
class ArrayData:
datas = []
まあ、こんな程度であればクラス化する必要性も感じませんがとりあえず。
次に、外部から値をこの"datas"に放り込めるように関数を作成します。
# 配列データを所持するクラス
class ArrayData:
# クラス内の配列
datas = []
# クラス内関数。
# 配列にデータを追加する。
# クラス内での関数はpublic関数になる模様
def add_data(self, data):
self.datas.append(data)
では、このクラスに値を突っ込んでみます。
class ArrayData:
datas = []
def add_data(self, data):
self.datas.append(data)
# 配列の中身をprintする
def print_datas(self):
# selfはこのクラス自身なので、自身の配列を呼び出す。
for in_data in self.datas:
# Python3.xからの書き方
# printの引数endに空文字を渡すことで、改行を消す。
print(in_data, end="")
sample_data = ArrayData()
sample_data.add_data("Hello")
sample_data.print_datas()
さて、このプログラムの実行結果はどうなるでしょうか?
"Hello"と表示されていれば正解です。
何を当たり前のことをと思うでしょう。
2. 少し混乱する挙動
この後が正直なんでそうなるのか個人的に全然分かりませんでした。
ArrayDataクラスを別の変数に突っ込んでそちらに値を新たに突っ込みます。
ここで事前に予想しましょう。
本来であれば、別の変数なので参照は別になって欲しいはずです。そして、誰もがそうなると思うのではないかと思います。
特に別の言語とかでオブジェクト指向を利用しているとそう思ってしまいます。
では、実際に動かしてみましょう。
class ArrayData:
datas = []
def add_data(self, data):
self.datas.append(data)
def print_datas(self):
for in_data in self.datas:
print(in_data, end="")
sample_data = ArrayData()
sample_data.add_data("Hello")
other_sample_data = ArrayData()
other_sample_data.add_data(", World.")
sample_data.print_datas()
print("")
さて、どうなったでしょうか?
見事に"Hello, World."と表示されたのではありませんか?
Pythonからプログラミングを始められた方であればなんら不思議ではないかもしれませんし
すでに、Pythonのミュータブルに慣れている方であればなんらおかしい挙動はしていないと言うことは分かっています。
先ほどした事前予想で”別の変数なのだから参照は別になるだろう”としました。
しかし、結果は”別の変数なのに参照は同じ”と言う結果が得られたのです。
これによって何が困るかと言うと
例えば上記other_sample_dataに”だけ”データを追加したい。と言うことが出来ません。
3. What Happened?
ヒントは”参照渡し”と”イミュータブル”です。
Pythonでは一度使用された変数の中身が一切変更されません。
これは変数の中身が壊れないようにするためのPython独特の文化です。
では、先のsample_dataとother_sample_dataも別物のはずですが
ここでPythonのミュータブルな値がよしなにしてくれています。
難しいことは一先ず置いておいて、実態を確かめてみましょう。
class ArrayData:
datas = []
def add_data(self, data):
self.datas.append(data)
def print_datas(self):
for in_data in self.datas:
print(in_data, end="")
# 自身のオブジェクトidを表示
def print_self_id(self):
print(id(self))
# 自身が持っている配列のオブジェクトidを表示
def print_datas_id(self):
print(id(self.datas))
sample_data = ArrayData()
sample_data.add_data("Hello")
other_sample_data = ArrayData()
other_sample_data.add_data(", World.")
sample_data.print_datas()
print("")
sample_data.print_self_id()
other_sample_data.print_self_id()
sample_data.print_datas_id()
other_sample_data.print_datas_id()
徐々にプログラムが長くなってきました。
もし、読みずらい場合は、各箇所でコメントを入れて分かりやすくしてみてください。
さて、結果を見てみましょう。
クラスオブジェクトID群
sample_data id -> 4497272672
other_sample_data id -> 4497272784
クラスオブジェクト内変数オブジェクトID群
sample_data.datas id -> 4496441160
other_sample_data.datas id -> 4496441160
IDは恐らく異なっているかとは思いますが、違うことが分かればOKです。
4. ミュータブルとイミュータブル
ここで注目して頂きたいのが
オブジェクトIDが同じものと違うものがあると言う点。
クラスのオブジェクトID群と変数オブジェクトID群で挙動に差があります。
ここが非常に面白い!
クラスは変数が別なので別のオブジェクトとして解釈されていますが
クラスオブジェクト内部の変数は、同じオブジェクトとして認識されています。
イミュータブルな値の中にあるミュータブルな値は、イミュータブルの側が変わっても同じ性質を持っている。と言うことです。
これ、すごく良くできている。
恐らく、イミュータブルの値をコピーする際は参照のコピーになっていて
中身が変わるまで以前の状態を保持し続ける。と言う規約にきちんと乗っ取って動作しているわけです。
今までC#などのオブジェクト指向言語を触る場合は、オブジェクトは生成時に初期化が走るので、中身がリンクされている状態なんて想像もできませんでしたが
Pythonはイミュータブルとミュータブルを巧みに扱って
オブジェクトの状態を正常に保つように運用されているのです。
なるほど、Pythonが好まれている理由の一端がなんとなく垣間見えた気がします。
さて、でも問題は解決していません。
ミュータブルによって配列の値が共存している状態では、個々の状態を”ユーザーの認識”として正常に運用できていません。
そこで、先ほどの「初期化」がポイントになります。
5. 新築のミュータブル
そう、オブジェクトの側がイミュータブルなのでそちらはオブジェクトが切り替わりましたが、中身が同じなのでミュータブルのままです。
その解決策が”初期化”になります。
具体的に見ていきましょう。
class ArrayData:
datas = []
# datasを空に初期化するための初期化関数を作る
def __init__(self):
self.datas = []
def add_data(self, data):
self.datas.append(data)
def print_datas(self):
for in_data in self.datas:
print(in_data, end="")
# 自身のオブジェクトidを表示
def print_self_id(self):
print(id(self))
# 自身が持っている配列のオブジェクトidを表示
def print_datas_id(self):
print(id(self.datas))
sample_data = ArrayData()
sample_data.add_data("Hello")
other_sample_data = ArrayData()
other_sample_data.add_data(", World.")
sample_data.print_datas()
print("")
sample_data.print_self_id()
other_sample_data.print_self_id()
sample_data.print_datas_id()
other_sample_data.print_datas_id()
初期化関数を追加しました。
もう一度プログラムを動かすと
クラスオブジェクトID群
sample_data id ->4310257504
other_sample_data id ->4310257616
クラスオブジェクト内変数ID群
sample_data.datas id ->4309426056
other_sample_data.datas id ->4310855112
今度は全てのIDが切り替わりました。
ミュータブルな値が更新されたので、別のオブジェクトと見なされるようになりました。
6. おわりに
たったこれだけのことなのですが、Pythonという言語が
とても考えられて作られていることが分かる事例だったのでつい記事にしてしまいました。
ぜひ、皆さんもPythonを使ってみましょう!
では。