タートルグラフィックスで学ぶ Python のキホン 第 19 章
教材コード
第 19 章

Python のデータ構造

入れ物を選ぶ.切り出す.重ねる.短く書く.

第 16 章でリストを,第 18 章で辞書を習いました. どちらも「たくさんのデータをまとめて持つ入れ物」です. Python には,この仲間があと 2 つあります. タプル(tuple)集合(set)です.

今回は,4 つの入れ物を一度に見わたして使い分けができるようにします. そのうえで,これまで説明しそびれていた便利な書き方— スライス(一部を切り出す), リストのリスト(マス目のデータ), 内包表記(1 行でリストを作る), key= を使った並べ替え—をまとめて身に付けます.

どれも「知らなくても書ける」ものばかりです. しかし知っていると,5 行かかっていたものが 1 行になります. 書く量が減れば,それだけ間違える場所も減ります. ここから先,自分のプログラムが急に短くなるのを実感できるはずです.

この章でできるようになること
  • listtupledictset の違いを説明でき,場面に応じて選べる
  • 他の言語の配列と Python の list の違いがわかる
  • よく使うメソッド(appendpopitemsadd など)を一覧から選んで使える
  • lensummaxminanyallenumeratezip を使える
  • スライス a[1:4]a[::2]a[::-1] で一部を切り出せる
  • リストのリストでマス目のデータを表し,二重の for で絵にできる
  • リストのリストをとして使い,行ごと・列ごとに集計できる
  • 内包表記 [f(x) for x in xs] でリストを 1 行で作れる
  • sorted(a, key=…, reverse=True) で,好きな基準で並べ替えられる
  • in の速さが入れ物によって違う理由を,感覚として説明できる

19.1 4 つの入れ物を見わたす

まず 4 つを並べて,違いを一度に見てしまいましょう. この表が今回のいちばん大事なページです.

リスト listタプル tuple辞書 dict集合 set
書き方[3, 1, 4](3, 1){"a": 1}{3, 1, 4}
空のもの[](){}set()
順番あるある入れた順に並ぶ無い
同じ値何個でも入る何個でも入るキーは 1 つだけ1 個に消える
あとから変えるできるできないできるできる(出し入れ)
取り出し方番号 a[0]番号 t[0]キー d["a"]取り出せない(有無だけ)
得意なこと順番が意味を持つデータ.あとから増やすあとから変わらない組.辞書のキーになれる名前から値を素早く引く重複を消す・入っているか調べる・共通部分
空の {} は辞書 {}空の辞書であって,空の集合ではありません. 空の集合が欲しいときは set() と書きます. {1, 2} と書けば集合,{"a": 1} と書けば辞書です. 同じ { } を使い回しているので,ここだけは覚えるしかありません.

19.1.1 タプル — あとから変えられない組

タプルは,丸かっこで書く入れ物です. リストとほとんど同じように使えますが,作ったあとで中身を変えられません. 足すことも,減らすことも,書き換えることもできません.

「変えられない」のは不便に聞こえますが,これは長所です. 「この 2 つは組で,ばらばらにならない」ということをプログラムの上で宣言できます. 座標 (x, y),日付 (2026, 9, 20), 赤緑青 (255, 128, 0) のように, まとまりに意味がある組によく使われます.

tuple.py
# タプル:あとから変えられない組

hito = (170, 62)             # 身長と体重を 1 組にする
print(hito)
print(hito[0], hito[1])      # 取り出し方はリストと同じ
print(len(hito))

shincho, taiju = hito        # 2 つの変数へ一度に取り出せる
print(shincho, "cm", taiju, "kg")

# hito[0] = 180              # ← # を外すと TypeError(変えられない)
# hito.append(3)             # ← これもできない

# 変わらないので,辞書のキーにできる
basho = {(0, 0): "まんなか", (100, 50): "右上"}
print(basho[(0, 0)])
print(basho[(100, 50)])

# dame = {[0, 0]: "むり"}    # ← リストはキーにできない(変わってしまうから)

# リストとタプルは行き来できる
print(list(hito))
print(tuple([1, 2, 3]))
知らないうちに使っている じつは,もう何度もタプルを使っています. for k, v in d.items():items() が返しているのは, (キー, 値) というタプルの並びです. pos() が返す座標もタプルですし, 第 14 章で習った「関数から 2 つの値を返す」書き方 (return x, y)も,中身はタプルです.

19.1.2 集合 — 重複が消えて,順番が無い

集合は,数学で習う集合とそっくりです. 同じものは 1 つしか入らず,順番を持ちません. 「何が入っているか」だけを覚えている入れ物だと思ってください.

set.py
# 集合:重複が消えて,順番が無い

kotae = ["いぬ", "ねこ", "いぬ", "うさぎ", "ねこ", "いぬ"]

shurui = set(kotae)          # リストから集合を作る
print(len(kotae), "個の答えのうち,種類は", len(shurui), "種類")
print(sorted(shurui))        # 順番が無いので,並べて表示すると読みやすい

print("ねこ" in shurui)       # 入っているかを調べるのが得意
print("とり" in shurui)

a = {1, 2, 3, 4}
b = {3, 4, 5}
print(sorted(a | b))         # 和集合:どちらかにあるもの
print(sorted(a & b))         # 積集合:両方にあるもの
print(sorted(a - b))         # 差集合:a にだけあるもの
集合は番号で引けない 集合には順番がありませんs[0] のように番号で取り出すことはできませんし, print(s) したときの並び順も,あてにしてはいけません. 順番が要るときは sorted(s) でリストに直してから使います.

19.1.3 どれを使うか,迷ったときの決め方

やりたいことを日本語で言ってみると,たいてい決まります.

  • 「1 番目,2 番目……と順に並べたい」「あとから足したり減らしたりする」 → リスト
  • 「2 つ(3 つ)をひとまとめにして渡したい」「もう変わらない」 → タプル
  • 名前で引きたい」「○○の△△を覚えておきたい」 → 辞書
  • ダブっているものを消したい」「入っているかだけ知りたい」「共通しているものを出したい」 → 集合
迷ったらリストで書き始める 迷ったらリストで書き始めてかまいません. あとから set(a)tuple(a) で作り直せます. 最初から正解を選ぼうとして手が止まるより, 動くものを書いてから直すほうが,ずっと早く進みます.

19.1.4 配列とリスト — 他の言語の本を読むときに

コラム:配列とリストは何が違うのか ほかの言語の本を開くと,まず配列(はいれつ,array)という言葉が出てきます. Python のリストとよく似ていますが,決定的な違いが 1 つあります.
他の言語の配列Python の list
大きさ作るときに個数を決めるint a[5]; と書いたら 5 個しか入らない決めないappend すればいくらでも伸びる
あふれたら6 個目を入れようとするとエラー(または,もっと悪いことが起きる)起きない.勝手に伸びる
中身の種類ふつうは全部同じ種類(整数なら整数だけ)混ぜてよい.[1, "ねこ", 3.5] も作れる
作り直し大きくしたいときは,新しく作って中身を移し替える要らない.Python が中でやってくれている
つまり,配列は「大きさが決まっているリスト」だと思ってください. Python にも array という名前の道具はありますが,ふつうは使いません. リストのほうが便利だからです. 「Python にはあの不便さが無い」ということを知っていれば, 他の言語の本を読んでも混乱しません.
ただでは無い ただし,便利さにはいつも代金があります. リストは「いつでも伸ばせる」ようにするために, 配列よりも少しだけ多くのメモリと手間を使っています. ふだんは気にしなくてよい差ですが, 「便利なものは,どこかで何かを払っている」という感覚は持っておいてください.
虫食い虫食い 19-A 4 つの入れ物を使い分ける

同じデータを 4 つの入れ物で扱います. ____ を埋めてください. 空の集合の書き方,重複の消し方,タプルの取り出し方がポイントです.

fill18_four.py
# 4 つの入れ物を使い分ける

kudamono = ["りんご", "みかん", "りんご", "すいか", "みかん", "りんご"]

# (1) リスト:順番どおりに 3 番目を表示する
print("3 番目は", kudamono[____])

# (2) 集合:何種類あるか
shurui = ____(kudamono)
print("種類の数は", len(shurui))

# (3) 辞書:それぞれ何個あるか
kazu = {}
for k in kudamono:
    kazu[k] = kazu.____(k, 0) + 1
print(kazu)

# (4) タプル:名前と個数を組にして,2 つの変数へ取り出す
ooi = ("りんご", kazu["りんご"])
namae, n = ____
print(namae, "が", n, "個でいちばん多い")

# (5) 空の集合を作る(空の辞書ではない書き方)
karappo = ____
print(len(karappo), type(karappo))

19.2 よく使うメソッドの一覧

a.append(5)append のように, 入れ物のうしろにドットを付けて呼ぶ命令メソッドといいます. 「その入れ物に対してできること」の一覧だと思ってください. ここで一度まとめておきます.覚えなくてかまいません.必要になったら見に来てください.

19.2.1 リストのメソッド

書き方すること元のリスト
a.append(x)末尾に x を 1 つ足す変わる
a.insert(i, x)i 番目に x を割り込ませる.後ろはずれる変わる
a.pop()末尾を取り出して取り除く変わる
a.pop(i)i 番目を取り出して取り除く変わる
a.remove(x)値が x のものを先頭から探して 1 つ取り除く変わる
a.sort()小さい順に並べ替える変わる
a.reverse()前後をひっくり返す変わる
a.extend(b)リスト b の中身をまとめて足す(a + b でも同じ)変わる
a.index(x)x が何番目にあるか.無ければエラー変わらない
a.count(x)x がいくつ入っているか変わらない
pop は番号,remove は値 a.pop(i)番号で取り除き, a.remove(x)で取り除きます. a = [10, 20, 30] のとき,a.pop(0) は 10 を取り除きますが, a.remove(0) は「値が 0 のもの」を探して見つからずエラーになります. よく取り違えるところです.
list_methods.py
a = [3, 1, 4]

a.append(9)          # 末尾に足す
a.insert(1, 7)       # 1 番目に割り込ませる
print(a)

x = a.pop()          # 末尾を取り出して取り除く
y = a.pop(0)         # 0 番目を取り出して取り除く
print("取り出した:", x, y)
print(a)

a.remove(7)          # 「値が 7 のもの」を取り除く
print(a)

a.extend([8, 8, 2])  # まとめて足す
print(a)

print("8 は", a.index(8), "番目")
print("8 は", a.count(8), "個")

a.sort()
print("小さい順:", a)
a.reverse()
print("逆順  :", a)

19.2.2 辞書のメソッド

書き方すること
d.keys()キーの一覧
d.values()値の一覧
d.items()(キー, 値) の組の一覧.for k, v in d.items(): で使う
d.get(k)値.キーが無ければ None(エラーにならない)
d.get(k, x)値.キーが無ければ x.数えあげの定番
d.pop(k)組を取り除きながら,その値を返す
d.clear()全部消す
k in dキー k があれば True(値ではなくキーを見る)
dict_methods.py
ten = {"あかり": 78, "けんた": 92, "ゆうき": 61}

print(list(ten.keys()))      # キーだけ
print(list(ten.values()))    # 値だけ
print(list(ten.items()))     # 組(タプル)の並び

for namae, t in ten.items():
    print(namae, "は", t, "点")

print(ten.get("あかり"))      # ある → 値
print(ten.get("みさき"))      # 無い → None
print(ten.get("みさき", 0))   # 無い → 0

print("合計", sum(ten.values()))
print("平均", sum(ten.values()) / len(ten))
print("最高", max(ten.values()))
dict_keys という表示 ten.keys()print すると dict_keys([...]) という見慣れない表示になります. これは「キーの一覧を必要になったら順に出すもの」で, リストそのものではありません. リストとして見たいときは list(ten.keys()) と包みます. for で回すだけなら,包まなくてもそのまま使えます.

19.2.3 集合のメソッド

書き方記号で書くとすること
s.add(x)x を 1 つ入れる.すでにあれば何も起きない
s.discard(x)x を取り除く.無くてもエラーにならない
s.remove(x)x を取り除く.無ければエラー
a.union(b)a | b和集合.どちらかにあるもの
a.intersection(b)a & b積集合.両方にあるもの
a.difference(b)a - b差集合.a にだけあるもの
x in s入っていれば True.とても速い
set_methods.py
s = {"りんご", "みかん"}

s.add("すいか")
s.add("りんご")              # すでにある.増えない
print(sorted(s), len(s))

s.discard("なし")            # 無い.エラーにもならない
s.discard("みかん")
print(sorted(s))

a = {"りんご", "みかん", "すいか"}
b = {"みかん", "いちご"}

print(sorted(a.union(b)))          # a | b と同じ
print(sorted(a.intersection(b)))   # a & b と同じ
print(sorted(a.difference(b)))     # a - b と同じ

19.2.4 自分で調べる — dir() と help()

上の表に載っていないメソッドもたくさんあります. 全部を覚える必要はありません. 動いている Python に直接たずねられるからです.

書き方すること
dir(もの)そのものにできることの名前の一覧.「何ができるんだっけ」のとき
help(命令)その命令の説明.「どう書くんだっけ」のとき
type(もの)そのものが何の仲間かlistdict など
ask_python.py
# 「何ができるの?」「どう書くの?」をプログラムに聞く

a = [1, 2, 3]
print(type(a))

# dir() … できることの一覧._ で始まるものは内部用なので読み飛ばす
for namae in dir(a):
    if not namae.startswith("_"):
        print(namae, end=" ")
print()

print("---")

help(list.insert)
他の入れ物も覗いてみる dir("") で文字列,dir({}) で辞書, dir(set()) で集合にできることが出ます. 知らない名前を見つけたら help(str.upper) のように 「仲間の名前.メソッド名」で説明を読めます. もっとくわしい説明は,日本語の公式ドキュメント docs.python.org/ja/3/ にあります.
虫食い虫食い 19-B 2 つのクラスの好きな色

2 つのクラスでアンケートを取りました. ____ を埋めて, 両方のクラスに出てきた色どちらかに出てきた色1 組にだけ出てきた色を求めてください.

fill18_sets.py
# 2 つのクラスの好きな色をくらべる

kumi1 = ["あか", "あお", "きいろ", "あか", "みどり"]
kumi2 = ["あお", "むらさき", "あか", "あお"]

# 重複を消して集合にする
s1 = ____(kumi1)
s2 = ____(kumi2)
print("1 組:", sorted(s1))
print("2 組:", sorted(s2))

# 両方に出てきた色
print("両方   :", sorted(s1.____(s2)))

# どちらかに出てきた色
print("どちらか :", sorted(s1.____(s2)))

# 1 組にだけ出てきた色
print("1 組だけ :", sorted(s1 ____ s2))

# 2 組に「きいろ」はあるか
print("2 組にきいろ:", "きいろ" ____ s2)

19.3 全体をまとめて調べる関数

入れ物の全体について何かを知りたいとき, わざわざ for で回さなくてよい命令があります. これらは入れ物の種類を選びません. リストでも,タプルでも,集合でも,辞書でも同じように使えます.

書き方返すもの自分で書くと
len(a)個数カウンタを 1 ずつ増やす
sum(a)合計(数のとき)s = 0 から足していく
max(a) / min(a)いちばん大きい/小さい値「いまのところの最大値」を覚えながら比べる
any(a)1 つでも成り立てば Truefor で回して,見つかったら break
all(a)全部成り立てば Truefor で回して,1 つでも外れたら False
sorted(a)並べ替えた新しいリスト選択ソート・挿入ソート(第 17 章)
enumerate(a)(番号, 中身) の組を順に出す自分で i を用意して増やす
zip(a, b)2 つのリストから同じ位置どうしの組を出すfor i in range(len(a)) で両方を添字で引く
builtins.py
ten = [72, 95, 61, 88, 40]

print("個数", len(ten))
print("合計", sum(ten))
print("平均", sum(ten) / len(ten))
print("最高", max(ten), " 最低", min(ten))

# any / all は「条件のリスト」に対して使う
gokaku = []
for t in ten:
    gokaku.append(t >= 60)
print(gokaku)
print("1 人でも合格した?", any(gokaku))
print("全員合格した? ", all(gokaku))

print("---")

# enumerate:番号と中身を同時に取り出す
for i, t in enumerate(ten):
    print(i, "番目は", t, "点")

print("---")

# zip:2 つのリストを同じ位置どうしで組にする
namae = ["あかり", "けんた", "ゆうき", "みさき", "そうた"]
for n, t in zip(namae, ten):
    print(n, t)

# zip で辞書を作ることもできる
print(dict(zip(namae, ten)))
range(len(a)) は enumerate に置き換えられる 第 16 章では,番号が欲しいときに for i in range(len(a)): と書いていました. enumerate を使うと for i, x in enumerate(a): と書けて, 番号と中身の両方が一度に手に入ります. a[i] と書かなくてよくなるので,書き間違いも減ります.
zip は短いほうで止まる zip は,長さの違うリストを渡すと 短いほうに合わせて止まります. 余ったぶんは黙って捨てられ,エラーにはなりません. 2 つのリストの長さがそろっているかは,自分で気をつけてください.

19.4 スライス — 一部を切り出す

a[2] と書けば 1 つだけ取り出せます. 続きの何個かをまとめて取り出したいときは, a[1:4] のようにコロンを使います.これをスライスといいます. 第 16 章で軽く触れましたが,ここで正面から扱います.

書き方意味
a[1:4]1 番目から,4 番目の手前まで(1・2・3 番目の 3 個)
a[:3]先頭から 3 番目の手前まで.始まりを省くと先頭から
a[3:]3 番目から最後まで.終わりを省くと最後まで
a[-2:]うしろから 2 個.マイナスは「うしろから数えて」
a[::2]1 つおき.3 つめの数は「とびとびの幅」
a[::-1]逆順.幅がマイナスだと後ろから進む
a[:]全部.コピーを作るときの決まり文句
終わりの数は入らない a[1:4]4 番目は入りません. 「1 番目から,4 番目の手前まで」です. range(1, 4) が 1・2・3 だったのと同じ決まりです. この「手前まで」は Python 中で一貫しているので, 終わりの数は入らないと覚えてしまってください.
slice.py
a = [10, 20, 30, 40, 50, 60]

print(a[1:4])      # 1 番目から 4 番目の手前まで
print(a[:3])       # 先頭から
print(a[3:])       # 最後まで
print(a[-2:])      # うしろから 2 個
print(a[::2])      # 1 つおき
print(a[::-1])     # 逆順
print(a[:])        # 全部

print("---")

# 文字列にも同じように使える
s = "たーとるぐらふぃっくす"
print(s[0:3])
print(s[-4:])
print(s[::-1])

print("---")

# コピーと「同じものの別名」の違い
b = a[:]           # コピー.b を変えても a は変わらない
b[0] = 999
print("a[0] =", a[0], " b[0] =", b[0])

c = a              # これは同じものに別の名前を付けただけ
c[0] = 777
print("a[0] =", a[0], " c[0] =", c[0])
コラム:b = a はコピーではない b = a と書いても,リストはコピーされません. 同じ 1 つのリストに ab という 2 つの名札が付くだけです. だから b を変えると a も変わります. これは初めのうち必ずはまる落とし穴です. 中身を別に持ちたいときは b = a[:](または b = list(a))と書きます.
スライスは元を変えない スライスは元のリストを変えません. 切り出した新しいリストが返ってくるだけです. 第 17 章で「sorted() は新しいリストを返し, sort() はその場で並べ替える」と習いましたが,それと同じ区別です.
虫食い虫食い 19-C スライスで切り分ける

1 年ぶんの月別データから,いろいろな切り出し方をします. ____ を埋めてください. 「終わりの数は入らない」ことに気をつけてください.

fill18_slice.py
# スライスで切り分ける

tsuki = ["1月", "2月", "3月", "4月", "5月", "6月",
         "7月", "8月", "9月", "10月", "11月", "12月"]

# (1) 前半 6 か月
print("前半:", tsuki[____])

# (2) 後半 6 か月
print("後半:", tsuki[____])

# (3) 4 月から 6 月まで(3 つ)
print("春 :", tsuki[3:____])

# (4) うしろから 3 か月
print("年末:", tsuki[____])

# (5) 1 つおき(1月,3月,5月,…)
print("奇数月:", tsuki[____])

# (6) 逆順
print("逆順:", tsuki[____])

# (7) コピーを作って,1 つだけ書き換える(元は変わらないこと)
copy = tsuki[____]
copy[0] = "睦月"
print(copy[0], tsuki[0])

19.5 リストのリスト — マス目のデータ

リストの中身は,何でも構いません.リストを入れることもできます. リストのリストを作ると,マス目(表)を表せます. 他の言語では二次元配列と呼ばれるものです.

2 行 3 列のマス目
masu = [
    [1, 2, 3],
    [4, 5, 6],
]
書き方意味
masu[0]1 行目まるごと[1, 2, 3]
masu[0][2]1 行目の 2 番目.3行が先,列が後ろ
len(masu)行の数.この例では 2
len(masu[0])列の数.この例では 3
masu[1][0] = 92 行目の 0 番目を書き換える
行が先,列が後ろ masu[行][列] の順です. タートルの座標は (x, y)(横が先)でしたが, マス目はたてが先になります. y で行を,x で列を指す,と決めておくと混乱しません.

マス目を全部たどるには,for二重にします (第 7 章の入れ子です).

grid.py
masu = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
]

print("行の数", len(masu))
print("列の数", len(masu[0]))
print("2 行目 3 列目は", masu[1][2])

# 行ごとに表示する
for gyo in masu:
    print(gyo)

print("---")

# 1 マスずつたどる
gokei = 0
for y in range(len(masu)):
    for x in range(len(masu[y])):
        gokei = gokei + masu[y][x]
print("全部の合計は", gokei)

# 行ごとの合計・列ごとの最大値
for y in range(len(masu)):
    print(y, "行目の合計", sum(masu[y]))

19.5.1 マス目を絵にする — ドット絵

マス目のデータは,そのまま絵になります1 のところに点を打つだけです. データを書き換えれば絵が変わる,というのが気持ちのよいところです.

dotart.py
from turtle import *

ht()
speed(0)

# 0 は何も描かない,1 は色を塗る
e = [
    [0, 0, 1, 1, 1, 0, 0],
    [0, 1, 1, 1, 1, 1, 0],
    [1, 1, 0, 1, 0, 1, 1],
    [1, 1, 1, 1, 1, 1, 1],
    [1, 0, 1, 1, 1, 0, 1],
    [1, 0, 1, 0, 1, 0, 1],
    [0, 1, 0, 0, 0, 1, 0],
]

print("たて", len(e), "行 / よこ", len(e[0]), "列")

masu = 40
hidari = -(len(e[0]) - 1) * masu / 2
ue = (len(e) - 1) * masu / 2

for y in range(len(e)):
    for x in range(len(e[y])):
        if e[y][x] == 1:
            pu()
            goto(hidari + x * masu, ue - y * masu)
            pencolor("darkorange")
            dot(masu - 4)

done()
図 19.1 リストのリストをそのまま絵にしたもの.01 を書き換えると絵が変わる.
色を増やすには 色を 01 の 2 種類にしましたが, 012…と増やして, 数と色を対応させた辞書を用意すれば, 何色でも使えます(第 18 章の辞書の出番です). 演習 19-1 でやってみましょう.
虫食い虫食い 19-D チェック模様を作る

マス目のデータをプログラムで作って描きます. たてと横の番号を足して 2 で割った余りで色を決めると,市松模様になります. ____ を埋めてください.

fill18_grid.py
from turtle import *

ht()
speed(0)

n = 8        # 8 行 8 列

# (1) マス目のデータを作る
masu = []
for y in range(n):
    gyo = []
    for x in range(n):
        gyo.append((x + y) % ____)     # 0 か 1 が交互に入る
    masu.____(gyo)                     # 1 行ぶんを足す

print(masu[0])
print(masu[1])

# (2) 絵にする
haba = 44
hidari = -(n - 1) * haba / 2
ue = (n - 1) * haba / 2

for y in range(____(masu)):
    for x in range(len(masu[y])):
        pu()
        goto(hidari + x * haba, ue - y * haba)
        if masu[y][x] == 0:
            pencolor("darkslateblue")
        else:
            pencolor("lightsteelblue")
        dot(haba - 4)

done()

19.5.2 表としてのリストのリスト — 1 行が 1 件

リストのリストは,マス目だけのものではありません. 1 行に 1 件ぶんのデータを並べると,そのままになります. 名簿,成績表,商品の一覧 — 世の中のデータの多くはこの形をしています.

このとき,1 行の中には種類の違うものを混ぜてよいのがリストの気楽なところです. 名前は文字列,点数は整数のまま並べられます.

1 行が 1 人ぶんの成績表
seiseki = [
    ["あかり", 78, 92, 61],
    ["けんた", 55, 70, 88],
    ["ゆうき", 90, 64, 73],
]
書き方取り出せるもの
seiseki[0]1 人ぶんまるごと["あかり", 78, 92, 61]
seiseki[0][0]その人の名前
seiseki[0][1]その人の1 科目めの点数
seiseki[0][1:]名前をとばして点数だけ(19.4 節のスライス)
len(seiseki)人数(行の数)

行ごとの集計は簡単です.1 行が 1 人ぶんなので, for gyo in seiseki: と回せばそのまま 1 人ずつになります. やっかいなのは列ごと(科目ごと)の集計で, こちらは行を縦に渡り歩くことになるので,列の番号を添字で指す必要があります.

table.py
kamoku = ["国語", "数学", "英語"]
seiseki = [
    ["あかり", 78, 92, 61],
    ["けんた", 55, 70, 88],
    ["ゆうき", 90, 64, 73],
]

print(seiseki[0])        # 1 人ぶんまるごと
print(seiseki[0][0])     # 名前
print(seiseki[0][1:])    # 点数だけ

print("---")

# 行ごと(人ごと)の合計と平均
for gyo in seiseki:
    namae = gyo[0]
    ten = gyo[1:]
    print(namae, " 合計", sum(ten), " 平均", sum(ten) / len(ten))

print("---")

# 列ごと(科目ごと)の合計 … 行を縦に渡り歩く
for x in range(len(kamoku)):
    gokei = 0
    for gyo in seiseki:
        gokei = gokei + gyo[1 + x]      # 名前のぶん 1 つずらす
    print(kamoku[x], "の合計", gokei)

print("---")

# 表の形に並べて表示する
print("名前  国語 数学 英語")
for gyo in seiseki:
    for atai in gyo:
        print(atai, end=" ")           # 改行しないで,つづけて出す
    print()                             # 1 行ぶん終わったら改行する
print の end= — 改行しないで出す print() は,ふつう表示のあとに改行します. print(atai, end=" ") と書くと, 改行のかわりに何を出すかを指定できます. ここでは全角の空白を出しているので,横に並んでいきます. 最後に print() だけを書くと,そこで改行されます. 表を画面に出すときの定番の書き方です.

マス目のときと同じく,for を二重にすれば 1 マスずつたどれます. for gyo in seiseki: の中で for atai in gyo: と書けば, 添字をまったく使わずに全部のマスを回れます. 添字が要るのは,「何行目・何列目か」を知りたいときと, 上の科目ごとの合計のように縦に渡り歩くときだけです.

表は辞書のリストでも書ける 同じ成績表は,第 18 章の辞書でも書けます.
python
seiseki = [
    {"名前": "あかり", "国語": 78, "数学": 92, "英語": 61},
    {"名前": "けんた", "国語": 55, "数学": 70, "英語": 88},
]
リストのリストは番号で覚える,辞書のリストは名前で覚えるgyo[2] は短いけれど「2 は数学だったかな」と迷いますし, 列の順番を入れ替えたとたんに壊れます. gyo["数学"] は長いけれど,読んだ瞬間に意味が分かります. 項目に名前があるなら辞書同じ種類の値がただ並ぶだけならリスト.19.1.3 節の決め方のとおりです.

19.6 内包表記 — リストを 1 行で作る

「空のリストを作って,for で回して,append する」. この 4 行の型は,これまで何度も書いてきました. Python には,これを1 行で書く書き方があります. 内包表記(ないほうひょうき)といいます.

同じことを 2 通りで
# これまでの書き方(4 行)
a = []
for x in range(1, 6):
    a.append(x * x)

# 内包表記(1 行)
a = [x * x for x in range(1, 6)]

読み方はうしろからです. 「range(1, 6)x それぞれについて, x * x を並べたリスト」. 英語の語順のままなので,慣れると英文として読めるようになります.

書き方意味
[f(x) for x in a]a の中身それぞれを f で変えたリスト
[x for x in a if 条件]条件に合うものだけを選んだリスト
[f(x) for x in a if 条件]選んでから変える
{f(x) for x in a}角かっこを波かっこにすると集合になる
comprehension.py
# 変える
print([x * x for x in range(1, 11)])
print([x * 2 for x in [3, 1, 4]])

# 選ぶ
print([x for x in range(1, 21) if x % 3 == 0])

ten = [72, 95, 61, 88, 40, 77]
print([t for t in ten if t >= 60])              # 合格者だけ
print(len([t for t in ten if t >= 60]), "人合格")

# 選んでから変える
print([t + 5 for t in ten if t < 60])           # 不合格者だけ 5 点おまけ

# 文字列にも
w = ["ringo", "ichigo", "suika"]
print([s.upper() for s in w])
print([len(s) for s in w])

# 辞書から
tensu = {"あかり": 78, "けんた": 92, "ゆうき": 61}
print([namae for namae in tensu if tensu[namae] >= 70])

# 集合の内包表記
print(sorted({len(s) for s in w}))
座標のリストを作る タートルとも相性がよい書き方です. [(x, 0) for x in range(-200, 201, 50)] と書けば, 横一列に並んだ座標のリストが 1 行でできます. for で回して描けば,等間隔に並んだ図形になります.
短ければよいわけではない 内包表記は短いときだけ使ってください. 条件が 2 つ 3 つと増えたり,中で計算が長くなったりしたら, ふつうの for で書いたほうが読みやすいです. 短く書くことが目的ではありません.読みやすいことが目的です.

19.7 並べ替え — 好きな基準で並べる

第 17 章で sorted()sort() を習いました. おさらいすると,違いは「新しいリストを返すか,その場で並べ替えるか」です.

書き方元のリスト返すもの使いどころ
sorted(a)変わらない新しい並べ替え済みリスト元の順番も残しておきたいとき.辞書や集合にも使える
a.sort()並べ替わるNone(無し)元の順番はもう要らないとき.リスト専用

ここから先が新しい話です. sorted には省略できる引数が 2 つあり, これを使うと好きな基準で並べ替えられます.

引数すること
reverse=True大きい順(逆順)にするsorted(a, reverse=True)
key=関数何で比べるかを決める.中身をその関数に通した結果で比べるsorted(w, key=len)(文字数の順)

key= には関数の名前を書きます. かっこは付けません(第 18 章で,辞書に関数そのものを入れたときと同じです). 自分で作った関数も渡せます.

sort_key.py
# (名前, 身長) のタプルのリスト
hito = [("あかり", 158), ("けんた", 172), ("ゆうき", 165),
        ("みさき", 149), ("そうた", 180)]

# そのまま並べると「名前の順」になる(タプルは先頭から比べられる)
print(sorted(hito))

print("---")

# 身長で比べたい → 「組から身長を取り出す関数」を渡す
def shincho(t):
    return t[1]

print(sorted(hito, key=shincho))
print(sorted(hito, key=shincho, reverse=True))

print("---")

# 文字数の順に並べる
w = ["ringo", "ichigo", "suika", "momo"]
print(sorted(w, key=len))

# 辞書を「値の大きい順」に並べる
ten = {"あかり": 78, "けんた": 92, "ゆうき": 61}

def atai(kv):
    return kv[1]

print(sorted(ten.items(), key=atai, reverse=True))
lambda という書き方 ほかの人のプログラムで sorted(ten.items(), key=lambda kv: kv[1]) という書き方を見ることがあります. lambda(ラムダ)はその場かぎりの小さな関数を書く書き方で, 上の def atai(kv): return kv[1] と同じ意味です. この教材では使いませんが,見たときに読めれば十分です.

19.7.1 並べ替えてから描く

並べ替えは,絵にする直前にやると効きます. データの並び順をいじらずに,見せ方だけ変えられるからです.

sorted_bar.py
from turtle import *

ht()
speed(0)
pensize(2)

hito = [("あかり", 158), ("けんた", 172), ("ゆうき", 165),
        ("みさき", 149), ("そうた", 180)]


def shincho(t):
    return t[1]


narabi = sorted(hito, key=shincho, reverse=True)
print(narabi)
print("いちばん高い人は", narabi[0][0])

x = -245
for namae, h in narabi:
    pu()
    goto(x, -150)
    seth(90)
    pd()
    pencolor("dimgray")
    fillcolor("steelblue")
    begin_fill()
    fd((h - 140) * 3)
    rt(90)
    fd(64)
    rt(90)
    fd((h - 140) * 3)
    rt(90)
    fd(64)
    rt(90)
    end_fill()

    pu()
    goto(x + 4, -178)
    pencolor("black")
    write(namae, font=("Arial", 13, "normal"))
    goto(x + 10, -142 + (h - 140) * 3)
    write(h, font=("Arial", 14, "normal"))

    x = x + 100

done()
そうた180けんた172ゆうき165あかり158みさき149
図 19.2 key= で身長の高い順に並べ替えてから描いたもの.
虫食い虫食い 19-E 点数の高い順にならべて描く

(名前, 点数) のタプルのリストを点数の高い順に並べ替え, 順位と点数を表示します. ____ を埋めてください. 合格者だけを選ぶところは,内包表記を使います.

fill18_rank.py
from turtle import *

ht()
speed(0)

seiseki = [("あかり", 78), ("けんた", 92), ("ゆうき", 61),
           ("みさき", 55), ("そうた", 88)]

# (1) 点数を取り出す関数
def tensu(t):
    return t[____]

# (2) 点数の高い順に並べ替える
narabi = sorted(seiseki, key=____, reverse=____)
print(narabi)

# (3) 合格者(60 点以上)だけを内包表記で選ぶ
gokaku = [t for t in seiseki if t[1] >= ____]
print(len(gokaku), "人合格")

# (4) 順位をつけて描く(番号は enumerate で取る)
y = 150
for jun, kumi in ____(narabi):
    namae = kumi[0]
    ten = kumi[1]
    pu()
    goto(-240, y)
    if ten >= 60:
        pencolor("navy")
    else:
        pencolor("crimson")
    write(str(jun + 1) + " 位 " + namae + " " + str(ten) + " 点",
          font=("Arial", 20, "normal"))
    y = y - 60

done()

19.8 in の速さは入れ物で違う

最後に,知っているかどうかで差が出る話をします. x in a という同じ書き方でも, a が何かによって速さがまるで違います

入れ物どうやって探しているかデータが 100 倍になると
リスト先頭から1 つずつ順に比べる.第 17 章で自分で書いた線形探索そのもの時間も約 100 倍
タプルリストと同じ.先頭から順に比べる時間も約 100 倍
辞書値から置き場所を計算して,そこを直接見るほとんど変わらない
集合辞書と同じ仕組み.そこを直接見るほとんど変わらない

たとえ話にするとこうです. リストは本を 1 冊ずつ手に取って表紙を見るやり方. 辞書と集合は背表紙の番号から棚の場所がわかっていて,そこへ一直線に行くやり方です. 本が 10 冊なら大差ありませんが,10 万冊になると決定的に違います.

本当に違うのか,測ってみましょう. 5 万個のデータを持つリストと集合に対して,「入っていない値」を 2000 回探します.

in_speed.py
# リストと集合で,in の速さをくらべる
import time

n = 50000
nakami_list = list(range(n))     # 0 から 49999 までのリスト
nakami_set = set(nakami_list)    # 同じ中身の集合

kaisu = 2000
sagasu = -1                      # わざと「入っていない」値を探す

t0 = time.time()
for i in range(kaisu):
    x = sagasu in nakami_list
t1 = time.time()

for i in range(kaisu):
    x = sagasu in nakami_set
t2 = time.time()

print("リストで", kaisu, "回調べる:", round((t1 - t0) * 1000), "ミリ秒")
print("集合で ", kaisu, "回調べる:", round((t2 - t1) * 1000), "ミリ秒")

何百倍という差が出たはずです(数字はパソコンによって変わります). リストのほうは,入っていない値を探すたびに5 万個すべてと比べているからです.

使い分けの目安 だからといって,いつでも集合を使えばよいわけではありません. 集合には順番がありませんし,同じ値を 2 つ持つこともできません. 「たくさんのデータに対して,何度も in で調べる」 という場面になったときに, 「集合に入れ直せば速くなるかもしれない」と思い出せればじゅうぶんです.
コラム:なぜタプルはキーになれるのか どうやって「置き場所を計算」しているのでしょうか. 値からハッシュ値という数を作り,それを住所として使っています. print(hash("ねこ")) で覗けます. タプルが辞書のキーにできてリストができないのは,この計算のためです. 中身が変わってしまうと住所も変わってしまうので, 変えられないタプルだけがキーになれるのです. くわしくは 2 年次の「データ構造とアルゴリズム」で習います.

19.9 この章で出てきた命令

書き方意味
(1, 2)タプル.あとから変えられない組
x, y = tタプルの中身を 2 つの変数に取り出す
{1, 2, 3}集合.重複が消え,順番は無い
set()空の集合({} は空の辞書)
set(a)リスト a から重複を消した集合を作る
list(t) / tuple(a)入れ物どうしを作り直す
s.add(x) / s.discard(x)集合に入れる/取り除く
a.union(b) / a | b和集合.どちらかにあるもの
a.intersection(b) / a & b積集合.両方にあるもの
a.difference(b) / a - b差集合.a にだけあるもの
a.append(x) / a.insert(i, x)足す/割り込ませる
a.pop() / a.pop(i)番号で取り出して取り除く
a.remove(x)値で探して取り除く
a.extend(b)リスト b の中身をまとめて足す
a.index(x) / a.count(x)何番目か/いくつあるか
d.keys() / d.values() / d.items()キー/値/組の一覧
d.get(k, x)値.無ければ x
len(a) / sum(a) / max(a) / min(a)個数/合計/最大/最小
any(a) / all(a)1 つでも成り立つ/全部成り立つ
enumerate(a)(番号, 中身) の組を順に出す
zip(a, b)2 つのリストから同じ位置どうしの組を出す
a[1:4]1 番目から 4 番目の手前まで(スライス)
a[:3] / a[3:] / a[-2:]先頭から/最後まで/うしろから
a[::2] / a[::-1]1 つおき/逆順
b = a[:]リストのコピーを作る(b = a はコピーではない)
masu[y][x]リストのリスト.行が先,列が後ろ
hyo[i][0] / hyo[i][1:]表としてのリストのリスト.1 件ぶんの 1 つめ/2 つめから先
print(x, end=" ")改行しないで続けて表示する
[f(x) for x in a]内包表記.変えたものを並べたリスト
[x for x in a if 条件]内包表記.条件に合うものだけ
sorted(a, key=f)f に通した結果で比べて並べ替える
sorted(a, reverse=True)大きい順に並べ替える
dir(もの) / help(命令)できることの一覧/説明
type(もの)そのものが何の仲間か

演習

出発点のプログラムを「▶ この演習を開く」で右側に読み込み, それを発展させて作品にしてください. できあがったら「⤓ 保存」でファイルに保存して,手元に残しておきましょう.

調べながら書く わからない命令が出てきたら,右のコード欄で help(…)dir(…) を実行して確かめられます. 調べながら書くという動きに慣れてください.
演習演習 19-1 ドット絵をつくる

リストのリストで自分のドット絵を作ります. 出発点には,白黒 2 色のマス目が入っています.

  1. マス目の中身を書き換えて,自分のドット絵にする(顔,文字,記号,キャラクターなど何でもよい).
  2. 色を3 色以上に増やす.012… と数を増やし,数と色を対応させた辞書を用意する.
  3. マス目の大きさ masu を変えても,絵が画面の中央に来るようにする(出発点の hidariue の計算が参考になります).
  4. 余裕があれば,dot ではなく四角い塗りつぶしbegin_fillend_fill)にして,すきまのない絵にする.
ヒント 絵は方眼紙に下書きしてから数に直すのが速いです. いきなりプログラムに打ち込むと,まず思ったとおりになりません.
演習演習 19-2 アンケート集計プログラム

クラスのアンケートを集計して,結果を絵にします. 集合・辞書・並べ替えを全部使う演習です.

  1. 何種類の答えがあったかを集合で求めて表示する.
  2. 答えごとの人数を辞書で数える.
  3. 人数の多い順key= で並べ替え,enumerate で順位を付けてコンソールに表示する.
  4. 結果を棒グラフか円の大きさでタートルに描かせる.答えの名前と人数も write で書き入れること.
  5. 余裕があれば,2 票以上のものだけを内包表記で選んで描く.
ヒント 組から人数を取り出す関数を作って key= に渡します(19.7 節). 棒の描き方は 19.7.1 節の例がそのまま使えます.
演習演習 19-3 スライスと内包表記で書き直す

長く書いたプログラムを,今回習った書き方で短くする演習です. 出発点には,わざと長く書いたプログラムが入っています.

  1. # の下に,スライスや内包表記を使った短い版を書く.
  2. 結果が同じになることprint で確かめる(print(a == b) と書けば True が出ます).
  3. 最後に,何行から何行に減ったかをコメントで書く.
  4. 余裕があれば,enumeratezip を使って書き直せるところも探す.
注意 短くすることが目的ではありません. 読みやすくなったかどうかを自分で判断してください. 「短くしたけれど読みにくくなった」と感じたら,それも立派な発見です. コメントにそう書いてください.
演習演習 19-4 名簿を並べ替えて絵にする

(名前, 点数, 身長) のタプルのリストを, いろいろな基準で並べ替えて絵にします.

そうたけんたあかりみさき
出発点のイメージ:点数の高い順に並んだ棒
  1. 点数の高い順に並べ替えて棒グラフにする.
  2. 名前の順(あいうえお順)に並べ替えたものも描く.並べ替える基準を変えるだけで済むことを確かめる.
  3. 60 点未満の棒だけ色を変える
  4. enumerate順位を,summaxmin平均・最高・最低を画面に書き入れる.
  5. 余裕があれば,身長の順にも並べ替えられるようにして,どの基準で並べるかを input() で選べるようにする.
ヒント 並べ替えの基準は「タプルから何番目を取り出すか」だけの違いです. def tensu(t): return t[1]def shincho(t): return t[2] の 2 つを用意して, key= に渡し分けてください.
演習演習 19-5 前に作ったものを,新しい道具で書き直す

動きは変えずに,書き方だけをよくすることを リファクタリングといいます. プロの現場でも日常的におこなわれている作業で, 新しい機能を足す前に,まず読みやすく整えるのがふつうです. 今回はそれを自分のプログラムでやってみます.

出発点には,「書き直す前」のプログラムが入っています. 同じことを何度も書き,変数を 5 個も並べた,いかにも長いプログラムです. 動かすと,星が 5 つ描かれ,点数の合計・平均・最高が表示されます.

  1. まず動かして,どんな結果になるかを覚える.書き直したあとで,同じ結果になることを確かめるためです.
  2. 星の部分をリストと for で書き直す.座標・色・大きさをタプルのリストにまとめると,1 つの for で済みます.
  3. 点数の部分をリストにして,summaxlen で書き直す.
  4. 名前のリストと点数のリストを zip で組にし,sorted(…, key=…, reverse=True) で高い順に並べて,enumerate で順位を付けて表示する.
  5. 合格者(60 点以上)の名前を,内包表記で取り出して表示する.
  6. 何行から何行に減ったか数えて,コメントに書く.空行とコメントを除いた行数で数えてください.
ヒント さらに進めたい人は,自分が前に作った作品で同じことをしてください. 第 11 章(ぱらぱら漫画)でコマをべた書きした人, 第 16 章(リスト)で同じ図形を並べた人は, 今回の道具で見ちがえるほど短く書けるはずです. 書き直す前と後の両方を残しておいてください.
注意 短くすることが目的ではありません.読みやすくすることが目的です. 「短くなったが,何をしているか分からなくなった」と感じたら, そこは戻してかまいません.その判断こそがリファクタリングの腕です.
自由演習 19-6 自由制作:データを絵にする

自分でデータを決めて,それを絵にしてください.題材は自由です.

  • 1 週間の勉強時間,好きな音楽の再生回数,科目ごとの点数,日ごとの気温,ゲームのスコア,都道府県の面積……何でもかまいません
  • リスト・タプル・辞書・集合のうち 3 つ以上を使うこと
  • 並べ替えsorted)を必ず使うこと
  • スライス・内包表記・リストのリストのうち,少なくとも 1 つを使うこと
  • 数だけでなく,名前や単位も画面に書くこと(write).絵だけでは読み手に伝わりません
  • 先頭にコメントで,題名・氏名・日付・何を表した絵かを書くこと
ヒント 絵の形は棒グラフでなくてもかまいません. 円の大きさ,線の長さ,色の濃さ,かめの位置,マス目の塗り分け— 数の大きさを何かの見た目に対応させるのがグラフの本質です. 面白い対応のしかたを考えてみてください.

まとめ

  • 入れ物は 4 つ.リスト(順番がある・増やせる)・タプル(変えられない・キーになれる)・辞書(名前で引く)・集合(重複が消えて順番が無い).
  • 他の言語の配列は「大きさが決まっているリスト」.Python のリストには,その不便さが無い.
  • 空の集合は set(){} は空の辞書.
  • lensummaxminanyallenumeratezip は,入れ物の種類を選ばずに使える.
  • スライス a[1:4] は「4 番目の手前まで」.a[::2] は 1 つおき,a[::-1] は逆順,a[:] はコピー.b = a はコピーではない.
  • リストのリストでマス目を表せる.masu[行][列]の順.二重の for でたどり,そのまま絵にできる.
  • 内包表記 [f(x) for x in a] で 4 行が 1 行になる.ただし短ければよいわけではない.
  • sorted(a, key=関数, reverse=True) で,好きな基準で並べ替えられる.
  • in は,リストでは端から順に探し,辞書と集合では直接見る.データが多いときに大きく差がつく.

次の回は最終回です.ここまでに拾いきれなかった Python の文法— breakcontinueandornot,文字列の扱いと f-stringtry による入力の失敗への備え—を,総まとめとして見わたします. そして,この教材では扱わなかったことと, その先に何があるのかもお話しします.