間違い等ありましたら、遠慮なくコメントください!
この記事の対象となる人
Pythonをある程度使えるようになった人
はじめに
Pythonを勉強してからしばらく経つと、下のような現象に直面するでしょう。
疑惑の処理1
a = ["hoge", "hoge", "hoge"]
b = a
a[0] = "piyo"
print(b)
結果
['piyo', 'hoge', 'hoge']
aに対して実行した結果が関係ないはずのbに影響しているというものです。この記事ではこの現象を理屈から理解することを目標にします。理解するために必要なワードをまず一つ一つ解説したいと思います。それとともに、この現象の回避方法として使われる下のコードの仕組みを理解することも目標にしたいと思います。
Pythonを学び始めた人向けに一部厳密には記述していない部分がありますが、本質ではないので注釈でさらっと触れる程度にします。読まなくても構いません。注釈にはプラスアルファ的なことも書いておきます。
疑惑の処理2
a = ["hoge", "hoge", "hoge"]
b = a[:]
a[0] = "piyo"
print(b)
結果
['hoge', 'hoge', 'hoge']
オブジェクトについて
プログラミングを学んでいると時々聞くワードかとは思いますが、なんだかホワンとしたイメージで敬遠してしまうことが多々あると思いますが、完結に説明したいと思います。横文字だからなんとなく難しそうなイメージがありますが、日本語訳は物です。なんの変哲もないただの物です。現実世界で言うと、消しゴム、カレー、地球はすべてオブジェクトです。コンピューターの世界では、2
、"piyo"
、["hoge", "piyo"]
、{1:"hoge", 2:"piyo"}
、True
などです。1ただ、コンピューターの世界では手に触れる物体としては存在できず、メモリというパソコンの中にあるパーツの中に0と1の羅列として存在しています。プログラミングを初めて1年未満の若造が言うのはどうかと思いますが、プログラミングなんて所詮オブジェクトをいじくり回しているだけです。
変数について
Pythonの他にプログラミング言語を学んだことがある方は、「変数は値を入れる箱だ」という解説を聞いたことがあるかもしれません。ですがPythonおいてはそれはあまり正しい説明ではなくて、「変数はオブジェクトにペッと貼り付ける名前シールだ」と説明するほうが的確です。もう少し正確な話をすると変数は、自分が貼られているオブジェクトはメモリの0と1の羅列の中のどこにあるのか、という情報だけを持っています。2変数はただの名前シールでしかないのです。ですが、なんの変哲もないただの名前シールのおかげで、コードの別の部分でそのオブジェクトが呼び出すことができるのです。3逆にこれ以外何も仕事はしません。
代入について
プログラミングの代入は、数学の代入とは随分と性格が違います。数学では左辺と右辺を入れ替えても何も変わりませんが、プログラミングでは大変なことになります。基本的には変数=オブジェクト
の形で使います。このような代入することで名前シールをペッとオブジェクトに貼り付けることができます。4右辺が"hoge"
や[2,4,42,4,10]
のように明らかにオブジェクトそのものである場合には問題ないのですが、そうでない場合もありあります。例えば、9 + 5
のような四則演算や"hoge hoge".slice()
といった関数がという場合です。このような場合には右辺がまず処理されて最終的な結果がオブジェクトとして生成されて、そこに名前シールが貼り付けられます。
変数=変数=変数=オブジェクト
のように使うこともできます。このときは一つの同じオブジェクトに3つのの名前ラベルをペッペッペッと貼り付けているのです。
変数=変数
の場合は、右辺の変数が張り付いているのと同じオブジェクトに左辺の変数をペッと貼り付けています。変数そのものは、自分が貼られているオブジェクトは、メモリのどこにあるのか、という情報だけを持っているので、この情報が左の変数に渡されるはごく当たり前の動作です。この場合は、左辺右辺どちらも変数なので、入れ替えても問題ないと感じられるかもしれません。しかし、Pythonでは左辺に変数を書くと同時に、その変数が定義されるので「まだ定義されていないよ〜」というエラーが出ます。
疑惑の処理を解決
ここまで出てきた要素を抑えれば、例の疑惑の処理を理屈から理解するための土台はすでに完成しています。早速解説していきます。
疑惑の処理1
a = ["hoge", "hoge", "hoge"]
["hoge", "hoge", "hoge"]
というリストのオブジェクトを作り、a
という名前シールを貼り付けています。リストの要素にはそれぞれ、番号が変数として割当てられています。
b = a
変数=変数
の形なので、a
が貼ってあるオブジェクトにb
も貼り付けています。
a[0] = "piyo"
上のワード解説を読んだ方ならこのコードに違和感を覚えるでしょう。なぜなら左辺が純粋な単なる変数ではなくて、リストの何番目かがついているからでしょう。ですが、Pythonのドキュメントに
代入はターゲットとオブジェクトの間に束縛を作ります。
とあるので、右辺が明らかにオブジェクトである以上左辺は変数でしょう。だとしたら、左辺は、"aのリストの1番目”という意味の名前シールなのです。このコードでは、新しく"piyo"
というオブジェクトを作って、この変数がもともと指していたオブジェクトと入れ替え、名前シールの付け替えもしているのです。5このようなリストのインデックスや、リストのスライス、オブジェクトの属性に代入の作業をする場合は、名前シールとして考えると回りくどいので単純にオブジェクトを入れ替えていると考えたほうがわかりやすいかもしれません。しかし、シールの貼付けという代入の本質は変わりません。
aとbは同じオブジェクトに結びついているので、このような状態でprint(b)
を行うと、b[0]が変更されているのが確認できるのは自明です。これで一つ目の謎が解けましたね。
疑惑の処理2
a = ["hoge", "hoge", "hoge"]
先ほど説明したので割愛しますね。
b = a[:]
疑惑の処理1の解説でa[0]
は変数として捉えましたが、右辺にある場合は、素直に変数=オブジェクト
として捉えましょう。指定された範囲の情報を取り出すという処理を行うための記号が[]
である、として考えればわかりやすいでしょう。そのためa[:]
という部分が処理されて["hoge", "hoge", "hoge"]
というオブジェクトが新たに作られます。そのオブジェクトのbという名前シールが貼り付けられます。
a[0] = "piyo"
先ほどと同じように、変数が表すオブジェクトを新しく作ったオブジェクトに入れ替えて、名前シールもつけ直すという処理をしています。
aとbは全く別のオブジェクトを指しているので、print(b)をすると、"piyo"
になっていないのは自明ですね。
これで完璧に理解できました。目標達成!!まだ続きますが。。。
よくある別の疑問
前述した疑惑の処理と関連して、こちらのコードもよく問題になります。
a = "hoge"
b = a
a = "piyo"
print(b)
結果
"hoge"
普通だったら結果は"piyo"
ではないかというのです。この問題でよくイミュータブル・ミュータブルの話題が出てきますが、理解するために必要な知識ではありません。6この記事でこれまでに全くこれらに触れていませんが、この記事の内容に沿って考えれば理屈から理解でき、自明だとわかるはずです。キーポイントは「代入のときに新しくオブジェクトを作って、名前シールを貼っている」です。
終わりに
初めてqiitaで記事を書いてみました。すでに似たような情報がネット上に散見されるのですが、必要な知識が一つにまとまったサイトが見つからなかったのでこの記事にまとめてみました。最後まで読んでわからなかった人、ごめんなさい、これから精進していきたいと思います。お詫びと言っては何ですが、こちらのサイトで遊んでみればなんとなく解ると思います。間違い、より良い解釈等あったら是非コメントください。励みになります。
参考にしたサイト
7. 単純文 (simple statement) — Python 3.8.2 ドキュメント
copy --- 浅いコピーおよび深いコピー操作 — Python 3.8.2 ドキュメント
-
厳密にはクラスからインスタンス化されたものをオブジェクトといいます。そのため、functionクラスからインスタンス化された関数、moduleクラスからインスタンス化されたモジュールもオブジェクトです。 ↩
-
この情報をid(identity)といいます。Pythonのコード中ではid()という関数を使って変数の持つアイデンティティを調べることができます。 ↩
-
これを参照といいます。参照ではコード中の変数の部分が、変数が貼られているオブジェクトの内容に化けます。 ↩
-
Pythonではこの貼付けの動作を束縛と説明します。 ↩
-
図からわかるように、リストの外に出た
"hoge"
というオブジェクトには名札が一つもついていない状態になってしまいました。名前がついていないオブジェクトはコードの他の部分で呼び出せなくなってしまうので、完全にいらない子になってしまいます。このようないらない子を自動的にナイナイしてくれるのが、「ガベージコレクション」というものです。 ↩ -
ミュータブルとはidを変えずにオブジェクトの持つ情報を変更できるという意味です。リストはミュータブルなオブジェクトです。イミュータブルとはidを変えずにオブジェクトの持つ情報を変更できないという意味です。ストリングはイミュータブルなオブジェクトです。ミュータブル・イミュータブルの話が何故出てくるのか自分なりに考えてみました。おそらく、
a[0] = "piyo"
の部分がミュータブルな場合にしかできないからではないでしょうか。 ↩