2024-2I プログラミング1 第13回 講義資料

2024年07月19日(金)1・2時限

1 準備

img

2 課題5

※ 入力期間中にフォームが開けない場合は、Teamsのチャットで和田に連絡をお願いいたします。

2.1 補足

入力期間になると Microsoftフォーム にアクセスできます。

2024年08月09日~08月15日のプログラミング学習の取り組みの報告フォームです。回答は、後日、氏名付きで2I学生とコース教員に共有します。最低ラインとして毎週180分以上、理想としては毎週840分以上(1日2時間)を推奨しています。

フォームには「出席番号」と「氏名」の他に次の項目があるので、記入して送信してください。期限を過ぎると、そのフォームにはアクセスできません。

例えば、以下のような感じで、具体的に記入してください。

TechFULの「プラクティス:プログラミング基礎」について、Pythonを使って「標準入出力」「変数」「型基礎」「算術演算」で合計21問をクリアした。「型」についてYouTube動画やウェブ記事で学習した。総合課題学習のプロジェクト「XXXX」のYYY機能の実装のために、YYについて調べて、実装を進めた(TypeScript)。YYY機能の設実装に関して進捗率が20%から50%になった。YouTubeでスクレイピング、HTTP関連の動画を5本程視聴した。

Pythonの関数についてウェブとYouTubeで理解を深めた(合計5時間ぐらい)。関数の定義(ラムダ式を含む)、位置引数、キーワード引数、可変長引数について知ることができた。また、タプルを使って戻値を複数個にする方法と、それを受け取る方法についても学んで、実際にプログラムを書いて確かめた。Gitについて調べて、コマンドラインから操作を試したがうまくいかかった(3~4時間ぐらい)。

GitHub Education について調べて申請した。VSCodeに「GitHub Copilot」の拡張機能をインストールしてコードの補完機能を使ってみた(合計3時間)。「苦しんで覚えるC言語」というウェブサイトでC言語の勉強をはじめた。3章の「画面への表示」まで進めた(2時間ぐらい)。VSCodeでC言語の環境環境の構築したが思った以上に苦戦して、とりあえず paiza.io で開発することにした。

3 プログラミングを上達させるためには

プログラミングは「語学」や「スポーツ」「楽器演奏」と同じく「方法や理論のレクチャーを受けるだけ」「解説を読むだけ」では、いつまでもモノにはなりません (=頭のなかに描いた「やりたいこと」を、プログラムとして自由に記述できるレベルにはなりません) 。語学、スポーツ、楽器演奏のように、実際に繰返し手を動かして試行錯誤しながら、その感覚をつかむ必要があります。

例えば、バスケにおいて「コーチからレイアップシュートの指導を受けて、その後、レイアップシュートを成功させることができた」という事実から「俺はレイアップシュートを完全に習得した (これ以上は練習する必要はないし、試合でもレイアップシュートで得点をとれる)」とか思ってしまう学生がいたら、かなり問題ですよね ?

これは、プログラミングでも同じで「Pythonの辞書型についての講義資料を読んで理解し、そのあとの演習課題もクリアした」からといって、それが身に付いたことにはなりません。実際に、繰返して使うことで、その結果として辞書型が身に付きます。この事実をしっかりと意識しておくようにしてください。

4 リストに格納な可能な変数の型

C言語における「配列 (Array) 」では、以下のように「配列の要素を 全て同じ型で統一 する必要」がありました。

#include <stdio.h>
int main(void){
  // arr は int型のみを「要素」にする必要がある
  int arr[] = {10,20,30,40};
}

一方で、Pythonの「リスト (List)」では、要素として 異なる型を混在させること が可能です。

実際に、以下に示すPythonプログラムのリスト arr には「整数型」「文字列型」「真偽型 (ブール型)」「リスト型」「辞書型」を混在させていますが、問題なく動作します。実際に動作と結果を確認してください。

特に、ここではリスト arr の内部要素としてリスト [66,77,88] を持つことができる点に着目してください (このことがPythonで 2次元リスト を実現する仕組みになります) 。

なお、第04行目enumerate については 第12回講義 で既に学習済みです。第05行目type については 第12回講義 で既に学習済みです。

%reset -f
arr = [ 52, 'ABC', True, [66,77,88], {'秀':5, '優':4, '良':3, '可':3} ]

for i,a in enumerate(arr):
  print(f'arr[{i}] の内容は {str(type(a)):<14} 型の {a} です。')

実行結果

arr[0] の内容は <class 'int'>  型の 52 です。
arr[1] の内容は <class 'str'>  型の ABC です。
arr[2] の内容は <class 'bool'> 型の True です。
arr[3] の内容は <class 'list'> 型の [66, 77, 88] です。
arr[4] の内容は <class 'dict'> 型の {'秀': 5, '優': 4, '良': 3, '可': 3} です。

ここで、arr[3] に格納されている [66,77,88]77 という数値を取得するためには arr[3][1] のように記述します。また、arr[4] の辞書型が持つ 3 という数値を取得するためには arr[4]['良'] のように記述します。

4.1 演習1 ( 目標時間: 8分)

期待する出力が得られるように、次のプログラムを追記してください。第03行目第04行目assert第11回講義 で既に学習済みです。

%reset -f
arr = [ [55,66,77,88], {'秀':5, '優':4, '良':3, '可':2} ]
assert type(arr[0]) == list
assert type(arr[1]) == dict
# ここから先にコードを追記する

期待する出力

arr[0][0] の内容は 55 です。
arr[0][1] の内容は 66 です。
arr[0][2] の内容は 77 です。
arr[0][3] の内容は 88 です。

arr[1]['秀'] の内容は 5 です。
arr[1]['優'] の内容は 4 です。
arr[1]['良'] の内容は 3 です。
arr[1]['可'] の内容は 2 です。

なお、ここでは次のようなプログラムを期待しているわけではありません。arrfor の組み合わせで対応する方法を考えてください。ヒント1ヒント2

%reset -f
arr = [ [55,66,77,88], {'秀':5, '優':4, '良':3, '可':2} ]
assert type(arr[0]) == list
assert type(arr[1]) == dict

msg='''\
arr[0][0] の内容は 55 です。
arr[0][1] の内容は 66 です。
arr[0][2] の内容は 77 です。
arr[0][3] の内容は 88 です。

arr[1]['秀'] の内容は 5 です。
arr[1]['優'] の内容は 4 です。
arr[1]['良'] の内容は 3 です。
arr[1]['可'] の内容は 2 です。
'''
print(msg)

5 二次元リスト

C言語 (Arudino言語) では次のようなコードで「3行4列」の 2次元配列 を扱うことができました。

#include <stdio.h>
int main(void){
  int i,j;
  int mat[3][4] = { { 10, 20, 30, 40},
                    { 50, 60, 70, 80},
                    { 90,100,110,120} };                   
  for(i=0;i<3;i++){
    for(j=0;j<4;j++){
      printf("mat[%d][%d]=%d\n",i,j,mat[i][j]);
    }
  }
}

この「C言語プログラム」の実行結果は次のようになります。

mat[0][0]=10
mat[0][1]=20
mat[0][2]=30
mat[0][3]=40
mat[1][0]=50
mat[1][1]=60
mat[1][2]=70
mat[1][3]=80
mat[2][0]=90
mat[2][1]=100
mat[2][2]=110
mat[2][3]=120

これと同等の「Pythonプログラム」は次のように記述することができます。なお、(1次元の)リストの「初期化」については 第08回講義 で既に学習済みです。

%reset -f
# mat: Matrix(行列) 2次元リストの初期化
mat = [[10, 20, 30, 40],
       [50, 60, 70, 80],
       [90,100,110,120]]

# row: Row(行)
for i,row in enumerate(mat):
  for j, e in enumerate(row) :
    print(f'mat[{i}][{j}]={e}')

上記のプログラムが十分に理解できない場合、まずは次のように プログラムの途中に print 文を挿入するなど、自分で手を動かし、動作を確認・理解するように努めてください

%reset -f
# mat: Matrix(行列)
mat = [[10, 20, 30, 40],
       [50, 60, 70, 80],
       [90,100,110,120]]

# row: Row(行)
for i,row in enumerate(mat): 
  print(f'mat[{i}]={row}') # 追加:ループ変数 i と row の内容を確認
  # for j, e in enumerate(row) :    # 不明な範囲はコメントアウト
  #   print(f'mat[{i}][{j}]={e}')   # 不明な範囲はコメントアウト

5.1 C言語との違い

C言語で2次元配列を構成した場合、行の要素数 (=列の長さ) を全て同じにする必要 がありました。一方、Pythonの2次元リストでは、行ごとの要素数が異なっていても問題ありません。

次のプログラムを実行し、行ごとに要素数が異なっていても問題ないことを確認してください。

%reset -f
arr2 = [[10, 20],          # 要素数2
        [10, 20, 30, 40],  # 要素数4
        [10, 20, 30]]      # 要素数3

for i,row in enumerate(arr2):
  for j, e in enumerate(row) :
    print(f'mat[{i}][{j}]={e}')

5.2 2次元配列の様々な初期化の方法

次の各プログラムは、どれも同じように2次元リスト mat を初期化します。いずれの初期化方法も読み書きできるようになっておいてください (自分では使わない初期化方法でも、プロジェクトメンバーの誰かが使う可能性があります) 。

なお、どの方法で初期化するのが最善であるかは状況によって異なります。また、第10回講義のリスト操作の負荷量の比較についても、再度確認しておいてください。

プログラムは眺めだけでは身に付かないので、少なくとも貼り付けして、実行してください。

%reset -f
mat = [[10, 20, 30],[40, 50, 60]] # 行単位の改行は必須ではない
print(mat)
%reset -f
mat = [] # 長さ0のリスト (空のリスト) を作成
mat.append([10,20,30]) # 要素にリストを追加
mat.append([40,50,60])
print(mat)
%reset -f
mat = [0]*2  # 長さ2のリストを作成。[1]*2 でも [None]*2 でもよい
mat[0] = [10,20,30] # 0番目の要素を上書き
mat[1] = [40,50,60]
print(mat)
%reset -f
mat = [ [0]*3, [0]*3 ]  # mat = [[0]*3]*2 とすると予期せぬ結果
# print(mat)
mat[0][0] = 10
mat[0][1] = 20
mat[0][2] = 30
mat[1][0] = 40
mat[1][1] = 50
mat[1][2] = 60
print(mat)
%reset -f
mat = [ [0]*3, [0]*3 ]  # mat = [[0]*3]*2 とすると予期せぬ結果
for i in range(6):
  mat[i//3][i%3] = (i+1)*10
print(mat)

特に list_init4.py において mat = [ [0]*3, [0]*3 ] ではなく mat = [[0]*3]*2 のように初期化すると 予期せぬ結果 になることを 実際に確認 (超重要) してください。

6 リストの浅いコピーと深いコピー

実用的なプログラムを作成する場合、リストや辞書の利用は必要不可欠です。そして、リストや辞書を使用する場合に、十分に理解しておかないとハマるもの (詰むもの) として 浅いコピー/深いコピー という概念があります。

また、またそれと密接に関連する概念として オブジェクトIDバインド(束縛)ミュータブル/イミュータブル というものがあります。

C言語の学習では「ポインタ」という概念/仕組みの壁を超えられずに脱落・挫折する人が多いことがよく知られています。同様にPythonの「浅いコピー/深いコピー」や「オブジェクトID」も 初学者にとって大きな壁 となります。一旦、理解してしまえば何ということもない概念・仕組みなのですが、初めて学ぶときには「非常に難解」「理解不能」という印象を受けると思います。しかし、ここの壁を超えて本質的な理解を得ないと今後の開発や学習に多大な支障が生じることになります (例えれば 分数や小数の本質を理解しないままに中学数学に取り組むようなもの です) 。

ここからの先の内容は 多くの人にとって簡単に理解できるものではない、少なくとも 数時間、2週間~1が月程度 は悩むものという前提で挑んでください (1時間悩んで理解できないからと諦めないでください、2・3日の時間をあけて見直すことで理解できることも多々あります) 。取り組む際のポイントは 頭で悩む (考える) だけではなくて「調べる」と「試す」をバランスよく組み合わせる ことです。

また、この講義資料だけではなくウェブやYouTubeなどの様々な資料や解説、生成AIなどもあわせて理解に努めてください。

アドバイス

ここから先、これまでに皆さんが頭のなかに築いてきた「プログラムの動作モデル」を破壊再構築してもらうような内容となります。例えれば「昼と夜があるのは太陽が動くから」と思っている幼稚園児や小学生に「昼と夜があるのは、実は太陽が動くからじゃなくて 、地球が自転してるから」と理解してもらう、それを受け入れてもらうような内容になります。

「地球は平面であり、また動いているのは太陽である」という理解のほうが直感的であり、また、その解釈であっても (身近な生活に関わるようなことは) ほとんど矛盾なく説明ができてしまいます。それゆえに「意味不明」と思考停止し、理解の追及を諦めてしまう子供もいます。

6.1 理解のためのステップ1

次のプログラムを実行したときの 第20行目の実行結果 (出力) について考えてみてください。そのうえでプログラムを実行し、「実際の実行結果 (出力)」と「自分の考えた実行結果 (出力)」を比較してみてください。

%reset -f

# a の初期化
a = [10,20,30]

# b に a をコピー (ここがポイントです)
b = a

# a と b の内容を確認
print(f'a={a}') # => a=[10, 20, 30]
print(f'b={b}') # => b=[10, 20, 30]

assert a == b   # a と b が「等しいこと」を念のために確認 

# a の2番目の要素を変更
a[2] = -1

# a と b の内容を確認
print(f'a={a}') # => a=[10, 20, -1]
print(f'b={b}') # => ??? どのうような出力を得るか ???

今度は、上記の 第07行目b=ab=[10,20,30] に書き換えて、その実行結果を確認してください。

なぜ、このような結果の違いが生じるのか」を、ここから順を追って考え、丁寧に謎を解明していきます。

まず、「ここで不可解と感じること」は上記の step1-1.py第16行目 で、a[2] = -1 のように リストaを対象に要素の書き換えをしている はずが、どういうわけか リストbに対しても影響を与えている ことだと思います。

このようなことは、少なくとも以下の step1-2.py のように 変数 a整数値を代入するケースでは生じていませんでした (実際に実行して確かめてください)。

%reset -f
a = 30 # リストではなく整数値
b = a

# a と b の内容を確認
print(f'a={a}') # => a=30
print(f'b={b}') # => b=30

assert a == b   # a と b が「等しいこと」を念のために確認 

# 変数 a に変更をくわえる
a = -1

# a と b の内容を確認
print(f'a={a}') # => a=-1
print(f'b={b}') # => b=30  ここでは b は影響を受けていない。

まずは、リストが関係するとき「だけ」に生じる不思議な挙動を「しっかりと認識する」ために、以下の step1-3.py を使って実験してみます。ここまでのことを踏まえて、以下のプログラムの実行結果を十分に検討・予想したうえで、実行して、その結果を確認してください。

%reset -f
a = [10,20,30]
b = a
c = a
a[0] = -1
b[1] = -2
c[2] = -3
print(f'a={a}') # 出力はどうなるか
print(f'b={b}') # 出力はどうなるか
print(f'c={c}') # 出力はどうなるか

上記 step1-3.py第03行目以降を色々と書き換え (例えば、第04行目c=a から c=b に書き換えるなどして) 、その挙動について考察してみたり、仮説を立ててみたりしてください。現段階では「なぜ、このような結果となるか」ではなく、まずは「(仕組みはブラックボックスでかまわないので、結果として) どのような結果となるか」を考えてみてください。

また、次の step1-4.py のようにリスト a に対して pop() で「要素数を減らす操作」をするとどうなるか、確認してみてください。

%reset -f
a = [10,20,30]
b = a
c = a
a.pop() # 末尾の要素を削除
print(f'a={a}') # => [10,20]
print(f'b={b}') # 出力はどうなるか
print(f'c={c}') # 出力はどうなるか

6.2 理解のためのステップ2

(十分に手を動かして結果について分析すれば) 以下の step2-1.py ようなプログラムでは、第05行目以降で変数 a に対して何らかの操作をすると「変数 b と 変数 c に対しても同じ操作が連動して適用されるような挙動を示すのではないか」あるいは「(連動しているのではなく) 変数 abc の実体は同じモノではないのか (Windowsファイルシステムのショートカットのように、ひとつの実体に対して複数のアクセス手段を持っている状態になっているのではないか)」といった仮説が立てられると思います。

%reset -f
a = [10,20,30]
b = a
c = a

ところで、次の step2-2.py ようなプログラムは試したでしょうか。結果を予測したうえで、実際に結果を確認してみてください。

%reset -f
a = [10,20,30]
b = a
c = a
a = [60,70,80] # a[0]=1 や a.pop() のような操作ではない点に注意!!
print(f'a={a}') # => [60,70,80]
print(f'b={b}') # 出力はどうなるか
print(f'c={c}') # 出力はどうなるか

これにより さらに混乱してきた と思います。

6.3 理解のためのステップ3

ここまでの状況を整理してみます。まず、次の step3-1a.pystep3-1b.py のように変数 a に代入しているものが「整数型」や「文字列型」のときは直感に反しない動作となります。ここでは、念のために assert を使って ab が「等しくないこと」も確認しています。以下のプログラムを実際に実行して結果を確認してください。

%reset -f
a = 10 # aに代入するのは「整数型」
b = a
a = a+1
assert a != b
print(f'a={a}') # => a=11
print(f'b={b}') # => b=10
%reset -f
a = 'ABC' # aに代入するのは「文字列型」
b = a
a = a+'DEF'
assert a != b
print(f'a={a}') # => a=ABCDEF
print(f'b={b}') # => b=ABC

また、変数 a に代入しているものが「リスト」であっても、次の step3-2a.py のように 第04行目a = [10,10,30] とした場合は直感に反しない動作 (= 変数bは影響を受けていない結果 ) となります。実際に実行して結果を確認してください。

%reset -f
a = [10,20,30]
b = a
a = [10,10,30] # a[2]が10
assert a != b
print(f'a={a}') # => [10,10,30] 
print(f'b={b}') # => [10,20,30]

しかし、上記 step3-2a.py第04行目a = [10,10,30] から a.pop()a[1]=10 に書き換えると、アサート文に引っかかるようになってしまいます (つまり ab は「等しい」 と判定されます) 。ここでも、絶対に面倒がらずに実行して結果を確認してください。

%reset -f
a = [10,20,30]
b = a
a.pop() # あるいは a[1]=10
assert a != b
print(f'a={a}')
print(f'b={b}') # ???

そして、上記の step3-2b.py第05行目 のアサート文をコメントアウトして 第07行目 で変数 b の値の出力すると、まるで「a に対する操作 ( a.pop()a[1]=10 ) が b に対しても連動して適用されている」あるいは「ab同じ実体を指すショートカットのように機能している」といえる結果となります。

一方で、step3-2b.py第03行目b=a から b = [10,20,30] にすれば、ab は独立した変数として振る舞うようになります (アサート文にもひっかかることがありません)。

%reset -f
a = [10,20,30]
b = [10,20,30] # ここを書き換えた
a.pop()
assert a != b
print(f'a={a}') # => a=[10, 20]
print(f'b={b}') # => b=[10, 20, 30]

以上のようにPythonプログラムが振る舞ることは、どのように説明をつけることができるのでしょうか

6.4 理解のためのステップ4

結論から言えば、次のような解釈から step3-2b.py のようなPythonプログラムの振る舞いを全て説明することができます。

  1. Pythonにおいて「データを抽象的に表したもの」を オブジェクト とよび、具体的には「整数」「実数」「文字列」「リスト」「辞書」…のように、変数に代入可能な全てのものは「オブジェクト」に位置づけられます。例えば 10'ABC'[10,20,30]True も全て変数に代入可能なデータであり、したがってオブジェクトとなります。
  2. プログラム実行中に新たにオブジェクトが作成されると、そのオブジェクトには オブジェクトID という識別子 (番号) が自動的に割り振られます。この「オブジェクトID」は、他のオブジェクトIDとは重複しない固有の整数値 (int型) であり、それは「オブジェクト」と1対1の関係になります。
  3. プログラム内部では「オブジェクトID」から「オブジェクトの本体 (つまりデータ)」にアクセスすることができます。
  4. プログラミング初学者に対して、a=10 について「 a という箱に 10 という値を入れる」と解釈するように説明することが多いですが、実は正しくありません (実際、この解釈では step3-2b.py のような実行結果になることを説明できません)。
  5. a=10 については10 というオブジェクトを生成し、その「オブジェクトID」を変数 a に格納する」 が、より正確な説明・解釈となります。
  6. 同様に print(a) は「変数 a に格納されている値を表示する」ではなく「変数 a に格納されている「オブジェクトID」からアクセスできる「オブジェクト」の値を表示する」 というのが、より厳密な説明になります。
  7. このように、変数に対して (オブジェクトそのものではなく) オブジェクトを一意に特定できるオブジェクトIDを格納することを バインド と表現します。バインドとは「束縛する、結びつける、紐づける」という意味で、ここでは「変数」と「オブジェクト」を紐づけするという意味で使われます。
  8. 以上より a=10 は「変数 a10 を格納する」ではなく「変数 a と、10 というオブジェクトを (オブジェクトIDを通して) バインドする (紐づけする)」と解釈するのが適切です。

バインドについては、実は 第02回講義 でも触れていました。

中級者向け: 厳密には「代入」ではなく「バインド(束縛)」

Pythonにおいて city='堺' という文は 厳密にいえば「変数 city を『堺』という文字列 (オブジェクト) にバインド (束縛)する」という意味になります。詳しくは 小山高専・技術支援室 を参照してください。

6.4.1 具体的なプログラムで解説①

次のプログラムを使って動作を具体的に解説していきます。

%reset -f
a = [10,20,30]
b = a
a[1]=40
print(f'a={a}') # => a=[10, 40, 30]
print(f'b={b}') # => b=[10, 40, 30]

第02行目 : [10,20,30] というオブジェクトを生成し、その「オブジェクトID」を変数 a に格納する。
厳密には 10 というオブジェクトを生成しその「オブジェクトID」を0番目の要素、20 というオブジェクトを生成しその「オブジェクトID」を1番目の要素、30 というオブジェクトを生成しその「オブジェクトID」を2番目の要素とするリスト型 のオブジェクトを生成し、その「オブジェクトID」を変数 a に格納する。

第03行目 : 変数 a に格納されている「オブジェクトID」をコピー (複製) して変数 b に格納する。ここで a から bコピー (複製) されたのは「オブジェクト」ではなく「オブジェクトID」であることに注意する。この結果、変数 a と 変数 b に格納されている「オブジェクトID」は同じものとなった。

第04行目 : 40 というオブジェクトを生成し、その「オブジェクトID」を「変数 a に格納されているオブジェクトIDに紐づくリスト型のオブジェクト」の1番目の要素に上書きする。

第05行目 : 変数 a に格納されている「オブジェクトID」に紐づいたリスト型オブジェクトの値を表示する。

第06行目 : 変数 b に格納されているオブジェクトID (=変数 a に格納されている「オブジェクトID」と同じ) に紐づいたリスト型オブジェクトの値を表示する。

以上の解釈・説明によって、第06行目の出力が b=[10, 40, 30] となることが自然に説明できました。

6.4.2 具体的なプログラムで解説②

次のプログラムを使って動作を具体的に解説していきます。

%reset -f
a = [10,20,30]
b = [10,20,30]  # ここを書き換えた
a[1]=40
print(f'a={a}') # => a=[10, 40, 30]
print(f'b={b}') # => b=[10, 20, 30] # ここの結果が異なる

第02行目 : [10,20,30] というオブジェクトを生成し、その「オブジェクトID」を変数 a に格納する。

第03行目 : 新たに [10,20,30] というオブジェクトを生成し、そのオブジェクトIDを変数 b に格納する。第02行目で生成されたオブジェクトと、この第03行目で生成されたオブジェクトは (値は同じであるが) 別物なので、当然、そのオブジェクトIDは違う。

第04行目 : 40 というオブジェクトを生成しその「オブジェクトID」を、「変数 a に格納されているオブジェクトIDに紐づくリスト型オブジェクト」の1番目の要素に格納する。

第05行目 : 変数 a に格納されている「オブジェクトID」に紐づいたリスト型のオブジェクトの値を表示する。

第06行目 : 変数 b に格納されているオブジェクトID (=変数 a に格納されている「オブジェクトID」とは違う) に紐づいたリスト型のオブジェクトの値を表示する。

以上の解釈・説明によって、第06行目の出力が b=[10, 20, 30] となることが自然に説明できました。

6.4.3 具体的なプログラムで解説③

次のプログラムを使って動作を具体的に解説していきます。

%reset -f
a = 10
b = a
a = a+1
print(f'a={a}') # => a=11
print(f'b={b}') # => b=10

第02行目 : 10 というオブジェクトを生成し、その「オブジェクトID」を変数 a に格納する。

第03行目 : 変数 a に格納されている「オブジェクトID」を変数 b にコピー(複製)する。

第04行目 : 変数 a に格納されている「オブジェクトID」に紐づくオブジェクト (つまり 10 ) と、1 を加算することで、新たなに 11 という新たなオブジェクトを生成し、その「オブジェクトID」を変数 a に格納する。

第05行目 : 変数 a に格納されている「オブジェクトID」に紐づいたオブジェクトの値を表示する。

第06行目 : 変数 b に格納されている「オブジェクトID」に紐づいたオブジェクトの値を表示する。

6.5 動作の解明のためのツール id()

オブジェクトIDは id() という組み込み関数で調べることができます。

%reset -f
a = [10,20,30]
b = a # 変数aに格納されているオブジェクトIDを複製して、変数bに格納する
print(f'id(a) => {id(a)}')
print(f'id(b) => {id(b)}')

print(f'a==b         => {a==b}')
print(f'id(a)==id(b) => {id(a)==id(b)}')
%reset -f
a = [10,20,30]
b = [10,20,30] # 新たに[10,20,30]が生成され、そのオブジェクトIDが変数bに格納される
print(f'id(a) => {id(a)}')
print(f'id(b) => {id(b)}')

print(f'a==b         => {a==b}')          # True
print(f'id(a)==id(b) => {id(a)==id(b)}')  # False

6.5.1 オブジェクトIDからオブジェクトを参照する

オブジェクトIDからオブジェクトを参照する方法を紹介しますが、通常のプログラムでは利用しません。非推奨です。あくまで、動作を理解するための実験に使ってください。

%reset -f
import _ctypes

x=10
print(f'id(x) = {id(x)}')
print(f'type(id(x)) = {type(id(x))}')

# id(x)からオブジェクトを取得
X=_ctypes.PyObj_FromPtr(id(x))
print(f'id(x)から取得したオブジェクト => {X}')

6.6 理屈は分かったものの不便ではないか

プログラムのなかで、あるリストを複製して、それを少しだけ変更して使いたい場合があります (当然ながら、複製元には影響を与えずに) 。そのようなとき 単純に b=a のようにリストをコピーする のではダメであることを学びました。

そのような目的のためには、次のようなプログラムを書く必要があります。

%reset -f
a = [10,20,30]

b = [0]*len(a) # aと同じ要素数でbを初期化
for i in range(len(a)): # 要素を1個1個コピー
  b[i]=a[i]

assert a==b          # オブジェクトの値は同じだが
assert id(a)!=id(b)  # オブジェクトとしては別物

b[1]=10  # 複製したものを部分変更

print(f'a={a}') # => a=[10, 20, 30]
print(f'b={b}') # => b=[10, 10, 30] # 複製先だけが変更されている

ただし、上記の copy1.py のようなプログラムは2次元リスト、3次元リスト、あるいは、辞書を要素とする場合には、その階層分だけ複製処理を繰返す必要があります。

例えば、次の copy1-a.py では意図するコピーができていません。

%reset -f
a = [[10,20],[30,40]] # 2次元リスト

# 詰めの甘いコピー
b = [0]*len(a)
for i in range(len(a)):
  b[i]=a[i]

b[1][0]=10  # 複製したものを部分変更

print(f'a={a}') # => a=[[10, 20], [10, 40]] # 複製元も影響を受けた
print(f'b={b}') # => b=[[10, 20], [10, 40]]

この問題は、次の copy1-b.py ように解決する必要があります。

%reset -f
a = [[10,20],[30,40]]

# 深く奥までコピーする
b = [0]*len(a)
for i in range(len(a)):
  b[i] = [0]*len(a[i])
  for j in range(len(a[i])):
    b[i][j]=a[i][j]

b[1][0]=10  # 複製したものを部分変更

print(f'a={a}') # => a=[[10, 20], [30, 40]] 
print(f'b={b}') # => b=[[10, 20], [10, 40]] # 複製先だけが変更されている

しかし、これは階層が増えてくると明らかに面倒です (バグの温床にもなります)。このようなときは copy モジュールを使用し、以下の copy2.py のように記述します。こちらは、2次元リスト、3次元リスト、あるいは、辞書を要素とする場合でも問題なく対応してくれます。

%reset -f
import copy # 要import
a = [[10,20],[30,40]]
b = copy.deepcopy(a) # この1行で深いコピー

b[1][0]=10  # 複製したものを部分変更

print(f'a={a}') # => a=[[10, 20], [30, 40]]
print(f'b={b}') # => b=[[10, 20], [10, 40]]

ここで b=a のようなオブジェクトIDだけのコピーを 浅いコピー (Shallow Copy)、それに対してオブジェクトそのものの複製をつくることを 深いコピー (Deep Copy) と言います。