Open In Colab

2. Pythonの基本 その2:#

[この章の目的] プログラミングで非常に重要な概念である配列や繰り返し処理について学ぶ。

2.1. リスト#

実際にいろんなデータを扱う際には、値や変数をまとめて処理したくなる。こうしたときに必要になるのが以下で扱うリスト型を始めとする配列である。

リストは、値や変数などを角括弧[ ]で括り、要素をカンマで区切ることで作ることができる。
例:

[1.0, 2.0, 3.0]

リスト自体に名前をつけて変数にすることも可能で、以下のようにする。

heights = [178.0, 180.0, 153.0]

# 変数heightsをprint
print(heights)

# 変数heightsの型をprint
print("変数heightsの型(type)は", type(heights))
[178.0, 180.0, 153.0]
変数heightsの型(type)は <class 'list'>

リストは、後で扱うNumPyの配列などに変換することもできる。ただし型が違えば使える操作も違う。まずは基本となるリストをしっかり理解しよう。

変数名に関する注釈

一時的な変数を使う場合はatmpといった意味のない変数名でも十分だが、 自身でコードを書くときはなるべく後で自分や他人が見たときになんの変数のつもりか分かるような変数名にしよう。※5日後の自分も他人のようなものなので、後で見たときに何の変数か分からないような変数名は避けるべきである(といいつつ、資料では意味のない変数名が度々登場する)。

リストの要素にできるのは数値だけではなく、文字列のリストも作ることができる。

names = ["Aさん", "Bさん", "宇大太郎さん"]

リストに入っている要素の数は、len()関数(lenはlengthの略)で見ることができる。

print("リストnamesは", names)
print("長さは", len(names))
リストnamesは ['Aさん', 'Bさん', '宇大太郎さん']
長さは 3

当然、長さに名前をつけて適当な変数として定義することもできる。 関数の出力の結果を何度も後で再利用する場合などは、このように変数に代入しておくと便利。

ln_a = len(names)
print("リストnamesの長さは", ln_a, "で、型は", type(ln_a))
リストnamesの長さは 3 で、型は <class 'int'>

文字列と数値を組み合わせたリストも作ることができる。 例:名前、身長、体重 =>

["Aさん", 178, 66]

また、これを拡張して、入れ子に(リストのリストを作成)することも。

 [ ["Aさん", 178,66], ["Bさん", 180, 70] ]

2.1.1. リストに関する禁止事項#

リストの変数名を絶対に!絶対に!!絶対に!!!list にしない。

list = [1, 2, 3]  # これはエラーにはならないが、絶対ダメ

list は、リストを作るためにPythonがあらかじめ用意している型の名前である。上のように代入すると、同じスコープで list(...) を呼ぼうとしても、自分で作ったリストを指してしまい、エラーになる。

list のような組み込みの名前と、文法上の予約語は別物である。予約語にはそもそも代入できないが、組み込みの名前は上書きできてしまうので注意しよう。エディタの色分けも参考にはなるが、色だけで判断しないこと。

誤って組み込みの名前を上書きしてしまったら、一度 ランタイムを再起動 すると、その変数を消去できる。

このほかにもprint = などとしてprint関数を破壊してしまうこともよくある。

2.1.2. リストの要素にアクセスする, インデックス(index)#

リストの中の要素にアクセスするときは、半角の角括弧を使って[整数]といった形で"番地"を指定する。
このときの番地(あるいは座標)を指定する整数のことを インデックス(index) と呼ぶ。

Pythonでは要素にアクセスするためのインデックスは1からではなく、0からカウントすることを覚えておこう。

person = ["Aさん", 178, 66]
print(person[0])
print(person[1])
print(person[2])
Aさん
178
66

なので、person[3]にアクセスしようとすると、

print(person[3])
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[6], line 1
----> 1 print(person[3])

IndexError: list index out of range

list index out of range(リストのインデックスが用意された範囲を逸脱している)というエラーが出る。

慣れないうちは0からカウントするのを変に思うかもしれないが、これは言語の仕様によるもの。C/C++も通常は0から、JuliaやFortranの配列は通常1から数える。

さらに、Pythonのリストでは負のインデックスを使って、末尾から要素を指定できる。数学の教科書では1から数えることも多いので、混乱しやすいが少しずつ慣れていこう。

person = ["Aさん", 178, 66]
print(person)
print(person[-1])
print(person[-2])
print(person[-3])
['Aさん', 178, 66]
66
178
Aさん

[-1]でアクセスすれば末尾の要素に、[-2]番目の要素は後ろから二番目といった具合。

長いリストを作って「えっとこれ前から何番目の要素だったっけ...?」というときに、
後ろからX番目の要素といったように、サクッと取得できるのが便利なときもある。

入れ子にしたリストの要素を見たり、使いたいときには、少々慣れが必要。

mylist = [ [[1,2], [3,4]], 5, 6]

このような場合では、一番外側の括弧から数えて、何番目の階層になっているかを考えることが必要となる。 練習してみよう。

練習

  1. [+コード]でセルを以下に追加し、下のリストを作成する
    data = [ ["Aさん", 178,66], ["Bさん", 180, 70] ]

  2. data[i][j]のi,jに可能な整数を入れて、Aさんの身長、Bさんの体重などをprint関数で表示してみましょう。

  3. 2.と同じことを、負のインデックスを駆使してやってみましょう。

  4. data = [ [[1,2], [3,4]], 5, 6]を作成して、 len(data[0]) #0番目の要素の数をprintしてみましょう。

  5. print(data[0][0][1])を実行してみましょう。

  6. 4・5の挙動から推測して、dataのリストの中から4を取り出したい場合どうすればいいのかprint関数を使いながら考えてみましょう。

テキストセルの内容をコードセルなどにコピー&ペーストしたければ、対象をドラッグして、Ctrl+Cでコピー,Ctrl+Vでペースト出来ます(Macの場合はCtrlに置き換えてください)

2.1.3. リストの結合#

2つのリストを和+で結合することができる。

a = [1, 3]
b = [2, 4]
c = a + b
print(c)
[1, 3, 2, 4]

上のリストを座標だと思うと、要素ごとの和になってほしいような気もするが、リストの足し算は要素ごとの和ではなくリストの結合を意味する。

なお、要素ごとの和のような、数学などで必要な演算は、以降で扱うnumpyというモジュールを使ってndarray型と呼ばれるものにすれば簡単に実行できる。

長さや階層の異なる2つのリストでも結合することができる。

c = ["Aさん"]
d = [[170.5, 60.0], ["東京都出身", "250歳"]]
c + d
['Aさん', [170.5, 60.0], ['東京都出身', '250歳']]

2.1.4. リストに要素を加える#

リストにあとから要素を加えたくなるときもある。そんなときは append関数か+=を使う。

person_1 = ["Aさん", 178, 66]
person_1.append("O型")
print("person_1", person_1)

person_2 = ["Bさん", 180, 70]
person_2 += ["A型"]
print("person_2", person_2)
person_1 ['Aさん', 178, 66, 'O型']
person_2 ['Bさん', 180, 70, 'A型']

リスト同士なら、a += bb の要素を a の末尾に追加できる。

ただし、リストでは a = a + b と完全に同じではない。+= は元のリストを変更し、a = a + b は新しいリストを作って a に代入する。同じリストを別の変数も参照している場合に違いが現れるので、後の「リスト操作の注意点」で確かめよう。

data = []
data += [["Aさん", 178, 66]]
data += [["Bさん", 180, 70]]
print("data", data)
data [['Aさん', 178, 66], ['Bさん', 180, 70]]

などとする。空リストに既存(新規)のリストを追加したいときに、上のように が二重に必要な理由はリスト同士の和の演算が結合となることから理解できる。 実際に1重の括弧で試してみよう:

data2 = []
data2 += ["Aさん", 178, 66]  # 1重カッコ これだと右辺のリストを空リストに結合することになる
data2 += ["Bさん", 180, 70]  # これも同様
print(data2)
print("data2", data2)
['Aさん', 178, 66, 'Bさん', 180, 70]
data2 ['Aさん', 178, 66, 'Bさん', 180, 70]

こうしてしまう(リスト同士の単なる結合)と、人ごとにデータが区切られていないので扱うのに不便となる。

2.1.5. リストの値の更新#

リストの要素は後から更新することもできる。

data = [["Aさん", 178, 66], ["Bさん", 180, 70]]

というリストがあったとして、Aさんの体重を修正したければ、

data[0][2] = 58  # Aさんの体重を更新
print(data)  # リストを表示
[['Aさん', 178, 58], ['Bさん', 180, 70]]

入れ子になっているリストに血液型を追加したければ以下のようにすればよい。

data[0] += ["A型"]
data[1] += ["B型"]

print(data)
[['Aさん', 178, 58, 'A型'], ['Bさん', 180, 70, 'B型']]

2.1.6. 要素に対応するインデックスの取得#

リストに用意されたindexという関数(正確にはリストのメソッドと呼ばれるもの)を使ってリスト内の要素のインデックスを取得することができる。

tlist = ["いちご", "りんご", "ぶどう"]
tlist.index("りんご")
1

重複する要素がある場合、初めにヒットしたインデックスを返す。

tlist2 = ["いちご", "りんご", "ぶどう", "メロン", "りんご"]
tlist2.index("りんご")
1

複雑なデータを扱う際に「あれ、あの要素ってどこの番地にあるんだっけ?」といった状況に便利な関数となる。 ただし、リストの中に指定した要素がない場合は、ValueErrorというエラーが出るので注意しよう。

tlist2 = ["いちご", "りんご", "ぶどう", "メロン", "りんご"]
tlist2.index("パイナップル")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[18], line 2
      1 tlist2 = ["いちご", "りんご", "ぶどう", "メロン", "りんご"]
----> 2 tlist2.index("パイナップル")

ValueError: 'パイナップル' is not in list

2.1.7. スライスを用いた部分リストの取得#

以下のリストがあったとき、

data = ["years", 1990, 1995, 2000, 2005, 2010, 2015, 2020]

始点,コロン(:),終点でインデックスの範囲を指定して、部分的に取り出すことが出来る。

data[2:5]
[1995, 2000, 2005]

終点のインデックスに相当する要素は取り出されないことに注意しよう。

また、

data[1:]
[1990, 1995, 2000, 2005, 2010, 2015, 2020]

とすると、最後の要素まで含まれる。

負のインデックスを使用することもできる。

data[1:-1]
[1990, 1995, 2000, 2005, 2010, 2015]

どのインデックス以降に、自分の興味のある要素があるのか予め分かっているとき(例えば、csvファイルのX列以降のデータを取り出したいときなど)に便利な機能である。 ただし、インデックスを具体的に指定する必要があるため、あまり柔軟な方法とは言えない。

特定の要素があるインデックスを動的に取得することで、より汎用性のあるコードを書くことができるようになる。

2.1.8. \(\clubsuit\) リスト操作の注意点: 同じリストの参照#

授業で扱う程度の内容のプログラミングに関する疑問は、ググれば自分でだいたい解決することができる。
ただしこの項目で述べることは(初学者にとって)「直感に反するのだけど、どこがおかしいのか分からないのでそもそもどうググっていいかが分からない」という点で、少し事情が異なる。

例を見せるために、以下の2種類のリストを用意する。

data1 = [["Aさん", 178, 66], ["Bさん", 180, 70]]
tmp = ["Aさん", 178, 66]
data2 = [tmp, tmp]

print("data1", data1)
print("data2", data2)
data1 [['Aさん', 178, 66], ['Bさん', 180, 70]]
data2 [['Aさん', 178, 66], ['Aさん', 178, 66]]

data2のようにまず雛形のリストtmpを作って人数分の長さを持つリストを作ってから中身を編集しようと考えた場合、

data2[1][0] = "Bさん"
data2[1][1] = 180
data2[1][2] = 70

という操作を思いつく。このとき、data2の2つ目(0から数えて1番目)の要素だけを編集したつもりでもdata2printすると

print(data2)
[['Bさん', 180, 70], ['Bさん', 180, 70]]

y = x としても、リストの中身がコピーされるわけではない。xy同じリストを参照する状態になる。そのため、y[1] を変更すると、x から見える要素も変わる。

id() はオブジェクトを識別する整数を返す。同じリストを指しているか確かめてみよう。

tmp = ["Aさん", 178, 66]
data2 = [tmp, tmp]
print(id(data2[0]), id(data2[1]))  # それぞれのidを調べてprint
print("idが等しいか", id(data2[0]) == id(data2[1]))  # id()はオブジェクトのidを確認する関数
4701559040 4701559040
idが等しいか True

今の場合、data2を最初に作ったときには0番目と1番目の要素(リスト)は同一のidを持つtmpというリストである。
したがってtmpの中身を書き換える操作(data2[1][0]="Bさん")は、tmpの更新を通してdata2の要素いずれもに影響する。

これは、data2 = [tmp, tmp]という操作が、"tmpと同じ値のリストを2つの用途に持つリストの定義"ではなく、"すでに定義された配列を参照したリストの定義"になっているため。

このように、リストを入れ子にする際には、とくに注意が必要となる。なんかへんだな?と思ったときは要素のidに気を配ってみるのも重要になる。
上のコードで、意図したものと違う挙動を起こした原因は、リストtmp参照する形data2を作ったことだった。
これはcopyモジュールのcopy関数を用いて配列のコピーを作成することで回避できる(モジュールについては4章で説明する)。

import copy  # copyというモジュールをインポートする

tmp = ["Aさん", 178, 66]
data2 = [tmp, copy.copy(tmp)]
print(
    id(data2[0]) == id(data2[1])
)  # ← data2の0番目と1番目のidが同じ(参照元が同じ)だと困るのでFalseであってほしい
False

上のような一重のリストなら copy で十分だが、入れ子のリストでは、内側のリストはまだ共有される(浅いコピー)。内側まで独立に変更したい場合は、copy モジュールの deepcopy を使う。

import copy

data = [["Aさん", 178, 66], ["Bさん", 180, 70]]

copydata = copy.copy(data)
deepcopydata = copy.deepcopy(data)


print(id(data), id(copydata), id(deepcopydata))
print(id(data[0]), id(copydata[0]), id(deepcopydata[0]))
4701535936 4701553792 4701525440
4701536064 4701536064 4701580224

上を見るとdataというリストとcopydataという"リスト同士"が異なるidを持っていても、それぞれの0番目の要素のidを見てみると、同じものを参照していることがわかる。 このように、ネストされたリストをコピーして別々に扱いたい場合は特に注意が必要で、deepcopyのような作業が必要となる。(私も初めてプログラミングで配列を使ったときに、この点に気が付かずに時間をかなり溶かしました)

2.2. \(\clubsuit\) タプル・辞書型#

\(\clubsuit\)のついたセクションは発展的な内容で、授業では時間の都合上スキップすることが多い項目となる。 以下のタプル・辞書型は使いこなせれば便利だが、授業では必ずしも必要ではない(リストで代用できる)ので、興味に応じて学習しよう。

リストのときと同様に、タプルや辞書型の変数名を絶対にtupledictにしないように注意しよう。そうしないと、組み込み関数が使えなくなってしまうので、コードが正しく動かないことに繋がる。

2.2.1. \(\clubsuit\) タプル(tuple)型#

タプルは"immutable"(要素が変更不可)なリストと覚えておけばよい。 リストは要素を[]で囲むことで作ることができた。タプルは丸括弧()で囲むことで作ることができる。

厳密には、タプルを作る決め手はカンマである。要素が一つの場合は (1,) のように書く。また、タプルの要素そのものを置き換えることはできないが、要素がリストなら、そのリストの中身は変更できる。

values = (1.0, 2.0, 3.0)
print(values, type(values))
print("0番目の要素は", values[0])  ##要素にアクセスするときはタプルのときでもやはり角括弧を使う
(1.0, 2.0, 3.0) <class 'tuple'>
0番目の要素は 1.0

タプルは、要素を追加・削除・置換しない値の組を表すときに便利だ。条件によってはリストより省メモリになったり、操作が速くなったりするが、必ず速いわけではない。

# 1.の例 円周率のべきを保持しておいて再利用する状況をイメージ
pi = 3.141592653589793
pi_pow = (1, pi, pi**2, pi**3)
print("piの0乗から3乗まで", pi_pow)

# (誤って)要素を変更しようとするとエラーとなる
pi_pow[0] = 0
piの0乗から3乗まで (1, 3.141592653589793, 9.869604401089358, 31.006276680299816)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[30], line 7
      4 print("piの0乗から3乗まで", pi_pow)
      6 # (誤って)要素を変更しようとするとエラーとなる
----> 7 pi_pow[0] = 0

TypeError: 'tuple' object does not support item assignment

次に、小さなリストとタプルを作る時間を比べてみよう。たとえば、位置を表す3つの値 x, y, z[x, y, z](x, y, z) にまとめる場面を考える。

1回だけでは一瞬で終わるので、同じ操作を200万回行う。ここでは作ったものを保存しないため、作成と破棄にかかる時間の比較になる。

測定には標準ライブラリの timeit を使う。最初の文字列が測りたいコード、setup が測定前の準備である。number は1回の計測で実行する回数、repeat は計測を繰り返す回数を指定する。7回測り、各回の値と最短時間を見てみよう。

import platform
import timeit

print("実行環境:", platform.python_implementation(), platform.python_version())

# 測定前の準備。この部分の時間は計測に含まれない。
setup = "x = 12.3\ny = 45.6\nz = 78.9"
number = 2_000_000
repeat = 7

list_times = timeit.repeat("[x, y, z]", setup=setup, number=number, repeat=repeat)
tuple_times = timeit.repeat("(x, y, z)", setup=setup, number=number, repeat=repeat)

print("リストの各回の時間(秒):", [round(t, 4) for t in list_times])
print("タプルの各回の時間(秒):", [round(t, 4) for t in tuple_times])

list_best = min(list_times)
tuple_best = min(tuple_times)
print(f"最短時間: リスト {list_best:.4f} 秒 / タプル {tuple_best:.4f} 秒")
print(f"所要時間の比(リスト / タプル): {list_best / tuple_best:.2f}")
実行環境: CPython 3.12.11
リストの各回の時間(秒): [0.0825, 0.076, 0.0792, 0.0788, 0.0783, 0.0779, 0.078]
タプルの各回の時間(秒): [0.0507, 0.0523, 0.0506, 0.0515, 0.0511, 0.051, 0.0535]
最短時間: リスト 0.0760 秒 / タプル 0.0506 秒
所要時間の比(リスト / タプル): 1.50

差は見えただろうか。最後の比が1.5なら、リストにはタプルの1.5倍の時間がかかった、という意味である。最短時間を使うのは、他の処理に邪魔されて遅くなった計測の影響を減らすため。各回の値も見て、結果が大きく揺れていないか確かめよう。

この例では3要素の容器を作成・破棄する速さを比べている。タプルなら中身の計算や要素の読み出しまで同じ比率で速くなる、というわけではない。差はPythonの実装・バージョンや実行環境によっても変わる。

なお、[1, 2, 3](1, 2, 3) をそのまま比べると、タプルだけが事前に作られた定数として再利用されることがある。ここでは変数を使い、両方を毎回作るようにした。

測定方法の詳細はtimeitの公式ドキュメントを参照。timeit は計測中、循環参照を回収するGCを一時的に止めるため、GCも含めた実際の処理全体の時間とは区別して考えよう。

タプルは辞書のキーとしても使える。ただし、要素もすべてハッシュ可能(数値や文字列など)である必要があり、リストを含むタプルはキーにできない。

mydict = {("田中", "太郎"): [180, 86], ("鈴木", "一郎"): [188, 65]}

2.2.2. \(\clubsuit\) 辞書型#

辞書型は、キーと値の2つの要素をあわせ持つ型となる. リストにいれたものをいっぺんに扱うときに、毎回インデックスを指定したりループで要素を回して、望むものを持ってくるのは面倒。

たとえば以下の名前と年齢のリスト

data=[[ "Aさん",25],["Bさん",21],["Cさん",18]]

があったとき、この章で扱う知識(ループ&条件分岐)だけを駆使して Bさんの年齢を取得するには例えば以下のようなコードになる。

data = [["Aさん", 25], ["Bさん", 21], ["Cさん", 18]]

for person in data:
    if person[0] == "Bさん":
        print("Bさんの年齢=", person[1])
Bさんの年齢= 21

このようなfor文とif文などを組み合わせた要素の探索が面倒なら、予め名前と年齢という2つの関係のある量を"辞書"として定義してしまえばよい。 辞書型は波括弧{}で囲むことで構成できる.

Dict_age = {"Aさん": 25, "Bさん": 21, "Cさん": 18}

Bさんの値(今は年齢)を知りたければ以下のように一行で取得可能.

Dict_age["Bさん"]
21

辞書を作る際には、 要素を取り出すためのキー(key)と値(value)の組み合わせで指定する. その際

{"key" : value}

keyvalueをコロン:をつかって区切り、複数要素を入れる際はカンマで区切る。 keyは文字列や数字(たとえば小中学校の出席番号とか)を使用することができvalueは様々な型が使える。

値(value)として、リストを保持することもできる次のように年齢と出身県のリストを値にもつ辞書にしてみよう。

Dict = {"Aさん": [25, "栃木県"], "Bさん": [21, "茨城県"], "Cさん": [18, "群馬県"]}

Cさんの個人情報にアクセスする際は

Dict["Cさん"]
[18, '群馬県']

リストとタプルは、速さだけでなく、後から要素を変更したいかどうかで使い分けよう。

print("keys:", Dict.keys())
print("values:", Dict.values())
print("items:", Dict.items())
keys: dict_keys(['Aさん', 'Bさん', 'Cさん'])
values: dict_values([[25, '栃木県'], [21, '茨城県'], [18, '群馬県']])
items: dict_items([('Aさん', [25, '栃木県']), ('Bさん', [21, '茨城県']), ('Cさん', [18, '群馬県'])])

2.3. ループ処理#

ループ処理は、プログラミングの中で最も重要な概念の一つとなる。ループ処理を使うことで、同じ処理を繰り返し行うことができる。

2.3.1. for文#

まず以下のコードを実行してみよう:

for i in range(5):
    print(i)
0
1
2
3
4

range(始点, 終点, 刻み幅) は整数の並びを表す。終点は含まれない。全要素をリストとして用意するわけではないが、for で順に取り出せる。

print(range(5))
print(type(range(5)))
range(0, 5)
<class 'range'>

rangeは、リストと互換性を持つため、list関数を使ってリストに変換することができる。

print(list(range(5)))
[0, 1, 2, 3, 4]

なおrange関数の引数をrange(始点,終点,間隔)と3つにすると、より複雑な整数の集合を作ることもできる。

for i in range(0, 6, 2):
    print(i)  # やはり6は含まれないことに注意
0
2
4

for 変数 in 対象: という形で、対象から一つずつ要素を取り出し、その値を変数に代入して、インデントされた処理を繰り返す。

fruits = ["いちご", "りんご", "ぶどう", "メロン"]  # リストを定義
for tmp in fruits:
    print(tmp)
いちご
りんご
ぶどう
メロン

上のコードでは、tmpという変数にfruitsというリストの中身が順番に当てはめられている事がわかる。 ループを用いてリストの中身にアクセスする方法は主に2通りで、

  1. インデックスのループを回してリストにアクセスする

  2. リストの要素に順番にアクセスする

で、上のコードは2.に相当する。上の例で、1.の方法を採る場合は

for i in range(len(fruits)):
    print(fruits[i])
いちご
りんご
ぶどう
メロン

とすればよい。

インデックスと要素を同時に取得して使いたいときにはenumerate関数を使って

for i, tmp in enumerate(fruits):
    print("リスト内の" + str(i) + "番目の要素は:", tmp)
リスト内の0番目の要素は: いちご
リスト内の1番目の要素は: りんご
リスト内の2番目の要素は: ぶどう
リスト内の3番目の要素は: メロン

上記のほか、forinを用いて、リスト(またはリストと互換性のある多くの型)の中身を順番に取り出すことができる。

2.3.2. ブロックとインデント#

ループの中で繰り返したい処理は、**インデント(字下げ)**して書く。Pythonでは、これによって処理のまとまり(ブロック)を区別する。

この資料では1段を半角スペース4個に揃える。文法上「必ず4個」というわけではないが、同じブロックで深さを揃える必要があり、タブとスペースを混ぜないようにしよう。

インデントがないと、次のようにエラーになる。

for i in range(2):
print(i)
  Cell In[46], line 2
    print(i)
    ^
IndentationError: expected an indented block after 'for' statement on line 1

では、次のコードでは print が何回呼び出されるだろうか。インデントに注目して、実行前に考えてみよう。

for i in range(2):
    print(i)
    for j in range(5):
        print(i, j)
print(i, j)
0
0 0
0 1
0 2
0 3
0 4
1
1 0
1 1
1 2
1 3
1 4
1 4

iに関する繰り返し(ループとも呼ぶ)は、i=0,1の2回で、インデントが1ブロック下がった操作を2回実行する。 つまり、print(i)を2回と、jに関するループを2回繰り返す。

2つめのfor文で、jはj=0,1,2,3,4をとるが、iのループのブロックに入っているので、print(i,j)は合計2×5=10回呼ばれる。 最後の行にあるprint(i,j)はどのループ(ブロック)の中にも入っていないので、実行されるのは1回だけとなる。

ということで、答えはC.の13回となる。

慣れないうちは

for i in range(2):
    print(i)
    for j in range(5):
        print(i, j)
    ## End j loop
## End i loop
0
0 0
0 1
0 2
0 3
0 4
1
1 0
1 1
1 2
1 3
1 4

などのように、どこでループを閉じるのかをコメントして練習するのも良いかも。コードを編集しているうちに

for i in range(2):
    print(i)
for j in range(5):
    print(i, j)
0
1
1 0
1 1
1 2
1 3
1 4

といったように意図しないインデントになってしまって、正しい答えを与えない(バグを作ってしまう)可能性がある。

Pythonはインデント(ブロック)が、文法的な意味を持つというコードがスッキリする反面、 ブロック構造が曖昧になる危険性がある、という特徴があるので覚えておこう。

練習

リスト data = [ ["Aさん", 178,66] , ["Bさん",180,70], ["Cさん", 165,55]]を用意し、
[体重を二乗して身長にかけ合わせた量]の総和を計算するコードを作ってみよう。

ヒント1: 身長は入れ子になっているリストの[1]番目,体重は[2]番目
ヒント2: total=0.0を定義して、体重の二乗×身長をfor文を使ってどんどん足していく。

###以下は、不完全なコードです。これにあと3行ほど書き足してみましょう。
###ちなみに答えは 2156493です.
data = [["Aさん", 178, 66], ["Bさん", 180, 70], ["Cさん", 165, 55]]
total = 0.0

さて、ループ(for文)の使い方がなんとなく分かっただろうか?どんどん使ってみて、慣れること。

他にも、たとえば、 1, 10, 100, 1000, 10000,...というループを回したい時、

for i in [1, 10, 100, 1000, 10000]:
    print(i)
1
10
100
1000
10000

と書くのではなく、べき乗に対するループを回す

for p in range(5):
    print(10**p)
1
10
100
1000
10000

など、考えたい値を直接ループで扱うのではなく間接的な値(今の場合p)を扱うなどの考え方も、コードをスッキリさせる上で重要となる。

2.3.3. リスト内包表記#

for文の別の使い方として、リスト内包表記と呼ばれる書き方がある。これを使うと、要素がたくさんのリストを簡単につくることができる。

# year を1900から2000まで変えていき、その値を詰めたリストyearsを作成
years = [year for year in range(1900, 2001)]
print(years)
[1900, 1901, 1902, 1903, 1904, 1905, 1906, 1907, 1908, 1909, 1910, 1911, 1912, 1913, 1914, 1915, 1916, 1917, 1918, 1919, 1920, 1921, 1922, 1923, 1924, 1925, 1926, 1927, 1928, 1929, 1930, 1931, 1932, 1933, 1934, 1935, 1936, 1937, 1938, 1939, 1940, 1941, 1942, 1943, 1944, 1945, 1946, 1947, 1948, 1949, 1950, 1951, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1959, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000]

2.3.4. break ループ処理を途中で抜けたい場合#

breakfor文を途中で抜け出すのに使う。

用途としては目的を果たしたのでもうループを繰り返す必要がないとき予期せぬ事が起きた場合にループ処理を終わらせてループを終了するときなど。

以下のような状況を考えてみよう

Aさんは、掛け金が20万円、コインの表が出たら掛け金+20万円もらえるゲームをしています。 Aさんは、所持金100万円からスタートして、一度でも負ければゲームを辞めることにしました。 ただし、ゲームへの参加回数は最大10回とします。

for文を使って、このゲームをシミュレーションするループを書いてみましょう。

import random  # これがなにかはとりあえず気にしなくてよい

# 最初の手持ち金と掛け金を定義
money = 1_000_000
bet = 200_000

# ゲームを開始, 最大10回まで繰り返すことをfor文で表現
for i in range(10):
    print(str(i + 1) + "回目のゲームスタート")
    money = money - bet
    result = random.choice([True, False])  # 1/2の確率で勝ちか負けかを決める
    if result:  # result==True, つまり勝った場合
        money += 400000
        print("勝った!!! 所持金は", money, "円です")
    else:
        print("負けたのでおしまい")
        break  # for文を抜ける

print("最終的な所持金は..." + str(money) + "円だ")
1回目のゲームスタート
勝った!!! 所持金は 1200000 円です
2回目のゲームスタート
勝った!!! 所持金は 1400000 円です
3回目のゲームスタート
負けたのでおしまい
最終的な所持金は...1200000円だ

random.choice([True,False])は1/2の確率でTrueFalseを与える処理になっている。

True/Falseをそれぞれ勝ち負けに対応させられるので、その結果に応じて処理を分岐させていることが分かる。

2.3.5. \(\clubsuit\) While文#

forに似たものとしてwhileもよく用いられる。

while文は、多くの場合はfor文とbreakを組み合わせたコードでも実装可能なので授業ではスキップする。

プログラムを書いていくうちに、繰り返しの数が前もってわからないケースに遭遇することがある。たとえば

  • 連続で6が5回でるまでサイコロをふる

  • 利益がある値を超えるまで株の売買をくりかえす

といったイメージ。この様な場合は、何回処理を繰り返せば良いか予め知ることはほとんど不可能となる。

サイコロの例の場合だと、たとえば5回ふっただけで6が連続で出る奇跡的な状況も有りえるし、1000回ふっても100万回振っても連続で5回は出ないかもしれない。(某漫画の地下チンチロ編のようなイカサマサイコロを使用するとグッと確率はあがる)このような処理を実装したい場合にはwhile文を使うとよい.

さっきのAさんのギャンブルを例に

手持ち100万からゲームを開始して 「手持ちが50万以上150万円未満ある限り賭けを続けるコード」

を作ってみよう。

import random  # ランダムに勝ち負けを決めるためrandomというライブラリを使います

money = 1000000
bet = 200000
# 手元に50万以上-150万未満ある限り賭け続けることをwhile文で表現
while 500000 <= money < 1500000:
    money -= bet
    result = random.choice([True, False])
    if result:
        money += 400000
        print("勝った!!")
    else:
        print("負けた..")
    # whileの条件文は、各繰り返しの前に評価されるので、ここで条件を満たしているかを確認
    print("現在の所持金は" + str(money) + "円", "条件のチェック", 500000 <= money < 1500000)
    # while文の条件がFalseであれば、自動的にループを抜ける
print("最終的な所持金は..." + str(money) + "円だ")
負けた..
現在の所持金は800000円 条件のチェック True
負けた..
現在の所持金は600000円 条件のチェック True
負けた..
現在の所持金は400000円 条件のチェック False
最終的な所持金は...400000円だ

といった具合. 使用例が思い浮かばなければ「whileなんてのがある」とだけ覚えておけば当面はOK。

ちなみに上のコードはrandomモジュールの(疑似)乱数を用いているので、実行ごとに勝ったり負けたり結果が変わる。何度か実行して確かめてみよう。

continue を使うと、その回のループの残りの処理を飛ばして、次の繰り返しに進む。ループそのものを抜ける break との違いに注意しよう。

money = 1_000_000
bet = 200_000

for i in range(10):
    print(str(i + 1) + "回目のゲームスタート, 所持金は", money, "円")
    money = money - bet
    result = random.choice([True, False])  # 1/2の確率で勝ちか負けかを決める
    if result:  # result==True, つまり勝った場合
        money += 400000
        print("勝った!!! ")
    else:
        # 追加した部分, continueと同じブロックの続く処理をスキップする
        if i <= 1:
            print("負けたけど...まだまだ挽回するぞ...")
            continue
        print("負けたのでおしまい")
        break

print("最終的な所持金は..." + str(money) + "円だ")
1回目のゲームスタート, 所持金は 1000000 円
負けたけど...まだまだ挽回するぞ...
2回目のゲームスタート, 所持金は 800000 円
負けたけど...まだまだ挽回するぞ...
3回目のゲームスタート, 所持金は 600000 円
負けたのでおしまい
最終的な所持金は...400000円だ

2.3.6. 例外処理#

以下の処理は[ある値から都度10を引いていって平方根を取った値を表示する]というコードになる

import math  # sqrt(square root)関数を使うための1行

s = 114
while True:
    s -= 10
    print(math.sqrt(s))
10.198039027185569
9.695359714832659
9.16515138991168
8.602325267042627
8.0
7.3484692283495345
6.6332495807108
5.830951894845301
4.898979485566356
3.7416573867739413
2.0
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[57], line 6
      4 while True:
      5     s -= 10
----> 6     print(math.sqrt(s))

ValueError: math domain error

math.sqrt に負の数を渡したため、ValueError が出て処理が止まった。

このように、起こりうるエラーが分かっていて、それに応じて処理を変えたい場合には try / except が使える。次の例では、平方根を計算できない場合だけ、その回の処理を飛ばす。0の平方根は計算できることにも注意しよう。

import math

s = 124
hit = 0
for i in range(20):
    s -= 10
    try:
        print(math.sqrt(s))
    except ValueError:
        print("sの値が" + str(s) + "になったのでsqrtが計算できません")
        hit += 1
print(str(hit) + "回 sqrtの計算でエラーがありました")
10.677078252031311
10.198039027185569
9.695359714832659
9.16515138991168
8.602325267042627
8.0
7.3484692283495345
6.6332495807108
5.830951894845301
4.898979485566356
3.7416573867739413
2.0
sの値が-6になったのでsqrtが計算できません
sの値が-16になったのでsqrtが計算できません
sの値が-26になったのでsqrtが計算できません
sの値が-36になったのでsqrtが計算できません
sの値が-46になったのでsqrtが計算できません
sの値が-56になったのでsqrtが計算できません
sの値が-66になったのでsqrtが計算できません
sの値が-76になったのでsqrtが計算できません
8回 sqrtの計算でエラーがありました

try 内で ValueError が発生すると、対応する except ValueError: の処理が実行される。別の種類のエラーまで見逃さないよう、ここでは処理したい例外を指定した。詳しくはPython公式ドキュメント: 例外を処理するを参照。

2.4. 条件分岐 (if文)#

if文では、書かれた条件が成立した場合、後に続くブロックを実行する。
if文を使って命題の真偽を判定することで、条件分岐を表現することができる。

a = 3.0
b = -0.3
if a > b:
    print("aはbよりも大きい")
if b > a:
    print("bはaよりも大きい")  ##これは呼び出されない
aはbよりも大きい

条件を満たすときはA,満たさないときはBをしたい、という場合にはelseを使う。

if a < b:
    print("aはbよりも小さい")
else:
    print("a>=b")
a>=b

elif(else ifの略)を用いれば、もう少し複雑な条件を表現することができる。

たとえば、もし条件1が満たされればAを実行、条件1は満たされないが条件2が満たされればBを、さらに1も2も満たされない場合はCを実行するという場合は下記のようなコードになる:

if a < b:  # 条件1
    print("a<b")
elif a == b:  # 条件2
    print("a=b")
else:
    print("a>b")
a>b

if文は入れ子構造にすることもできる。その際はインデントを使ってブロックを表現する。

たとえばaが偶数の場合は値をそのまま表示して、aが奇数の場合は3の倍数かそうでないかで処理を変える事を考えると、以下のようになる

if a % 2 == 0:
    print(a)
else:
    if a % 3 == 0:
        print(str(a) + "は3の倍数です")
    else:
        print(str(a) + "は3の倍数ではありません")
3.0は3の倍数です

if文を使って条件分岐を作るときは条件分岐にモレがないか注意が必要となる。例えば、変数aの値によって処理を行う場合

if a > 0:
  ## なんらかの処理1
if a < 0:
  ## なんらかの処理2

と書いてしまうと、a=0の場合、if文を2つともすり抜けてしまい、バグの原因になることがある。 はじめのうちは少々面倒でもelseを使って、意図しないすり抜けがないかチェックするのが良い。

a = 0  # aをいろんな値に変えて実行してみてください
if a > 0:  # aが0より大きい場合
    print("処理1:a+2=", a + 2)
elif a < 0:
    print("処理2:a*2=", a * 2)
else:
    print("ゼロだよ?なんにもしなくていいの?")
ゼロだよ?なんにもしなくていいの?