PHPしか知らない僕がPythonを少し触ってみたよ 〜基本的に参照渡し〜

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
テニヌならこれくらいはできます

この記事を三行にまとめると

基本的に変数は参照渡しになるようです
樺地は無我の境地を使わずとも手塚ゾーンをコピーできた
仁王は手塚ファントムすらもコピーできた
PHPでは、変数に値を渡す方法として主に「値渡し」と「参照渡し」ってのがあります。しかしPythonでは、基本的に変数は参照渡しになるようです。一見値渡しっぽい動きをしていても、実際は参照渡しになってるらしい。

どういうことなのか順番に見ていきましょう。



PHPの値渡しと参照渡し

値渡しというのは、値だけを渡すことです。文字通りの説明になってしまいますが、例えばこんなの(↓)が値渡しです。

$a = 1
$b = $a

$bという変数に$aという変数を代入していますね。

この時、例えば$aの値を変更しても$bの値は変わりません。逆もまたしかり。

//$aを変更
$a = 1
$b = $a
$a = 2

echo $a; //2
echo $b; //1

//$bを変更
$a = 1
$b = $a
$b = 2

echo $a; //1
echo $b; //2

こんな風に、値渡しと言ったら変数の中に入っている値だけを渡すようなものと認識しておけば良いと思います。

対して参照渡しというのは、変数そのものを渡すようなイメージです。参照先が一緒になるって言い方でも良いのかな?

$a = 1
$b = &$a

この時、$aと$bは変数名が違うだけで同じものを参照する状態になってます。なので、$aの中身を変更すると$bも変わる。逆もまたしかり。

//$aを変更
$a = 1
$b = &$a
$a = 2

echo $a; //2
echo $b; //2

//$bを変更
$a = 1
$b = &$a
$b = 2

echo $a; //2
echo $b; //2

片方の変数に2を入れた時点で、両方とも中身が2になります。これが参照渡しです。

何だろう……例えばタンスの引き出しの一段目に$aというラベルを貼ったとして、二段目の引き出しに$bというラベルを貼って一段目の引き出しの中身をコピーするのが値渡しなのに対して、参照渡しの場合は$bというラベルも一段目に貼る、みたいな感じかな。だから常に中身が一緒になる、みたいな。余計分かりづらいか。すまん。

値渡しの$b=$a





Pythonの値渡し?

では上の値渡しと同じようなコードをPythonで書いてみます。

a = 1
b = a
a = 2

print(a) #2
print(b) #1

aの中身を入れ替えてもbは変わっていません。PHPと同様に値渡しっぽいですよね。でも挙動的には参照渡しをしているそうです。

僕もちゃんと理解できてるか怪しいんであれですが……さっきのタンスの例で言うと、まず一段目の引き出しにaというラベルを貼って1を入れる。

a=1

次に「b = a」の部分でbというラベルも一段目の引き出しに貼る。この時点ではaとbが同じ引き出しを指します。つまり参照渡しです。

b=a

そんでもって次の「a = 2」は、二段目の引き出しに新たにaというラベルを貼り直して、そこに2を格納している……みたいなイメージだと思われます。だから一段目に貼られたbの方は特に変化しない。

a=2



リストや辞書の参照渡し

int型やstr型だと上記のような値渡しっぽい参照渡しの動きになるのですが、リストや辞書だとPHPの参照渡しと同じような動きになります。つまりどちらかの変数の中身を入れ替えればもう片方も変わることになる。

a = [1, 2, 3]
b = a
a[0] = 5

print(a) #[5, 2, 3]
print(b) #[5, 2, 3]

a[0]の中身を入れ替えた時点でb[0]の中身も入れ替わります。辞書の場合も同様。

だから例えば複数の変数をまとめて初期化しとこうなんて思った時に、こんな書き方をするとうっかり参照渡しが行われちゃうんで、注意が必要です。

a = b = []

二つの変数を別々に使う想定でいたのに、一方の値を変更するともう一方も変わってしまって、思わぬ動きをしてしまうかもしれない。

a = b = []
a.append(1)

print(a) #[1]
print(b) #[1]



関数の場合も参照渡しになるのでPHPと同じ感覚で書いてるとおかしなことになっちゃうかもしれない。

def sample(b):
  b[0] = 5

a = [1, 2, 3]
sample(a)

print(a) #[5, 2, 3]

returnで値を受け取らなくても関数を実行した時点でaの中身が変わっています。



参照渡ししたくない場合

時にはリストや辞書の中身だけを他の変数にコピーしたいこともあるかもしれない。そんな時はどうするか。

a = [1, 2, 3]
b = a.copy()
a[0] = 5

print(a) #[5, 2, 3]
print(b) #[1, 2, 3]

リストには「copy()」という関数が用意されているので、それを使えば参照渡しじゃなくなります。辞書の場合も使える。

ただこのcopy()は階層が多次元になると適用されません。

a = [1, 2, [1, 2, 3]]
b = a.copy()

a[0] = 5
a[2][0] = 10

print(a) #[5, 2, [10, 2, 3]] 
print(b) #[1, 2, [10, 2, 3]] 

ちょっと分かりづらいかもしれませんが、階層の深い[10, 2, 3]のところは、bも値が変わってしまっていますね。参照渡しになってる証拠です。

もし多次元のリストや辞書をコピーするなら、copyモジュールを使うのが良いみたいです。

import copy

a = [1, 2, [1, 2, 3]]
b = copy.deepcopy(a)

a[0] = 5
a[2][0] = 10

print(a) #[5, 2, [10, 2, 3]] 
print(b) #[1, 2, [1, 2, 3]]

copyモジュールの「deepcopy()」を使えば多次元でも問題ない。一次元のリストや辞書でも使えるので、階層がどうなるか分からないのであればdeepcopy()を使っとけば大丈夫でしょう。






PHPだとobject型の場合はデフォルトが参照渡しになりますが、連想配列や関数はそうじゃないので、慣れないうちは「あれ?」って思うことがあるかもしれません。でも基本は参照渡しだってことと、リストや辞書はコピーする機能があることを知っておけば恐るるに足らずでしょう。

コピーといえば、テニプリで無我の境地はいろんなプレイヤーの技をコピーして使えるというとんでもな能力でしたが、なぜか手塚ゾーンだけはできないという条件がありました。でも樺地は無我の境地を使わずとも手塚ゾーンをコピーできた。ってか無我の境地(百錬自得の極み)すらもコピーしてた。そして仁王は手塚ファントムすらもコピーできた。

その意味が分かるな?



その他のPythonを少し触ってみたの記事はこちら
前書きと索引的な
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください