【Python】共有リファレンス

python_logo

こんにちはー!

今回は変数とオブジェクトを語る上で重要な概念の一つ共有リファレンスについてまとめます。

そして共有リファレンスを説明するためには、オブジェクトの可変性・不変性についても見ていきますよー!

今回の話は変数とオブジェクトの関係について知っていたほうがイメージしやすいです。

以下の記事をご賞味あれ!変数とオブジェクトについて語ってます!

【Python】動的型付け(Dynamic Typing)

本投稿の内容

  1. 共有リファレンスとは
  2. 不変オブジェクト
  3. 可変オブジェクト
  4. 共有リファレンスの恐ろしさ
  5. オブジェクトの上書きを避ける
  6. 「同等と同一」問題

共有リファレンスとは

変数はオブジェクトへのリファレンスを保持しているだけのただの箱です。

変数は代入されればそのオブジェクトのリファレンスを持ちます。

さて、同じオブジェクトを別々の名前の変数に代入した場合を考えて見ましょう。

複数の変数が同じオブジェクトに対してリファレンスを保持していることを共有リファレンスといいます。

(2つ以上の変数が)共有(しているオブジェクトに対しての)リファレンスっていうことです。

さて、それだけなら話はここまでですが、わざわざ取り上げるのです。何かあるのです。

そうです。問題は共有するオブジェクトの種類によって微妙に困ったことになるのです。

オブジェクトの種類は2つあります。不変オブジェクトと可変オブジェクトです。それぞれ見ていきましょう。

不変オブジェクト(immutable)

オブジェクトの中身(値)を変更することが不可能なオブジェクトです。

俺の中身絶対守るマン。それが不変オブジェクト。

彼は何の問題も起こしません。Python界の優等生!それがイミュータブルオブジェクトさん。

よく上げられるものとしてintさん、strさん、tupleさんがいます。

可変オブジェクト(mutable)

オブジェクトの中身を変更することが可能なオブジェクトです。

全てにルーズな男。それが可変オブジェクト。

全てに寛容でいくらでも彼の心に土足で踏み込むことができます。

但し、デリケートに触ってあげないと痛い目を見るのはあなたです。

代表的なのはlistさん、dictさんでしょう。

共有リファレンスの恐ろしさ

何はともあれ、下のコードを読んでいただきたい。何が起きているか分かるだろうか。

上の例は不変オブジェクトの演算を行っている。

文字列同士の演算の結果は常に新しいオブジェクトを生成する。つまり、この例だと B += "Up?" 新しいオブジェクトを新しく作成してBに詰め込んでいる

今やAとBと入っているオブジェクトは違うのだ。だから結果が意図したものとなっている。

下の例は可変オブジェクトの演算を行っている。

CにDの値を代入して、そのあとでやっぱりDの値を変えたいと思い、Dの値を変えた。Dは変わった。だがCは変わって欲しくなかった。

そう、CとDは可変オブジェクトが格納されているので中身をいくらでも替えられる。新しいオブジェクトが生成されるわけではない

従ってCとDは同じオブジェクトを保持しているために悲しい結末に至ってしまった。

オブジェクトの上書きを避ける

どうしたら、CとDは適切に分離できるのか?

簡単な話だ。コピーして新しいオブジェクトを作ってあげればいい

幸いPythonにはそのような方法はいくつかある。

おや?ネストしているとうまくいかないようだ。

実はコピーにも2つの種類がある。浅いコピー(シャローコピー)と深いコピー(ディープコピー)だ。

浅いコピー(shallow copy)

このコピーは新しくオブジェクトを作成して、その中にコピー元のオブジェクトの参照で埋めていくのだ。

この場合で困るのはやはり、中身が可変オブジェクトの場合だ。

中身が可変オブジェクトだと、その参照をコピー先に渡してしまう。そうするとわざわざコピーする意味がなくなってしまう。

だって、コピー先でそのオブジェクトの内容を変えたら変わっちまうんだぜ?

なんて罪深いんだ可変オブジェクト!

深いコピー(deep copy)

さて、上記の問題に対する対策がこのディープコピーだ。

なんていったって、新しいオブジェクトを作ってさらにその中身もコピーするのだ。(そしてその中身もコピーする)

copyモジュールのdeepcopyでうまくできる。

「同等と同一」問題

実は、不変オブジェクトも問題を起こさないわけではない。 とはいってもそんなに影響はない。

というのも、Pythonでは同等と同一は似ているが微妙に異なっている。

2つの意味は以下のように区別される。

  • 同等(equal)とは値が同じならばTrueとされる。
  • 同一(identical)とはオブジェクトが同じならばTrueとされる。

とりあえず下の例を見てみよう。

ビルトイン関数のidはオブジェクトのメモリの場所を返している。同一とはこの値が同じことをさす。

もう分かったろう。同等と同一は違うのだ。

Tips・注意点

さて、実は気をつけるべき箇所は意外とあります。
少しだけ確認していきます!

copyより スライシング を使うこと

これはパフォーマンスの問題です。

スライシングができるオブジェクトはこっちを使ったほうがパフォーマンスが良いです。

それはスライシングが言語仕様としてサポートされているのでPythonの裏で動くCが高速だからです(CPythonであることを前提としています)。

一方copyは組み込みモジュールなのでPythonで逐次的に動いています。

不変オブジェクトは再利用される

さて、同一と同等の問題の特殊ケースになります。

先ほどの説明ですと、生成されたオブジェクトが異なれば全て同一ではないってことになります。

実はこれには例外があります。以下のコードをまず読んでください。

新しくオブジェクトを生成しているように見えて実は同じリファレンスを保持しています。

そうです。値が不変であることは再利用しても別にいいよね?ってことでPythonがキャッシュしているのです。

削除されてもしばらくはキャッシュされる

これも不変オブジェクトに関係しています。

不変オブジェクトの中身は変更されることがありません。

使われなくなったからといってガベージコレクションが削除するのでは、頻繁に使う値を何度も何度も生成することになります。

これはもったいないです。なので、不変オブジェクトはキャッシュされて再利用されます。

ガベージコレクションされるまではキャッシュされ再利用されます。

まとめ

共有リファレンスについてまとめました。

変数は所詮オブジェクトのリファレンスしか持っていません。
従って参照先のオブジェクトが可変か不変かによってそれぞれ特性が有ります。

その特性によって引き起こす問題を今回みていきました。

少しふざけすぎた説明が多かった気がしますが、まぁ言いたいことはたぶん伝わったのではないでしょうか。

オライリーの著者が英語の本は結構砕けていて読んでいて楽しいです。ちょっとそれにインスパイアされています。

以上になります。

みなさんのPythonライフがよりよくなるように祈っています!