5.5 タプル、集合型、辞書型

(1/1)
通知表のイラスト
今回は、コンテナデータ型のタプル集合型辞書型の使い方を学ぶ。
最後に、コンテナデータ型を活用してクラスの成績表を作り、個人合計点や科目毎平均点を計算するプログラムを作る。

目次

サンプル・プログラム

タプル

Python には、順序をもつデータ構造として、リスト配列以外にタプルがある。
# タプル
# 東京駅 北緯35.681111, 東経139.766667
tokyoStation = (35.681111, 139.766667)
print(tokyoStation)
print(f"北緯{tokyoStation[0]:2.3f}, 東経{tokyoStation[1]:3.3f}")
プログラム "tuple1.py" を実行してみてほしい。
タプルは、要素をカンマ , で並べるところはリストと同じだが、丸括弧 (...) で囲む。
要素へのアクセスは、リストと同じく、タプル名[インデックス] で行う。
tokyoStation[0] = 36.1
タプルリスト配列と異なるのは、要素の変更が許されていないことだ。要素の並べ替えもできない。
ある地点の緯度・経度のように、複数の値がセットになった固定値を表すのに適している。
5.4 リストと配列」では、トランプのカードをリストに見立てたが、52枚のカードのスートとナンバーは決まっている。そこで、カードの1枚1枚を、スートとナンバーの2組のデータから成るタプルに見立ててみよう。
山札と手札のデータ構造
# スート一覧
Suits = ( "spade", "heart", "diamond", "club" )

def initCards(stocks):
	"""山札を用意する
	
	Args:
		stocks(list): 山札を格納するリスト
	
	Returns:
		None
	"""
	# 山札に4×13枚を加えていく
	for s in Suits:
		for i in range(1, 14):
			stocks.append((s, i))

def shuffleStocks(stocks):
	"""山札をシャフルする
	
	Args:
		stocks(list): 山札を格納するリスト
	
	Returns:
		None
	"""
	random.shuffle(stocks)

def stocks2hands(stocks, hands, n, id):
	"""山札stocksの上からn枚を取り出してプレイヤーidの手札handsに加える.
	
	Args:
		stocks(list):	山札
		hands(list):	手札
		n(int):			取り出す枚数
		id(int):		プレイヤーID
	
	Returns:
			bool: True 成功 / False 失敗(山札が足りない)
	"""
	# 山札が足りているかどうか
	if (len(stocks) < n):
		return False

	# 山札から取り出す
	sn = slice(None, n)
	# 手札に加える
	hands[id].extend(stocks[sn])
	# 山札から削除する
	del stocks[sn]

	return True
# メイン・プログラム =======================================================
# 山札を空にする
stocks = []
# 場札を空にする
layouts = []
# 手札を空にする
hands = [ [], [], [], [], [] ]

# 山札を用意する
initCards(stocks)
# 山札をシャフルする
shuffleStocks(stocks)
# 山札からプレイヤー#0に5枚のカードを配る
stocks2hands(stocks, hands, 5, 0)

# プレイヤー#0の手札を表示
print(hands[0])
プログラム "shaffle2.py" を実行してみてほしい。
ユーザー定義関数 initCards は引数で与えられたリスト stocks にスート4×ナンバー13個のタプルを append していく。
ユーザー定義関数 shuffleStocks は、引数で与えられたリストをシャフルする。リストの要素を順序をシャフルするだけなので、要素の実体であるタプル(スート, ナンバー)の順序は変わらない。実際のカードをシャフルするのと同じ動きになる。
ユーザー定義関数 stocks2hands は、山札リスト stocks の上から n枚を取り出してプレイヤーリスト id の手札リスト handsに加える。

このようにリストタプルを活用することで、リアル世界にあるデータ(ここではトランプのカード)を忠実に Pythonプログラムで扱えるデータ構造に置き換えることができる。ここで学んだデータ構造を活用し、「6.2 ユーザー定義クラス」ではポーカーの役を判定するプログラムを作ってみることにする。

集合型

これまで見てきたコンテナデータ型は要素に順序(シーケンス)があった。これをシーケンスと呼ぶ。
前回の一覧表に示したように、Python には順序をもたないコンテナデータ型がある。集合型(set)がそれだ。
# 集合型
vegetables = { "キャベツ", "ニンジン", "トマト" }
fruits1    = { "リンゴ",   "ミカン",   "トマト" }
fruits2    = { "ミカン",   "リンゴ",   "トマト" }
プログラム "set1.py" を実行してみてほしい。
集合型は、要素をカンマ , で並べるところはリストと同じだが、全体をブレース {...} で囲む。
# ニンジンはどちらの集合に含まれているか
s = "ニンジン"
print(s in vegetables)
print(s in fruits1)
集合型に要素が含まれているかどうかは、リストと同様、in演算子を使って判定できる。
# 要素が同じで順序の違う集合の比較
print(fruits1 == fruits2)
集合型には要素の順序(シーケンス)がない。要素の順序が異なっていても、含まれている要素が一致すれば、2つの集合は等しくなる。
# 要素を加える
fruits1.add("スイカ")
print(fruits1)

# すでにある要素を加える
fruits1.add("ミカン")
print(fruits1)
集合型に要素を加えるには appendメソッドを使う。重複する要素を加えても何も起こらない(例外も発生しない)。数学の集合と同じものだとを考えてほしい。
# 要素を削除する
fruits1.remove("ミカン")
print(fruits1)
集合型から要素を削除するには removeメソッドを使う。
次に、プログラム "set2.py" を実行してみてほしい。
2つの集合の和集合は |演算子で、積集合は &演算子を使って簡単に求めることができる。
集合型
# 集合型
vegetables = { "キャベツ", "ニンジン", "トマト" }
fruits     = { "リンゴ",   "ミカン",   "トマト" }

# 和集合
s1 = vegetables | fruits
print(s1)

# 積集合
s2 = vegetables & fruits
print(s2)

辞書型

辞書型(dict)は、集合型と同じく順序(シーケンス)はないが、要素をキーによって識別することができる。配列のインデックスが文字列になったデータ型と考えてもらっていい。PHPなど他の言語では、連想配列と呼ばれるデータ構造である。
たとえば、下表のような1人分の成績表(データの集まり)を辞書型で表現してみよう。
成績表(1人分)
出席番号国語算数理科社会教科合計
17水野亜美681005470292
# 1人分の成績表を辞書型で表す.
score = { "ID" : 17, "lastName" : "水野", "firstName" : "亜美",
	"国語" : 68, "算数" : 100, "理科" : 54, "社会" : 70 }
プログラム "testScore1.py" を実行してみてほしい。
辞書型は、ダブルクォーテーション(またはシングルクォーテーション)で囲んだ文字列がキー、コロン : を挟んで右側の値が要素である。キー要素のセットをカンマ , で並べ、全体をブレース {...} で囲む。また、ブレース {...} の途中で適当に改行してもデータには影響しない。キーに日本語が使える。
# 国語・算数・理科・社会の合計点を求める
# 単純な足し算
totalScore1 = score["国語"] + score["算数"] + score["理科"] + score["社会"]
print(f"4教科合計点 = {totalScore1}")
辞書型の要素を参照するには、辞書名["キー"] と書く。
ここでは、キーの国語、算数、理科、社会に対応する要素を加算することで、個人の4教科合計点数を計算できる。
# for文を使う
subjects = ( "国語", "算数", "理科", "社会" )	# 辞書キーをタプルに用意する
totalScore2 = 0
for key in subjects:
	totalScore2 += score[key]
print(f"4教科合計点 = {totalScore2}")
科目名のキーをタプル subjects として別に用意しておく。科目名は変更になることがないので、タプルにした。
for文 を使うことで、キーに対応する要素を1つずつ取り出して加算することで、4教科の合計点数を計算する。
# sum関数を使う
totalScore3 = sum(score[key] for key in subjects)
print(f"4教科合計点 = {totalScore3}")
sum関数 には for文を組み合わせることができ、これを使えば合計点をわずか1行で計算することができる。
Python には非常に多くの関数やメソッド、文法があるが、このように、合計点を求めるという目標を達成するのに複数のプログラムの書き方があり、Python のすべてをマスターしていなくても目標を達成することはできるだろう。

クラス全員の成績表

次に、クラス全員の成績表を処理するプログラムを作ってみよう。プログラム "testScore2.py" を実行してみてほしい。
クラスの成績表はExcelに下表のような形式で記入されているものとする。CSVファイルからデータを読み込むこともできるが、まだファイル入出力を学んでいないので、ここでは、Excelの成績表(見出し行を含む)を冒頭の文字列 contents にコピー&ペーストして使うことにする。
クラスの成績表
出席番号国語算数理科社会
1相沢優利83633181
2相原莉緒77687779
3東青731004288
4池上行夫71984076
5石川詩乃688248
6岩本寿84535489
7江田圭介80665579
8771006188
9奥村空斗80917083
10小田沙都希72643184
11金谷聖子61703491
12川越翔風71937081
13桜田愛海74785589
14志賀聖人67886189
15下田671004977
16下村66799077
17水野亜美681005470
18土田祥真86736085
19坪田明澄821004788
20寺島77326189
21豊島菜々64604687
22仲田璃子67894787
23並木樹里74665587
24奈良保夫75617088
25野口莉乃69545280
26野島絵美梨79725582
27洋臣89925179
28平本雅月84715282
29藤谷珠希55876489
30藤本稜真661006182
31水上花名77764785
32水口凛香86674677
33宮島蒼羽62805086
34宮本七緒741007183
35村田夏織77574179
# 成績表(Excelからコピー&ペースト)
contents = """
出席番号	姓	名	国語	算数	理科	社会
1	相沢	優利	83	63	31	81
2	相原	莉緒	77	68	77	79
3	東青	冴	73	100	42	88
4	池上	行夫	71	98	40	76
5	石川	詩乃	68	82	48	
6	岩本	寿	84	53	54	89
7	江田	圭介	80	66	55	79
8	奥	楓	77	100	61	88
9	奥村	空斗	80	91	70	83
10	小田	沙都希	72	64	31	84
11	金谷	聖子	61	70	34	91
12	川越	翔風	71	93	70	81
13	桜田	愛海	74	78	55	89
14	志賀	聖人	67	88	61	89
15	下田	紬	67	100	49	77
16	下村	蒼	66	79	90	77
17	水野	亜美	68	100	54	70
18	土田	祥真	86	73	60	85
19	坪田	明澄	82	100	47	88
20	寺島	紫	77	32	61	89
21	豊島	菜々	64	60	46	87
22	仲田	璃子	67	89	47	87
23	並木	樹里	74	66	55	87
24	奈良	保夫	75	61	70	88
25	野口	莉乃	69	54	52	80
26	野島	絵美梨	79	72	55	82
27	畑	洋臣	89	92	51	79
28	平本	雅月	84	71	52	82
29	藤谷	珠希	55	87	64	89
30	藤本	稜真	66	100	61	82
31	水上	花名	77	76	47	85
32	水口	凛香	86	67	46	77
33	宮島	蒼羽	62	80	50	86
34	宮本	七緒	74	100	71	83
35	村田	夏織	77	57	41	79
"""

# 改行で分割してリストに代入する
lines = contents.strip().split("\n")
print(lines)
Excelのようなスプレッドシート型のデータを Pythonコンテナデータ型として扱いたいときは、まず1行ずつ分割する。この1行をレコードと呼ぶ。そして、その1行から、さらに1つ1つの列に分割する。1列をカラムと呼ぶ。
Python の文字列オブジェクトには stripメソッドsplitメソッドが用意されており、これを使うことで改行文字(エスケープシーケンス \n)で1行ずつに分割し、lines というリストへ代入する。つまり、splitメソッド でリストに分割し、stripメソッドを使って区切り文字を消去する。
# 改行で分割してリストに代入する
lines = contents.strip().split("\n")
print(lines)
カラムに分割するには splitメソッドを使って、区切り文字のタブ文字(エスケープシーケンス \t)で分割してリスト keys に代入する。
# 1行目をキーとしてタプルに代入する
keys = tuple(lines[0].split("\t"))
print(keys)
残りの行はデータとして辞書リスト scores に代入する。scores はリスト型で、要素1つに1人分の成績表である辞書型データが入る。
見出し(0番目)を除くリストは lines[1:] で表すことができるので、これを for文を使って処理していく。1行をカラム(列)に分割するには、先ほどやったようにsplitメソッドを使う。
# 残りの行をデータとして辞書リストに代入する
scores = []
for line in lines[1:]:
	values = line.split("\t")
	record = {}
	i = 0
	for key in keys:
		record[key] = values[i]
		i += 1

	scores.append(record)

# 画面に成績表を表示する.
print(scores)
さて、画面に表示すると、辞書リスト scores の点数が文字列型であることがわかる。元のデータ contents が文字列型なので、当然の結果である。このあと、点数の合計計算するときに不便なので、数字があったら数値型で辞書リスト scores に格納するようプログラムを改良してみよう。
# 残りの行をデータとして辞書リストに代入する
scores = []
for line in lines[1:]:
	values = line.split("\t")
	record = {}
	i = 0
	for key in keys:
		record[key] = int(values[i]) if values[i].isdigit() else values[i]
		i += 1

	scores.append(record)

# 画面に成績表を表示する.
print(scores)
プログラム "testScore3.py" を実行してみてほしい。
改良したのは、リスト record に代入する部分だ。isdigitメソッドは、文字列が数字かどうかを判定する。これを後述する条件式により、数字なら int関数を使って整数型に変換して代入する。
だが、まだ改良の余地がある。
# 残りの行をデータとして辞書リストに代入する
scores = []
for line in lines[1:]:
	values = line.split("\t")
	record = { key: (int(value) if value.isdigit() else value) for key, value in zip(keys, values) }
	scores.append(record)

# 画面に成績表を表示する.
print(scores)
プログラム "testScore4.py" を実行してみてほしい。リスト record を1行で生成している。
int(value) if value.isdigit() else value の部分は "testScore3.py" と同じだが、このあとの for文に注目してほしい。この for文では、リスト keys から1つずつ取りだし変数 key に代入するのと同時に、リスト values から1つずつ取りだし変数 value に代入するのを同時に行っている。こういうときに zip関数を使う。

条件式

プログラム "testScore3.py" の説明で「条件式」が登場したが、ここで改めて説明する。
		record[key] = int(values[i]) if values[i].isdigit() else values[i]
右辺に ifelse が登場するが、最後にコロン \A; がないので、これは if文ではない。
これは条件式といって、式の一種だ。一般に "A if B else C" という書き方をして、Bが成り立つとき(if)にはA、そうでないとき(else)にはCを返す。
上記プログラムでは、values[i] が数字なら(isdigit)int(values[i]) を、それ以外なら values[i] を返す働きをする。
if文で表すと下記のようになるが、これを1つの式にまとめた形である。
if values[i].isdigit():
    record[key] = int(values[i])
else:
    record[key] = values[i]

合計点・平均点を計算する

次に、クラス全員の点数合計や科目別平均点を計算し、表形式に整形して画面に出力するように改良する。プログラム "testScore5.py" を実行してみてほしい。
# 計算対象とする科目(キー)
subjects = ( "国語", "算数", "理科", "社会" )
まず、計算対象となる教科(見出し行の文字列)をタプル subjects に用意しておく。
# 見出し行
str = ""
for key in keys:
	str += f"{key:4} "
print(str)
見出し行は、for文を使って実装した。
# 成績表
for score in scores:
	str = ""
	for key in keys:
		str += f"{score[key]:4} "
	# 個人の得点合計
	totalScore = sum(score[key] for key in subjects if isinstance(score[key], (int, float)))
	str += f"| {totalScore:4}"
	print(str)
print("-" * 60)
次に成績表の表示だが、for文を使って1人=1行を表示する。
合計得点を計算するのに sum関数を使っているのは "testScore1.py"と同じだが、じつは、児童によっては得点が入っていない科目がある。その科目を欠席したという想定だ。この場合、辞書リストに空文字が入るため、sum関数から外さないと例外が発生してしまう。
ここで登場するのが isinstance関数だ。isinstance(オブジェクト, データ型) のようにして使い、オブジェクトがデータ型(またはそのサブクラス)に一致すると True を返す。得点がfloatになることはないのだが、ここでは (int, float) というタプルを指定し、intまたはfloatであれば sum関数を適用するようにした。
# 科目別平均値
totalAverage = 0
str = " " * 20
for key in subjects:
	scoreData = [score[key] for score in scores if isinstance(score[key], (int, float))]
	average = sum(scoreData) / len(scoreData)
	totalAverage += average
	str += f"{average:3.1f} "
str += f"| {totalAverage:3.1f}"
print(str)
最後に科目別平均値を計算する。
辞書リストを縦方向に計算することになるわけだが、ここでも sum関数を適用できる。
リスト scoreData には、1科目分の点数が要素として入っていく。このとき、isinstance関数を使って数字以外は除外する。あとは、sum関数を使って科目毎合計を計算し、len関数を使って求まる要素の数で割れば科目毎の平均値が算出できる。

今回は半角空白と全角空白の違いを配慮していないので、横方向がズレてしまう行があるのはご容赦願いたい。

Python では、リスト辞書型を活用することで、Excelのようなスプレッドシート型のデータを扱うことが出来る。また、繰り返し制御を使わずに合計や平均を計算することができる。

長さの単位を変換する

1センチをインチに変換するなど、度量衡の変換はプログラムが得意とするところだ。
Python では 辞書型を使って、容易に度量衡を変換することができる。そこで、長さの単位を変換するプログラム "unitConvert.py" を作ってみた。
GUIとして、「1.4 ブラウザを使ったGUI」で紹介した pywebview を利用する。あらかじめ pywebview をインストールしておいてほしい。また、HTML部分を独立したファイル "unitConvert.html" にしており、Pythonプログラム "unitConvert.py" と同じディレクトリに配置すること。
変換辞書 TableConvert は、長さの単位をキーに、その長さメートルで表したときの値を要素としている。単位変換は、この変換辞書1つだけで賄うようにする。また、ここにある単位以外も自由に辞書に追加できるものとする。
def unitConvert(num, sour, dest):
	"""長さの単位を変換する.
	
	Args:
		num(float): 長さ
		sour(str) : 変換元の単位
		dest(str) : 変換先の単位
	
	Returns:
		float: 変換後の長さ
	"""

	# 引数のバリデーション
	if not sour:
		raise ValueError(f"変換元の単位が指定されていません")
	elif not sour in TableConvert:
		raise ValueError(f"{sour} は変換できません")
	
	if not dest:
		raise ValueError(f"変換先の単位が指定されていません")
		return Null
	elif not dest in TableConvert:
		raise ValueError(f"{dest} は変換できません")
	
	# Decimal型に変換してから計算する
	try:
		numDec = decimal.Decimal(num)
	except Exception as errmsg:
		raise ValueError(f"{num}に数値化できない文字が含まれています")
	
	# いったんメートルに変換する
	numDec = numDec * decimal.Decimal(TableConvert[sour])
	# 変換先の単位に変換する
	numDec= numDec / decimal.Decimal(TableConvert[dest])
	
	return float(numDec)
変換処理はユーザー関数 unitConvert で行う。
引数は、変換元の長さ、変換元の単位(辞書キー)、変換先の単位(辞書キーの3つ)。関数の前段で、これらの引数のバリデーションを行う。
変換処理だが、まず、浮動小数演算誤差を避けるため、数値をDecimal型に変換する。
次に変換元の長さをメートルに変換し、続いて変換先の単位に変換する。このように2段階で変換することで、上述の変換辞書 TableConvert だけで単位の変換を実現できる。

練習問題

次回予告

Python などのモダンなプログラミング言語では、コンピュータが扱う全てのデータをオブジェクト(またはインスタンス)と呼ぶ。数値や文字列はもちろん、画像や音声データ、そしてプログラムもオブジェクトだ。
オブジェクトの設計図(定義)をクラスと呼ぶ。クラスの中ではオブジェクトを操作するメソッドを定義する。そして、クラスを設計してオブジェクトを処理するプログラミング手法をオブジェクト指向プログラミングと呼ぶ。
次回は、Python のデータ型がクラスであり、データの実体がオブジェクトになっていることを学ぶ。

コラム:パーサ

プログラミング言語にはコンパイラ、インタプリタ、アセンブラがあると紹介したが、いずれの場合も、まず、入力したプログラム(テキスト)を解釈する作業が必要である。
まず、プログラムコードを制御命令や関数、変数、演算子と行った字句に分割する字句解析(C言語のlex)、次に分割された字句を解釈してコンピュータが実行できる命令に変換するパーサー(C言語ではyacc)の2つが最低限必要となる。

そのプログラミング言語を使って自分自身のパーサーをプログラミングできるかどうかは、チューリング完全性の必要条件なのだが、もちろん Python はこの条件をクリアしている。Python は単純であるがゆえに簡単に字句解析やパーサを作ることができ、多くのOSに移植される原動力となっている。制御文の末尾がコロン : で明示され、ブロックは必ずインデントを要求するという文法が、パーサーの負担を減らしているのだ。
(この項おわり)
header