1 準備
- GoogleColab.
にログイン、もしくは、ローカル環境の Jupyter
を起動し、「
PG1-第13回講義.ipynbという名前でノートブックを作成しておいてください。
- 課題2 として制作してもらった作品 (タートルグラフィックス) をGoogleClassroomで公開しています。クラスメイトが、どのような作品をつくったのか確認しておいてください。また、これらの作品は知能情報コースの教員と、担任の先生、3年生にも共有しています。
2 課題5
- 今回が「前期末試験前の最終授業」となります。そして、次回の授業は
9月20日(金) で 約2カ月のブランク(空白期間)
が生じることになります。
- 語学やスポーツと同様に、プログラミングでも初心者が2カ月ものブランクをあけると、未学習に近い状態まで巻き戻ってしまいます。それは皆さんも教員も望まないはずです。そのため、夏休み中にも、言語や内容・方法・スタイルは問いませんので、週あたり 最低でも3時間 は何かしらプログラミングに取り組んでほしいです。
- とはいえ、人間、何も縛りがないとついついやらずじまいになってしまいます。わずかでも、強制力を働かせるために、課題5 として、週単位で「プログラミングの学びの報告」をしてもらい、それを課題点として成績に反映させる方法をとります。
- 08/09(金) ~ 08/15(木) の取組み \(\to\) 課題5-学習報告①入力期間 08/16 ~ 08/22
- 08/16(金) ~ 08/22(木) の取組み \(\to\) 課題5-学習報告②入力期間 08/23 ~ 08/29
- 08/23(金) ~ 08/29(木) の取組み \(\to\) 課題5-学習報告③入力期間 08/30 ~ 09/05
- 08/30(金) ~ 09/05(木) の取組み \(\to\) 課題5-学習報告④入力期間 09/06 ~ 09/13
- 09/06(金) ~ 09/13(金) の取組み \(\to\) 課題5-学習報告⑤入力期間 09/14 ~ 09/19
※ 入力期間中にフォームが開けない場合は、Teamsのチャットで和田に連絡をお願いいたします。
2.1 補足
入力期間になると Microsoftフォーム にアクセスできます。
2024年08月09日~08月15日のプログラミング学習の取り組みの報告フォームです。回答は、後日、氏名付きで2I学生とコース教員に共有します。最低ラインとして毎週180分以上、理想としては毎週840分以上(1日2時間)を推奨しています。
フォームには「出席番号」と「氏名」の他に次の項目があるので、記入して送信してください。期限を過ぎると、そのフォームにはアクセスできません。
- この期間の総取り組み時間を記入してください。単位は「分(min)」で記入してください。
- この期間の取組み内容を具体的かつ客観的に報告してください。最低180文字以上を記入していないと加点対象になりません。
例えば、以下のような感じで、具体的に記入してください。
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) 」では、以下のように「配列の要素を 全て同じ型で統一 する必要」がありました。
一方で、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]['良']
のように記述します。
arr[3][1]やarr[4]['良']という記述によって、実際に77と3が取得できることを確認してください (printで実際に値を出力してみてください)。- ヒント:
print(f'arr[3][1] の内容は { } です。')の{と}に適切な変数を記述する。 - ヒント:
print(f'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 です。
なお、ここでは次のようなプログラムを期待しているわけではありません。arr
と for の組み合わせで対応する方法を考えてください。ヒント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 = [] # 長さ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=a を
b=[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
に対しても同じ操作が連動して適用されるような挙動を示すのではないか」あるいは「(連動しているのではなく) 変数 a と
b と c の実体は同じモノではないのか
(Windowsファイルシステムのショートカットのように、ひとつの実体に対して複数のアクセス手段を持っている状態になっているのではないか)」といった仮説が立てられると思います。
ところで、次の 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.py や step3-1b.py のように変数
a
に代入しているものが「整数型」や「文字列型」のときは直感に反しない動作となります。ここでは、念のために
assert を使って a と b
が「等しくないこと」も確認しています。以下のプログラムを実際に実行して結果を確認してください。
%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
に書き換えると、アサート文に引っかかるようになってしまいます
(つまり a と b
は「等しい」 と判定されます)
。ここでも、絶対に面倒がらずに実行して結果を確認してください。
%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
に対しても連動して適用されている」あるいは「a
と b
が同じ実体を指すショートカットのように機能している」といえる結果となります。
一方で、step3-2b.py の 第03行目
を b=a から b = [10,20,30]
にすれば、a と b
は独立した変数として振る舞うようになります
(アサート文にもひっかかることがありません)。
%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プログラムの振る舞いを全て説明することができます。
- Pythonにおいて「データを抽象的に表したもの」を
オブジェクト
とよび、具体的には「整数」「実数」「文字列」「リスト」「辞書」…のように、変数に代入可能な全てのものは「オブジェクト」に位置づけられます。例えば
10も'ABC'も[10,20,30]もTrueも全て変数に代入可能なデータであり、したがってオブジェクトとなります。 - プログラム実行中に新たにオブジェクトが作成されると、そのオブジェクトには オブジェクトID という識別子 (番号) が自動的に割り振られます。この「オブジェクトID」は、他のオブジェクトIDとは重複しない固有の整数値 (int型) であり、それは「オブジェクト」と1対1の関係になります。
- プログラム内部では「オブジェクトID」から「オブジェクトの本体 (つまりデータ)」にアクセスすることができます。
- プログラミング初学者に対して、
a=10について「aという箱に10という値を入れる」と解釈するように説明することが多いですが、実は正しくありません (実際、この解釈ではstep3-2b.pyのような実行結果になることを説明できません)。 a=10については「10というオブジェクトを生成し、その「オブジェクトID」を変数aに格納する」 が、より正確な説明・解釈となります。- 同様に
print(a)は「変数aに格納されている値を表示する」ではなく「変数aに格納されている「オブジェクトID」からアクセスできる「オブジェクト」の値を表示する」 というのが、より厳密な説明になります。 - このように、変数に対して (オブジェクトそのものではなく) オブジェクトを一意に特定できるオブジェクトIDを格納することを バインド と表現します。バインドとは「束縛する、結びつける、紐づける」という意味で、ここでは「変数」と「オブジェクト」を紐づけするという意味で使われます。
- 以上より
a=10は「変数aに10を格納する」ではなく「変数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 具体的なプログラムで解説③
次のプログラムを使って動作を具体的に解説していきます。
第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)}') # False6.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) と言います。