Python 参照先アドレスについて

f:id:monozukuri-bu:20190730112500j:plain

Python3エンジニア認定基礎試験に合格したふっちーです。
合格ラインギリギリでしたがなんとか1発で受かりました。
ありがとうございます。

というわけで今回もPythonをやっていきます。
勝って兜の緒を締めよ、です。

今回は参照先アドレス特集です。
前回でも少し触れた内容ですが、これがきっかけでPythonの学習が捗りました。
ちょっとしたもやもやが晴れるかもしれません。
さあ、一緒に学んでいきましょう。

環境

今回もGoogle Colabolatory(Colab)を使用しました。
colab.research.google.com

参照先アドレスとは

参照先アドレスとは変数の情報(数値や文字列など)を保管している住所の様なものです。
変数が持っている情報をコンピュータが知りたい場合に、この参照先アドレスでどこに保管してあるかを調べるのに使います。
参照先アドレスは組み込み関数id()で確認できます。
実際に見てみましょう。

n = 3
print(id(n))
出力結果:
10968864

変数(n)が持っている情報(3)は10968864という場所に保管されていることが分かりました。
つまり、コンピュータがnの情報を知りたい場合は、10968864という場所に行けば、3を見つけられるという事です。

次に変数nの情報を変更してみましょう。

n = 9
print(id(n))
出力結果:
10969056

このとき、3は10968864に残ったままです。あくまでnの参照先アドレスが9の10969056に変わっただけです。
nに9という情報が設定されたので、コンピュータがその分新しく参照先アドレスを割り振ったにすぎません。
このように参照先アドレスは変数に情報を設定した瞬間にコンピュータが自動的に決定します。

変更不能体と変更可能体

前回にも説明しましたが、Pythonには変更不能体と変更可能体が存在します。

変更可能体 変更不能
リスト、ディクショナリ タプル、文字列、数値

変更不能体とは要素の一部が変更不可であり、変更可能体は要素の一部が変更可能であるという規則があります。
そして、この区別は参照先アドレスについて考えると一気に理解が深まります。

リストが要素の変更をするときの参照先アドレスを見てみましょう。

v_to_z = ['v', 'w', 'x', 'd', 'z']
print(id(v_to_z))
v_to_z[2] = 'k'
print(id(v_to_z))
出力結果:
140656074921096
140656074921096

このように変化がありません。

タプルと文字列は変更不能体なので、上と同じことをしようとするとエラーが発生します。
また、数値を変化させると参照先アドレスが変化しましたが、これはタプルや文字列でも同じです。

w = 'double'
print(id(w))
w = 'world'
print(id(w))
出力結果:
140103509005120
140103134067488

つまり、以下のようにまとめられます。

変更不能体:同じ参照先アドレスでは要素を変更できない
      →変数の再設定は可能だが、要素の変更は不可である
変更可能体:同じ参照先アドレスで要素を変更できる
      →変数の再設定も要素の変更も可能である

要素の変更と変数の再設定

「要素の変更」と「変数の再設定」で変更不能体と変更可能体を区別できることを説明しました。
では、具体的にどのような場合で「要素の変更」と「変数の再設定」は起こるのでしょうか。

実際に確認してみましょう。

a = [6]
print(id(a))

a[0] = 8
print(id(a))

a = [7]
print(id(a))
出力結果:
140094475436616
140094475436616
140094475304712

3行目はaの0番目を8に「要素の変更」をしているのに対し、5行目ではaそのものを[7]に「変数の再設定」をしています。
確かに、参照先アドレスが変化していますね。

続いては以下の例です。

b = [2]
print(id(b))

b.append(5)
print(id(b))
b = [2]
print(id(b))

b = b + [5]
print(id(b))

どちらも[2, 5]が出力されますが、参照先アドレスはどうなっているでしょうか。

出力結果:
140094475437320
140094475437320
出力結果:
140094475304392
140094475437512

このように出力だけ見ても変化がないものでも、参照先アドレスが異なる場合があるのです。
結論としては、「変数名 = 」が再設定で、それ以外は要素の変更である、というのが一番覚えやすいと思います。

スコープ

スコープとは、変数が適用される範囲の事を指します。
スコープにはローカルスコープやグローバルスコープなどがあります。
他にもスコープはありますが、今回取り扱うのはこの2つです。

例とともに説明していきます。

x = 1

def local1():
  x = 2
  print(x)
  
local1()
print(x)

まず、ローカルスコープは関数の内側が範囲となります。
defから1つ目のprint(x)までの部分です。

def local1():
  x = 2
  print(x)

次に、グローバルスコープはモジュール全体が範囲となります。

x=1グローバル変数x=2はローカル変数と呼ばれています。

このスコープによって変数を参照できる範囲が変化します。
ローカルスコープは、その外(今回はグローバルスコープのみ)のスコープにある変数を参照できます。
反対にグローバルスコープは、ローカルスコープにある変数を参照できません。

言葉だけでは理解しにくいと思うので、上の例を動かしてみましょう。

出力結果:
2
1

まず初めに、x=1が読み込まれ、グローバル変数xに1が設定されます。
次に関数(local1)が読み込まれ、ローカル変数xに2が設定されます。
そして、local1を呼び出し、関数内のprintが動きます。→出力:2
関数の処理が終わると、最後のprintが動いて全体の処理が終わります。→出力:1

つまり、順番ではx=2の方が新しく設定されていますが、グローバルスコープのx=1に影響がありません。
これが上の書いてあるグローバルスコープはローカルスコープにある変数を参照できないの意味です。
ローカルスコープでグローバルスコープにある同じ変数名の情報が変化しても、グローバルスコープ側には関係ないのです。

同じxでも参照先アドレスはグローバルスコープ版とローカルスコープ版で分かれていると考えられます。

グローバル変数とローカル変数の例外

ローカルスコープ側で変数が変化しても、グローバルスコープ側には関係ないと説明しました。
しかし、例外が存在します。
それが今回解説した「要素の変更」と「変数の再設定」に関係しています。

もしかしたら、気づいた方もいるかもしれませんね。
結論から言うと「要素の変更」はグローバル変数にも影響を与えます。

例とともに解説します。

x = [90]

def local2():
  x[0] = 50

print(x)

local2()
print(x)
出力結果:
[90]
[50]

printはグローバルスコープにしかありませんが、要素の変更でxが変化しました。

この謎を解くカギは参照先アドレスとローカル変数はグローバル変数を参照できるという点です。

まず、グローバルスコープでx=[90]が設定されました。
次に関数local2の定義でxを参照していますが、ローカルスコープには変数xの定義が存在しません。
この場合は、一つ外側のスコープに変数を探しにいくため、グローバルスコープの変数xを参照することになります。 つまり、同じ参照先アドレスに「要素の変更」をしているという事です。
よって、最後のprintはxに設定されている参照先アドレスの情報を出力するため、変更されたものが出力されました。

まとめ

今回は参照先アドレスについてまとめました。
・参照先アドレスとは変数の所有物(数値や文字列など)を保管している場所を表した住所の様なものである。
・参照先アドレスの中身が変更できるかできないかで変更可能体と変更不能体でわかれる。
・「要素の変更」が参照先アドレスに影響を与えず、「変数の再設定」は参照先アドレスを変更することになる。
・スコープでもこの法則は適用される。

私はスコープや変更不能体などが理解しづらかったのですが、参照先アドレスのおかげでスムーズに理解することが出来ました。
拙い説明でしたが皆さんの理解に役立てられたら光栄です。