- ベストアンサー
Python:浅いコピーと深いコピー
(1) rgba = rgb(代入)は浅いコピー(参照渡し)、 rgba = rgb.copy()(コピーメソッド)と rgba = rgb[:](スライス代入?)は深いコピー(値渡し)と理解してよいのでしょうか? copy()(コピーメソッド)は浅いコピーと書いてあります。 https://atmarkit.itmedia.co.jp/ait/articles/1906/04/news009_4.html#shallowcopy (2)上記リンクに「intlist1[0]へ代入をすれば、名札の付け替えが行われる」とありますが、rgb.append("Alph")、rgb[-1]="Alpha"、rgba.append("Alph")、rgba[-1] = "Alpha"のどれをやってもid変わりません。 上記2点ってPythonの仕様変更ですか? コード(Google colabo) rgb = ["Red"] # ["Red", "Green", "Blue"] print('\n', 'rgba = rgb') rgba = rgb print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) print('\n', 'rgb.append("Alph")') rgb.append("Alph") print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) print('\n', 'rgb[-1]="Alpha")') rgb[-1]="Alpha" print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) print('\n', 'rgba.append("Alph")') rgba.append("Alph") print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) print('\n', 'rgba[-1] = "Alpha"') rgba[-1] = "Alpha" print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) print('\n', 'rgba = rgb.copy()') rgba = rgb.copy() print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) print('\n', 'rgba = rgb[:]') rgba = rgb[:] print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) print('\n', 'rgb.append("Alph")') rgb.append("Alph") print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) print('\n', 'rgb[-1]="Alpha")') rgb[-1]="Alpha" print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) print('\n', 'rgba.append("Alph")') rgba.append("Alph") print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) print('\n', 'rgba[-1] = "Alpha"') rgba[-1] = "Alpha" print(rgb, rgba) print('id(rgb)', id(rgb), 'id(rgba)', id(rgba)) 実行結果 rgba = rgb ['Red'] ['Red'] id(rgb) 135952212587840 id(rgba) 135952212587840 rgb.append("Alph") ['Red', 'Alph'] ['Red', 'Alph'] id(rgb) 135952212587840 id(rgba) 135952212587840 rgb[-1]="Alpha") ['Red', 'Alpha'] ['Red', 'Alpha'] id(rgb) 135952212587840 id(rgba) 135952212587840 rgba.append("Alph") ['Red', 'Alpha', 'Alph'] ['Red', 'Alpha', 'Alph'] id(rgb) 135952212587840 id(rgba) 135952212587840 rgba[-1] = "Alpha" ['Red', 'Alpha', 'Alpha'] ['Red', 'Alpha', 'Alpha'] id(rgb) 135952212587840 id(rgba) 135952212587840 rgba = rgb.copy() ['Red', 'Alpha', 'Alpha'] ['Red', 'Alpha', 'Alpha'] id(rgb) 135952212587840 id(rgba) 135951358069184 rgba = rgb[:] ['Red', 'Alpha', 'Alpha'] ['Red', 'Alpha', 'Alpha'] id(rgb) 135952212587840 id(rgba) 135951358898688 rgb.append("Alph") ['Red', 'Alpha', 'Alpha', 'Alph'] ['Red', 'Alpha', 'Alpha'] id(rgb) 135952212587840 id(rgba) 135951358898688 rgb[-1]="Alpha") ['Red', 'Alpha', 'Alpha', 'Alpha'] ['Red', 'Alpha', 'Alpha'] id(rgb) 135952212587840 id(rgba) 135951358898688 rgba.append("Alph") ['Red', 'Alpha', 'Alpha', 'Alpha'] ['Red', 'Alpha', 'Alpha', 'Alph'] id(rgb) 135952212587840 id(rgba) 135951358898688 rgba[-1] = "Alpha" ['Red', 'Alpha', 'Alpha', 'Alpha'] ['Red', 'Alpha', 'Alpha', 'Alpha'] id(rgb) 135952212587840 id(rgba) 135951358898688
- みんなの回答 (10)
- 専門家の回答
質問者が選んだベストアンサー
> この「何か」(値の事)に名前を与えて頂けると助かります。 > さっきはこの「何か」も名前が分からないのでオブジェクトと呼びました。 「オブジェクト」でいです。 分類が必要なら、適宜修飾語を付けて区別します。 > じゃあ変更3は最深層のオブジェクト変更だから変更1と同じですね と思ったら変更2と同じになっている。これはなぜなの?というのが分かりません。 「最深層」と「それ以外の階層」という区別をしているのなら、間違いです。強いて言うと「最浅層」と「それ以外の階層」でしょうが、「階層」という言葉には違和感があります。 変更3というのは、下記のことですよね? print('変更3') rgba=["Red", "Green", "Blue", "Alph"] correct_rgba = rgba.copy() correct_rgba[-1] = "Alpha" print(correct_rgba) print(rgba) 変更しているのは、correct_rgba[-1] というもっとも浅い部分の末尾要素の= による置き換えですね。 オブジェクトの改変という意味では、correct_rgba が参照している list オブジェクトの改変です。一番上というか親というか。 > じゃあ変更3は最深層のオブジェクト変更だから まさかと思いますが、「最深層」と「リストの末尾」を混同してませんよね? a = [[[1],[2]],[3,4]] a ・・・ 親のオブジェクト(浅いコピーだと複製オブジェクトが出来る) a[0] ・・・ その子オブジェクト(浅いコピーだとオブジェクト実体はコピー先と共有) a[0][0] ・・・さらにその子オブジェクト(aから見ると孫)(同上) 以前書いたように、「listの浅いコピー」とは、 [ y for y in x ] とイコールなので、 def shallow_copy_of_list(x): result = [] for y in x: result.append(y) return result と展開して、途中に確認用のprint()を入れてみてはどうでしょうか?
その他の回答 (9)
- cametan_42
- ベストアンサー率62% (162/261)
> ご教示ありがとうございました。やっと浅いコピーってなんなん?の答えにたどり着きました。つまり「コピー先の変更が元のリストまで及ぶ条件ってなんなん?」って話なんですが、「入れ子の場合の孫オブジェクト以下は全て」とわかりました。 うん、結論から言うとそういう事だね。 コピーってのは悩ましい問題だ。 ただ、ちと覚えておいて欲しいのは、確かに一般に「浅いコピー」「深いコピー」って概念は浸透してる。ただし、それは「すげぇ重要」って事じゃないんだ。 要は、「コピー」に対して「浅いコピー」「深いコピー」が生じる、ってのはあくまで「実装上の都合」なんだよ。実装者が「そうしよう」ってしただけ、であって、「必ずそうせねばならない」ってモンではないの。 ここは覚えておこう。 と言うのも「浅いコピー」「深いコピー」が生じる、ってのは2つ程理由がある。 ユーザーから言わせて貰えば 「"コピーを取る"っつーのなら、ガタガタ言わずに全部コピーすりゃエエやん。」 ってカンジの「文句」はそれはそれで理があるんだ(笑)。そもそも実装上の問題から言うと、単純に対象とする全データの「コピー」を取ってもらえれば、困る事がないんだよな。「浅いコピー」「深いコピー」が生じるのは、ぶっちゃけ、ユーザーに取っては不便でしかない。 一つ目の理由は、単純に、過去のコンピュータだと「メモリの量が少なかったから」なの。ある巨大(と思える)データがあって、それをバカ正直に全部コピーを取って他のメモリ領域に複製する、となると「メモリ上の実行可能エリアが圧迫される」。言い換えると、プログラミング言語実装者は「メモリを圧迫する可能性があるプログラミング言語実装を作る」事を極端に畏れてたわけだ。 よって、「コピー」と言いながら表層だけコピーを取って、うまい具合に指し示してるデータそのものを「共有しよう」としたわけだな。 少なくとも、Pythonが作られた1990年前後だとパソコンのメモリとかは今と比べると極端に少なかったわけだ。パソコンで「充分なメモリ」が備えられ始めたのって1990年代後半に入ってから、なんだよな。具体的に言うとWindows 95が出た辺りでPCが持つメモリの量ってのが極端に増える事となる。それまではRAMって半導体は結構コスパが悪くって、そうそう簡単に増やせない、言い換えるとCPUより結果値の張る部品だったんだ。 繰り返すけど、Windows 95以前は、パソコンとは「プログラムをマトモに書くにはメモリ容量が小さい」機器だったんだよ。 と言う事は、だ。今じゃないにしても将来的には、もっとパソコンのメモリ容量が増えて、「コピーと言えば問答無用に深いコピーを指す」ようになる、ってのはあり得るシナリオなんだ。テラバイトレベルのメモリがフツーにPCに搭載されるようになれば、「コピーを取る」時に使用可能なメモリ容量の残量を気にして「浅いコピーを取る」必要はなくなるだろう。そうすれば「浅いコピー」ってのはもう「古い概念」になってしまう。 それが一つ目。 もう一つの理由ってのはモロに実装上の都合だ。言い換えると「浅いコピー」は実装が簡単だけど、「深いコピー」ってのは思ったより厄介なんだよ(笑)。 浅いコピーってのはもう一度説明するけど、「ガワ」だけをコピーするんだけど、その「ガワ」が持ってるポインタの中身までコピーする。これは実装的には簡単なんだよな。 一方、深いコピーを実装するにはtraverse(走査)と言う事柄をやんなきゃなんない。何故なら、一般に、「リストのリスト」と言うモノを作った際に、それは「木構造」と言われるモノになるから、だ。 木構造 (データ構造): https://ja.wikipedia.org/wiki/%E6%9C%A8%E6%A7%8B%E9%80%A0_(%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0) 大学でコンピュータサイエンスを学ぶと、当然「木構造の走査」(traverse)は授業で扱うネタなんだけど、学生側だと「涙目になる」課題だと思う(笑)。ルートから葉を全部辿っていって、これを逐一コピーする、ってのは言う程簡単じゃないし、実行効率も落ちる、んだよね。全ての葉(末端、つまりデータ)を渡り歩くようにプログラムを書くのは結構難しいんだ(これが「得意です!」って人がいわゆる「ハッカー」になってく・笑)。 # Pythonのリストによる「木」の表現例 # ['文字', 左の子, 右の子]と言うリストを「ノード」(節)と呼ぶ。 # 人間が見れば節の'文字'はA、B、C、D、E、F、G、H、Iと言う8文字のアルファベットだと言う事はすぐ分かるが、プログラム上、この全部を「拾っていく」と言うのは結構厄介だ。 >>> tree = ['F', ['B', ['A', None, None], ['D', ['C', None, None], ['E', None, None]]], ['G', None, ['I', ['H', None, None], None]]] # なお、「親」「子」「孫」ってのは木構造対象として考えると「技術用語」になる。 上の例だと本当に「木」を意図して書かれたリストオブジェクトだけど、事実上、例えばこんな簡単なリスト構造でさえ「木構造」なんだ。 >>> obj = [[1, 'one'], [2, 'A', 'B', 'C']] 全ての木構造に対して汎用的な「コピー」を取るのは難しい。言い換えると「深いコピー」ってのは汎用的に書かれた「木構造」の複製処理であって、好んで書きたいか否か、と言われると、恐らく「否」って答える人の方が多いんじゃない(笑)? 結果、「浅いコピー」の方が実装は簡単。よってそれがデフォルト処理だ、って方が多くなるわけ。 繰り返すけど、「浅いコピー」「深いコピー」ってのは、破壊的なプログラミング、要は「代入を多用する」プログラミングに於いて「気をつけないとならない」って事なんだよね。なるたけ代入を使わない「非破壊的な」プログラミングを意識する限り、実は表面的にはそれほど問題にはならないんだ。 > わかってから言えることだけれど、浅いコピーは一段階(子オブジェクト)が別個にふるまってくれるから実用上は十分かな。 そういう事、になる。 加えると、前にも書いたけど、リスト操作に於いて、appendとかextendとかの「破壊的メソッド」を使うと、この「浅いコピー」「深いコピー」の概念が必要になる。 前にも言ったけど「破壊的メソッドをなるたけ使わない」ようにすれば、この辺のメンド臭い事に関わらずに済むわけ(笑)。この辺のメソッドを使わないだけで「気にせなアカン事」が減って、ラクチンにプログラムを書く事が出来るし、実はPythonってそういう風には設計されてるのよね。・・・原作者(グイド)が必ずしもそれを意図してたわけじゃないにしてもさ。 繰り返すけど、「リストを破壊的に変更する」メソッドは「作ったプログラムの最後の仕上げ」に必要となれば「置き換える」為に存在する。「必要となるかならないか」はプロファイラが教えてくれる。要は最後にプロファイラが何か言うまで、「非破壊的な」プログラムを書くようにすればいい。 そうすれば破壊的な動作により「思ってたんと違う」結果になる、って事が避けられるからね。
お礼
ご解説ありがとうございます。C++のポインタのせいで参照渡しが難しいものだけれど速さのために仕方ない、というイメージがついちゃってましたがそもそも実装側からすれば簡単なんですね。確かに深くネストしたデータの完コピって追っかけるの大変です。 計算機科学(?)専門の方からするとあほらしい質問だと思うのですが、こちらまで降りてきて親切に教えて下さってありがとうございます。
- notnot
- ベストアンサー率47% (4900/10358)
まず、 > 正しい言葉で質問できず申し訳ありません。層ではなく、親・子・孫・ひ孫というんですね。 私の使った「親・子・孫・ひ孫」は別にIT用語ではなくて日常用語の意味です。ITの説明に出てきた単語が全てIT用語だとは思わないで下さい。ほとんどが日常用語です。 つまり「~というんですね。」という理解は間違いです。人により説明に使う日常用語は違います。 > ”最浅層”(子)でわかりました。つまりこういう事ですね。 そうですね。「listの浅いコピー」が [y for y in x] であることがきちんと理解できて、「listの個々の要素の参照/置き換え/追加/削除」などの基本概念が正しく理解できていれば、ほぼ自明です。 subarist00さんの場合は該当しない気がしますが、このあたりが理解できない人というのは、 ・変数への代入 ・オブジェクトの改変(オブジェクトの一部への代入など) の区別が付いていないケースが多いです。 a = xxx と a[1] = xxx が同じ概念に見えてしまう。
- cametan_42
- ベストアンサー率62% (162/261)
> MIT標準教科書、めっちゃいい本でした。 そりゃあ良かった。 僕は初版しか持ってないんだけど、ホント、下手なPython本買うならこれ買っとけば値段は高いけど暫くは持つハズです。複数の本持ってても良いことないしね。 さて。 > 多分私が分かっていないのはそういう事でいいんですよね? はい、いいです。 > 最後の例で変更1、2は理解できますが、変更3がなぜ「名前の付け替え」が起こるのかが理解できません。 「変更3」ってこのパターンの事かな? # intlist1 と intlist2は各リストオブジェクトの参照先を共有している intlist1 -> [ , , ] ↓ ↓ ↓ [1, 2] [3, 4] [5, 6] ↑ ↑ ↑ intlist2 -> [ , , ] # intlist1の要素0の参照先を変更 intlist1 -> [ , , ] ↓ ↓ ↓ 'foo' [1, 2] [3, 4] [5, 6] ↑ ↑ ↑ intlist2 -> [ , , ] 基本的にこの動作はデフォ動作なんだ。 例えば >>> intlist1 = [1, 2, 3] >>> intlist2 = intlist1.copy() として、 >>> intlist1[0] = 101 とした時、 >>> intlist1 [101, 2, 3] >>> intlist2 [1, 2, 3] となるけど、両リストが持ってるアドレスは、 all(id(i) == id(j) for i, j in zip(intlist1, intlist2)) False であり、 >>> any(id(i) == id(j) for i, j in zip(intlist1, intlist2)) True となる。具体的には両リストの0番目の要素が指してる整数オブジェクトが違うんで、アドレスは同一じゃない。 intlist1 -> [ , , ] ↓ ↓ ↓ 101 1 2 3 ↑ ↑ ↑ intlist2 -> [ , , ] と言うカタチで、2と3は共有してるけど、先頭だけは違う状態だ、って事だな。 多分混乱してる一番の原因は最初の例が「文字列」だからだと思うんだ。 >>> strlist1 = ['foo', 'bar', 'baz'] >>> strlist2 = strlist1.copy() 問題は、こういう操作が可能なんじゃないか、って思える辺りなんだ。 >>> strlist1[0][0] = 'b' 例えば「文字列が無い」C言語みたいな言語(気の利いたC言語入門書では文字「配列」と呼んでいるが、これは一般的には文字列じゃない)だとこれは妥当に思える操作なんだけど、生憎そうはならない。 Pythonでは文字列はイミュータブル、つまり変更出来ない。言い換えると「破壊的操作」である文字列の中身を「改変する」って事は出来ないんだ。 フツーの言語じゃこれは「あり得る」操作なんだけど、生憎Pythonはそうじゃない。また、前に見た通り、一旦Pythonで文字列が生成されるとそれは唯一無二の存在となる。つまり、とあるメモリ領域に生成された以上、コピーさえ出来ない物体と貸す、んだ。 言い換えると、リストのdeepcopyでさえ「文字列を複製する事は出来ない」。試してみれば分かるけど、deepcopyを噛ましても、リストに含まれるデータとしての文字列の複製は出来ない、んだ。 # Pythonでの面白い実験 >>> a = 'hoge' >>> b = 'hoge' >>> id(a) == id(b) True # 文字列では変数は「同じオブジェクトを指してる」事が分かる。つまり、一旦'hoge'と言う文字列を生成すると、それはメモリ上では唯一無二の存在となる。 >>> a = [1, 2] >>> b = [1, 2] >>> id(a) == id(b) False # 一方、リストオブジェクトの場合は、「新規生成」が基本だと言う事が分かり、変数aが指す[1, 2]と変数bが指す[1, 2]は同じアドレスに存在しない事が分かる。結果、2つはアドレス的には「同値ではない」と言う事だ。 結果、文字列を含むリストを「改変」する際には、要素が指す文字列オブジェクトを「直接改変する」事は出来ないんで、ポインタを「別に生成した」文字列を指すように変更する。結果これがデフォ動作なんだよ。 んで元に戻ろう。 例えば >>> intlist1 = [[1, 2], [3, 4], [5, 6]] >>> intlist2 = intlist1.copy() とした後、 >>> intlist1[0][0] = 101 と言う操作の操作対象はあくまで、外側のintlist1じゃなくってintlist1の第0要素のアドレスが指しているリスト[1, 2]なんだ。 つまり、外枠を考えなければ >>> [1, 2][0] = 101 と言うような操作を「直接」指定してるだけであって、「リストオブジェクトの要素がそれぞれ指してるオブジェクトを別のオブジェクトに挿し替える」と言うルールを侵してるわけじゃあないんだ。 もう、intlist1[0]と言う指定をした時点で内側のリストが「あたかも変数のように」指定されていて、「それに対しての操作」になっている。intlist1と言う大枠に対して「こうしろ」って指定してるわけじゃないんだ。 だからこういう風に「内側のオブジェクト」が変更されているわけだ。 >>> intlist2 [[101, 2], [3, 4], [5, 6]] 分かりづらければ、[]による要素指定の影響を受けるのは1段階まで、って覚えておけばいい。事実、例えば、 >>> intlist1 = [[1, 2], [3, 4], [5, 6]] >>> intlist2 = intlist1.copy() >>> intlist1[0] = [101, 2] >>> intlist2 [[1, 2], [3, 4], [5, 6]] と、一段階で指定して、intlist1の0番目の要素を丸ごと[101, 2]で置き換えるようにすれば、全くintlist2の変更はない、って事が分かるだろう。「多次元リスト」でリストのリストの・・・って[][]...で指定しなければ、実は文字列を扱った時と同じような動作になっている。 また、加えると、Pythonの場合は文字列、タプル、とイミュータブルなデータが結構多い。タプルなんかも「変更不可」なんで、[][]...って連鎖で「内側のタプルを指定してそれ自体を破壊的に変更する」ってのは許されてないんだ。 # 怒られる例 >>> intlist1 = [(1, 2), (3, 4), (5, 6)] >>> intlist2 = intlist1.copy() 結果、[][]...の連鎖で「アレ?」っておかしな現象に見舞われる、ってのは実はリストを要素とするリストくらいしか無い、とも言えるんだよ。リストで多次元リストを作ろうとさえしなければあんま影響がない、っつーか・・・(笑)。 なお、先にも書いた通り、リストは内容が全く同じでも「別のアドレスを割り当てて新規作成される」んで、ハッシュテーブル(辞書型)のキーになる事は出来ない。言っちゃえばPythonでは[1, 2]と[1, 2]が「同値かどうか」判別出来ないんだよね(笑)。よって「唯一無二」として検索用のキーが必要となる辞書型のキーには適さない、と(笑)。副次的にそういった現象が起こるわけ。 以上、かな。
お礼
ご教示ありがとうございました。やっと浅いコピーってなんなん?の答えにたどり着きました。つまり「コピー先の変更が元のリストまで及ぶ条件ってなんなん?」って話なんですが、「入れ子の場合の孫オブジェクト以下は全て」とわかりました。 Google Colabo print('オリジナル') a = [[[[[1], 2], 3], 4], 'foo'] b = a[:] print('a=', a) print('b=', b) print('a[0]=6 子オブジェクト') a = [[[[[1], 2], 3], 4], 'foo'] b = a[:] a[0] = 6 print('a=', a) print('b=', b) print('a[1]=6 子オブジェクト') a = [[[[[1], 2], 3], 4], 'foo'] b = a[:] a[1] = 6 print('a=', a) print('b=', b) print('a[0][0]=6 孫オブジェクト') a = [[[[[1], 2], 3], 4], 'foo'] b = a[:] a[0][0] = 6 print('a=', a) print('b=', b) print('a[0][1]=6 孫オブジェクト') a = [[[[[1], 2], 3], 4], 'foo'] b = a[:] a[0][1] = 6 print('a=', a) print('b=', b) print('a[0][0][0]=6 ひ孫') a = [[[[[1], 2], 3], 4], 5] b = a[:] a[0][0][0] = 6 print('a=', a) print('b=', b) print('a[0][0][1]=6 ひ孫') a = [[[[[1], 2], 3], 4], 5] b = a[:] a[0][0][1] = 6 print('a=', a) print('b=', b) オリジナル a= [[[[[1], 2], 3], 4], 'foo'] b= [[[[[1], 2], 3], 4], 'foo'] a[0]=6 子オブジェクト a= [6, 'foo'] b= [[[[[1], 2], 3], 4], 'foo'] a[1]=6 子オブジェクト a= [[[[[1], 2], 3], 4], 6] b= [[[[[1], 2], 3], 4], 'foo'] a[0][0]=6 孫オブジェクト a= [[6, 4], 'foo'] b= [[6, 4], 'foo'] a[0][1]=6 孫オブジェクト a= [[[[[1], 2], 3], 6], 'foo'] b= [[[[[1], 2], 3], 6], 'foo'] a[0][0][0]=6 ひ孫 a= [[[6, 3], 4], 5] b= [[[6, 3], 4], 5] a[0][0][1]=6 ひ孫 a= [[[[[1], 2], 6], 4], 5] b= [[[[[1], 2], 6], 4], 5] 要は子オブジェクトのリストだけは複製してるから別個に変えられるけれど、それ以下は元のオブジェクトを参照したままだから別個には変えられないと。
補足
これびっくりです。教えて頂いてありがとうございます。 # Pythonでの面白い実験 >>> a = 'hoge' >>> b = 'hoge' >>> id(a) == id(b) True わかってから言えることだけれど、浅いコピーは一段階(子オブジェクト)が別個にふるまってくれるから実用上は十分かな。
- notnot
- ベストアンサー率47% (4900/10358)
> 浅いコピーではcorrect_rgbaとrgbはidが別でも同じオブジェクトを参照するのでは? ????? 「idが異なる」==「別オブジェクト」 「idが同じ」==「同じオブジェクト」 ですよ。 オブジェクトが list という「他のオブジェクトを内部に含むオブジェクト」ですが、そのあたりは理解できていますか? ・外側のオブジェクトは異なる ・内側のオブジェクトは同じオブジェクトを参照している というのが浅いコピーの結果生じることです。 a = [[1,2],[3,4]] b = a.copy() print(id(a), id(a[0]), id(a[1])) print(id(b), id(b[0]), id(b[1])) aとbは別のオブジェクトを指しているが、 a[0]とb[0]は同じオブジェクトを指している
お礼
ご回答ありがとうございます。浅いコピーはidが違えば別のリストオブジェクト、しかし別のリストオブジェクトが同じ「何か」を参照していることは理解できています。 この「何か」(値の事)に名前を与えて頂けると助かります。 さっきはこの「何か」も名前が分からないのでオブジェクトと呼びました。
- notnot
- ベストアンサー率47% (4900/10358)
ちょっと補足です。言葉が足りてませんでした。 コピーされた新規オブジェクト(rgbが参照しているオブジェクトとは別物)を変更しているので、コピー元であるrgbaには無関係です。 ↓↓↓ コピーされた新規オブジェクト(rgbが参照しているオブジェクトとは別物)の浅いレベルの要素を変更しているので、コピー元であるrgbaには無関係です。 浅いコピーされたオブジェクトの深いところを変更すると、さっきの回答の最後の例のように、コピー元とコピー先で共用されているオブジェクトの改変になるので、また違ってきます。
お礼
ご回答ありがとうございます。 >浅いコピーされたオブジェクトの深いところを変更すると、 ここなんですよね。深いところってどこ? 最深層(?)のオブジェクトを変更するとrgba、correct_rgba どっちも変わる(変更1) 最深層(?)ではない参照を変更するとrgba、correct_rgba 片方だけ変わる(変更2) じゃあ変更3は最深層のオブジェクト変更だから変更1と同じですね、と思ったら変更2と同じになっている。これはなぜなの? というのが分かりません。 変更1 [[101, 2], [3, 4], [5, 6]] [[101, 2], [3, 4], [5, 6]] 変更2 ['foo', [3, 4], [5, 6]] [[101, 2], [3, 4], [5, 6]] 変更3 ['Red', 'Green', 'Blue', 'Alpha'] ['Red', 'Green', 'Blue', 'Alph'] 深いところってどこ?参照、オブジェクトの順に深いの?まったく別のレイヤーがあるなら、何と何と何というレイヤーがあって、どう言う順番で深いの?というのが明確に書いてあるサイトがなかなかありません。これだけ大事なことなら明確に書いてあるサイトがあってもよさそうなものですが。
- notnot
- ベストアンサー率47% (4900/10358)
> VBの経験で値渡しと参照渡ししか知らなかったので、このあいの子のような浅いコピーがよくわかりませんでした。 まず、値渡し(call by value)や参照渡し(call by reference)は、「call by」という言葉から分かるとおり、関数やメソッドを呼び出す(callする)際の仮引数と実引数の関係を分類する概念なので、今回のように「浅いコピー」「深いコピー」「単なる代入」の話をしている際には全く関係ない概念です。 a = b のような代入を「参照渡し」と表現するのも言葉の使い方として間違っています。 > この「できるだけ参照渡し」ってのがよくわかりません。 なので、「できるだけ参照渡し」という言葉は最初から破綻していて、分からなくても問題ありません。関数・メソッドへの引数の渡し方に「できるだけ」とかありません。 >下記コードの変更3でcorrect_rgba[-1]が参照渡しでなくなってしまうのはなぜなんでしょうか? 「参照渡しでなくなる」というのが意味不明ですが、 print('変更3') rgba=["Red", "Green", "Blue", "Alph"] correct_rgba = rgba.copy() correct_rgba[-1] = "Alpha" print(correct_rgba) print(rgba) は、correct_rgba に浅いコピーをした後、コピーされた新規オブジェクト(rgbが参照しているオブジェクトとは別物)を変更しているので、コピー元であるrgbaには無関係です。 listの「浅いコピー」は概ねこう言う処理です。 def shallow_copy_of_list(x): return [y for y in x] 「深いコピー」は、単にxの各要素をyとして取り出して並べるのでは無く、yを再帰的に深いコピーしたものを並べます。 listと数値限定で書くとこんな感じでしょうか。 def deep_copy_of_list(x): if isinstance(x,list): return [deep_copy_of_list(y) for y in x] else: return x 使用例としては、 a = [[1,2],[3,4]] b = deep_copy_of_list(a) print(a,b) a[0][0] = 99 print(a,b) 浅いコピーだと、a[0]とb[0]は同一オブジェクト(idが同じ)ですが、深いコピーだと別物(b[0]はコピーで新規作成されたもの)です。 なので、 a = [[1,2],[3,4]] b = [y for y in a] # 浅いコピー print(a,b) a[0][0] = 99 print(a,b) と結果が異なります。
お礼
ご回答ありがとうございます。 >、correct_rgba に浅いコピーをした後、コピーされた新規オブジェクト(rgbが参照しているオブジェクトとは別物)を変更しているので、コピー元であるrgbaには無関係です。 申し訳ないのですがわかりません。浅いコピーではcorrect_rgbaとrgbはidが別でも同じオブジェクトを参照するのでは? 深いコピーは参照もオブジェクトも別なのでa, bが完全に独立に操作できることは理解できます。
補足
ここまで書いてみて気が付いたのですが、こういうのってnoteかqiitaに誰か書いてそうですね。探してみます。
- cametan_42
- ベストアンサー率62% (162/261)
> (2)上記リンクに「intlist1[0]へ代入をすれば、名札の付け替えが行われる」とありますが、rgb.append("Alph")、rgb[-1]="Alpha"、rgba.append("Alph")、rgba[-1] = "Alpha"のどれをやってもid変わりません。 「名札の付け替え」って表現がイマイチなんだけど、結局ポインタとか知らない人の為に易しく書こう、ってぇんでそうなってんでしょ。 実際は「名札の付替え」じゃなくって「指してるオブジェクト」を変更してる、って事だ。ポインタの参照先を変更してる。 そして「代入」はあくまで代入であって、新規な何かを生成するわけじゃあないんだ。 > rgb.append("Alph") これはrgbって言うモノが指してるリストオブジェクトに対して「追加」してるだけで、rgbをコピーしてるわけじゃない。 あくまで「元々あった」rgb対象の操作なんで、新しいリストを生成はしてない。 よってid(アドレス)は変わらない。 > rgb[-1]="Alpha" これも元々あったrgbが指してるリストオブジェクトの末尾に再代入しただけ、だ。繰り返すけど「代入」が新しいオブジェクトを生成する事はない。 よって、rgbは同じアドレスを指し続ける。 例えば次のような実験をしたかったのかしらん。 >>> rgb = ["Red"] >>> rgba = rgb + ["Alpha"] >>> rgba[:1] ['Red'] >>> id(rgba[:1]) == id(rgb) False rgbaはリストrgbとリスト["Alpha"]を結合したリストだけど、appendと違って、+と言う操作はリストを新規生成する。 結果、rgba[:1]と言うスライス操作はrgbそのものと「全く同じ内容を返す」が、idを調べてみると同一のリストを指してない事が分かる。
お礼
あとNo.1の方へのお礼欄にも書いたのですが、最後の例で変更1、2は理解できますが、変更3がなぜ「名前の付け替え」が起こるのかが理解できません。変更3では配列サイズもデータ型も変わっていない。何が原因で名前の付け替え(参照先アドレス新規作成・変更)が起きているのか。 これが知りたくて冒頭の例文を作りました。こちらをご教授頂けましたらありがたいです。
- cametan_42
- ベストアンサー率62% (162/261)
んん〜。説明が難しいよね(笑)。 正直言うと、非破壊的にプログラム書いてるとあまり意識せん話なんだ(笑)。代入を多用する人に取っては重要なんだけど、そうじゃない人に取っては割にどうでもいい話になる、っつーか・・・(苦笑)。 っつーか、この辺、本気で理解したかったら、あの悪名高い「ポインタ」の概念を知らないとなんない。そう、これはポインタの話なんだ。 > rgba = rgb(代入)は浅いコピー(参照渡し) いや、これは違う。代入は代入かな。 例えば、件のページの例に従うと、strlist1ってのは strlist1 -> [ , , ] ↓ ↓ ↓ 'foo' 'bar' 'baz' って言う「構造」になっている。矢印が「ポインタ」だ。 具体的にはstrlist1って変数にはある「リスト」が存在するアドレスが入ってるの。で、その「リスト」の各要素にも実際には「アドレス」が入っていて、それらアドレスは'foo'、'bar'、'baz'って言う「文字列」がそれぞれ存在してる場所を意味してる。 Pythonプログラム上は「リストに文字列が入ってる」ように見えるように作られてるんだけど、実際は'foo'、'bar'、'baz'って言う「文字列」はリストの中には特に存在せんのよね。あるのは「そいつらのアドレス情報だけ」なんだ。 んで、strlist3にstrlist1を「代入する」ってのは、 strlist3 -> strlist1 -> [ , , ] ↓ ↓ ↓ 'foo' 'bar' 'baz' strlist3と言う変数がstrlist1と言う変数をポインタで指してる、って事。言い換えるとstrlist3はstrlist1と言う変数が「存在する」アドレス情報を代入された、って事なんだ。結果、そのリスト自体はコピーされてない。単にstrlist3はstrlist1を指して、strlist1はそのリストを指して・・・とポインタが連鎖しているに過ぎない。変数strlist3を使う際にはポインタの連鎖を手繰って行ってそのリストへたどり着くわけだ。 > rgba = rgb.copy()(コピーメソッド)と rgba = rgb[:](スライス代入?)は深いコピー(値渡し)と理解してよいのでしょうか? いや、両者とも浅いコピーでしょ。 例えばね。浅いコピーってのは要するにこの例だと、「リストと言うガワ」を複製するわけ。その時点でポインタ、つまり「どのアドレスを指してるのか」って情報も複製されるわけだ。 ただし、そのアドレスが指してるデータ・・・OOPだとオブジェクトって言った方がいいのかしらん、それ「自体は」コピーされないんだ。 1: strlist1がある。 strlist1 -> [ , , ] ↓ ↓ ↓ 'foo' 'bar' 'baz' 2: strlist2 = strlist1.copy()はstrlist1が指してる「リスト構造」を、含んでるアドレス情報を合わせてまるっとコピーする。 strlist1 -> [ , , ] ↓ ↓ ↓ 'foo' 'bar' 'baz' ↑ ↑ ↑ strlist2 -> [ , , ] 3. ただし「メモリ上のどっかに存在する」'foo'、'bar'、'baz'って言う「文字列」本体をどっか別の場所にコピーするわけではない。 「浅いコピー」ってのは3番がキモなのね。リストの各要素が「指してる」アドレスはコピーするけど、参照先のデータを複製して別のメモリに割り当てるわけじゃないんだ。 言い換えると、「指してるオブジェクト」さえコピーして、アドレスもそれに見合ったように「新しく」生成するのを「深いコピー」って呼ぶんだよ。 intlist1 -> [ , , ] ↓ ↓ ↓ [1, 2] [3, 4] [5, 6] intlist2 -> [ , , ] ↓ ↓ ↓ [1, 2] [3, 4] [5, 6] intlist1をコピーして、リストの参照先のオブジェクトまでコピーして、別のメモリ領域を確保して、intlist2が指してるリストの「各アドレス情報」まで更新するのを「深いコピー」って呼ぶんだ。 従って、アドレスの話をすると、intlist1の[1, 2]とintlist2の[1, 2]は「別物」だ。 注: [1, 2]とか簡単に書いたけど、図を描くのが難しいんで避けたが、実際はここもリストと言う「ガワ」があって、各要素は1と言うオブジェクトと2と言うオブジェクトが「存在する」場所を指している。 また、Pythonの実装上、一旦文字列が生成されたらそれは唯一無二の存在となって、例えば"hoge"が一旦生成されればメモリ上の別の位置に"hoge"は生成出来ない。従って、文字列に対してはcopyメソッドは存在しない。 事実上、Pythonの文字列は、「文字列定数」と呼んでもいい物体になっている。 同様に、整数なんかも「一旦生成されれば」文字列と同じような不動のオブジェクトになるらしい。つまり、整数も「定数」のような性質になっている。 ちょっと実験してみよう。 >>> intlist1 = [[1, 2], [3, 4], [5, 6]] >>> intlist2 = intlist1.copy() >>> all(id(i) == id(j) for i, j in zip(intlist1, intlist2)) True intlist1を生成してintlist2をintlist1の「浅いコピー」として生成する。 さて、allってのは引数に与えられたシーケンスが全部真を返すかどうか、ってのを調べる関数だ。 all: https://docs.python.org/ja/3/library/functions.html#all ここでは要は、intlist1とintlist2の2つの先頭要素から順次調べていって、idが全部同じ(つまり要素側のリストオブジェクトのアドレスが同じ)かどうか調べている。 結果、全部同じだからTrueを返すわけだ。つまり、intlist1とintlist2の2つは[1, 2]、[3, 4]、[5, 6]って言う「リスト」を「共有してる」んだ。 一方、「深いコピー」も試してみよう。 >>> from copy import deepcopy >>> intlist1 = [[1, 2], [3, 4], [5, 6]] >>> intlist2 = deepcopy(intlist1) >>> all(id(i) == id(j) for i, j in zip(intlist1, intlist2)) False intlist1を「深いコピー」したintlist2の各要素に対して、id(アドレス)が同じかどうか調べてる。答えはFalse、だ。 しかし、allは「一個でもFalseがあれば」Falseにしちまうので、1つだけアドレスが違い、他はアドレスが同じかもしんない。 そういう場合にはallよりanyを使った方がいいだろう。 any: https://docs.python.org/ja/3/library/functions.html#any >>> any(id(i) == id(j) for i, j in zip(intlist1, intlist2)) False 「一個でも」アドレスが同じなのか、と訊くと「違う」と言われた。 つまり、ディープコピーで作ったintlist2が抱えてるリストオブジェクトの「参照先」ってのはintlist1のそれらとは「全然違う別物」って事になる。
お礼
先日はありがとうございました。MIT標準教科書、めっちゃいい本でした。他も教材が充実したので教材探しはやめて読むほうに集中しています。 まずNo.1の方のご教示で下記の整理ができました。 代入=参照渡し=そもそもコピーではない コピーメソッド、スライス代入=できるだけ参照渡し=浅いコピー というんですね。 ポインタの理解はデータのメモリ上のアドレス、配列の場合は先頭データのアドレスと覚えています。逆にそういう目で見ていたので、「先頭データのアドレスしか参照しない」「浅いコピー=まるごと参照渡し」「深いコピー=まるごと値渡し(別データ生成)」だろうという先入観があったので、まさか「配列の一部だけ値渡し、残りは参照渡し」などというあいの子みたいなものが存在して、「配列の各要素が個別にアドレス(ポインタ)持ってて浅いコピーではそれをコピーしてる」などというのは予想の斜め上を行っていました。 多分私が分かっていないのはそういう事でいいんですよね? 科学用途なのでVBで書いていたころは代入はかなり使ったので(数値計算するので)結構重要になります。あとPyOpenGL使って3Dグラフィックにも挑戦してみたいので(と言ってもきらびやかなもんではく、ワイヤーフレームやポリゴンの描画、アニメーションができる簡易CADです)、やはり数値演算は多用すると思います。(VBだからポインタ使ってません(笑))。
- notnot
- ベストアンサー率47% (4900/10358)
> (1) rgba = rgb(代入)は浅いコピー(参照渡し)、 rgba = rgb.copy()(コピーメソッド)と rgba = rgb[:](スライス代入?)は深いコピー(値渡し)と理解してよいのでしょうか? いいえ。単なる代入は同じオプジェクトを複数の変数から参照しているだけで、オブジェクトのコピーは発生しませんので、浅いコピーではないです。 「浅いコピー」「深いコピー」は「オブジェクトがどのようにコピーされるか」の区別であって、オブジェクトの参照のコピーである単なる = による代入とは関係ありません。 copy()と[:]だと、オブジェクトのコピー(新たなオブジェクトの生成とそのオブジェクトの中味をコピー元から作成)が発生します。この時は浅いコピーです。 > copy()(コピーメソッド)は浅いコピーと書いてあります。 その通りです。 > (2)上記リンクに「intlist1[0]へ代入をすれば、名札の付け替えが行われる」とありますが、rgb.append("Alph")、rgb[-1]="Alpha"、rgba.append("Alph")、rgba[-1] = "Alpha"のどれをやってもid変わりません。 これは、rgbとrgbaが参照している同じ1つのオブジェクトを加工しているだけであって、オブジェクトとしてはずっと同じです(オブジェクトの中味が変化しているだけ)ので、idも同じままです。 > 上記2点ってPythonの仕様変更ですか? このあたりは昔から同じというか、Python以外のオブジェクト指向言語でも同じです。 お書きのプログラムですが、複数パートに分かれていますが、最初に rgb = ["Red"] したきりで、パート毎でのrgba への代入もあったり無かったりなので、どのタイミングで追加した"Alph"なのかが、訳わかんない状態の結果表示となります。 各パート毎の先頭で、 rgb = ["Red"] rgba = rgb または rgba = rgb.copy() または rgba = rgb[:] の2行を毎回書くのが良いでしょう。 再度書きますが、 ・代入だけではオブジェクトの生成はありません。右辺と同じオプジェクトです ・~.copy() や ~[:] で浅いコピーオブジェクトが生成されます。それを rgba に代入すると、代入では新たなオブジェクトは生まれませんが、copy()や[:]で生まれた新オブジェクトへの参照を代入しています。
お礼
ご回答ありがとうございます。そうか。 勘違いポイント1) 代入=参照渡し=そもそもコピーではない コピーメソッド、スライス代入=できるだけ参照渡し=浅いコピー というんですね。 VBの経験で値渡しと参照渡ししか知らなかったので、このあいの子のような浅いコピーがよくわかりませんでした。 疑問ポイント2) この「できるだけ参照渡し」ってのがよくわかりません。下記コードの変更3でcorrect_rgba[-1]が参照渡しでなくなってしまうのはなぜなんでしょうか? Google colabo print('\n', '浅いコピー') intlist1 = [[1, 2], [3, 4], [5, 6]] intlist2 = intlist1.copy() print(intlist1, intlist2) print(id(intlist1), id(intlist2)) print('変更1') intlist1[0][0] = 101 print(intlist1, intlist2) print('変更2') intlist1[0] = 'foo' print(intlist1, intlist2) print('変更3') rgba=["Red", "Green", "Blue", "Alph"] correct_rgba = rgba.copy() correct_rgba[-1] = "Alpha" print(correct_rgba) print(rgba) 実行結果 浅いコピー [[1, 2], [3, 4], [5, 6]] [[1, 2], [3, 4], [5, 6]] 132583838220224 132583838214528 変更1 [[101, 2], [3, 4], [5, 6]] [[101, 2], [3, 4], [5, 6]] 変更2 ['foo', [3, 4], [5, 6]] [[101, 2], [3, 4], [5, 6]] 変更3 ['Red', 'Green', 'Blue', 'Alpha'] ['Red', 'Green', 'Blue', 'Alph']
お礼
辛抱強くご教示いただきましてありがとうございます。というよりも、全力で(調べて)質問文を書いていますが、深い「何か」、浅い「何か」の「何か」に対応する用語が見つからないため、正しい言葉で質問できず申し訳ありません。層ではなく、親・子・孫・ひ孫というんですね。 ”最浅層”(子)でわかりました。つまりこういう事ですね。 Google Colabo a = [[[[[1], 2], 3], 4], 5] b = a[:] print(a) print(b) print('a[1]=6') a = [[[[[1], 2], 3], 4], 5] b = a[:] a[1] = 6 print(a) print(b) print('a[0][1]=6') a = [[[[[1], 2], 3], 4], 5] b = a[:] a[0][1] = 6 print(a) print(b) print('a[0][0][1]=6') a = [[[[[1], 2], 3], 4], 5] b = a[:] a[0][0][1] = 6 print(a) print(b) print('a[0][0][0][1]=6') a = [[[[[1], 2], 3], 4], 5] b = a[:] a[0][0][0][1] = 6 print(a) print(b) print('a[0][0][0][0][0]=6') a = [[[[[1], 2], 3], 4], 5] b = a[:] a[0][0][0][0][0] = 6 print(a) print(b) 実行結果 [[[[[1], 2], 3], 4], 5] [[[[[1], 2], 3], 4], 5] a[1]=6 [[[[[1], 2], 3], 4], 6] [[[[[1], 2], 3], 4], 5] a[0][1]=6 [[[[[1], 2], 3], 6], 5] [[[[[1], 2], 3], 6], 5] a[0][0][1]=6 [[[[[1], 2], 6], 4], 5] [[[[[1], 2], 6], 4], 5] a[0][0][0][1]=6 [[[[[1], 6], 3], 4], 5] [[[[[1], 6], 3], 4], 5] a[0][0][0][0][0]=6 [[[[[6], 2], 3], 4], 5] [[[[[6], 2], 3], 4], 5]