図形のデータを設計する
同じ作品を,データの選び方と関数の分け方から見直す.
- リスト・タプル・辞書・集合を選んだ理由を説明する
- 生成・更新・描画を分けて,絵を描かずに更新を検証する
- 元の値を変更する関数か,新しい値を返す関数かを決める
21.1 まず作品の仕様を書く
色と大きさの違う四角形を,指定した順に描く作品を作ります.後から移動・並べ替え・保存を追加できるようにします.最初に『何を覚えるか』『どの操作で何が変わるか』を日本語で書きます.
| 覚えたいこと | 選ぶ型 | 理由 |
|---|---|---|
| 1個の座標 | タプル (x, y) | 2つの値で1組.移動時は組を置き換える |
| 図形の属性 | 辞書 | position・size・color の名前で読む |
| 作品の図形 | リスト | 重なりの順番を保持し,同じ図形も入れられる |
| 使用している色 | 集合 | 重複を除く.描画順には使わない |
21.2 生成・更新・描画を分ける
create_shape は値を検査して辞書を作ります.move_shape は元の辞書を書き換えず,移動後の辞書を返します.draw_shape だけが Turtle を操作します.計算と画面を分ければ,移動の正しさを assert で調べられます.
raise ValueError("理由") は,その場で自分から例外を起こして処理を止める文です.create_shape では,大きさが0以下なら辞書を作らずに止めます.受け取る側は第20章の try/except ValueError で,理由を表示したり別の処理へ進んだりできます.
また {**shape, "position": 新しい値} は,shape の中身を写した新しい辞書を作り,position だけを置き換える書き方です.
from turtle import *
def create_shape(position, size, color):
if size <= 0:
raise ValueError("大きさは正にしてください")
return {"position": tuple(position), "size": size, "color": color}
def move_shape(shape, dx, dy):
x, y = shape["position"]
return {**shape, "position": (x + dx, y + dy)}
def draw_shape(pen, shape):
pen.penup()
pen.goto(*shape["position"])
pen.setheading(0)
pen.pencolor(shape["color"])
pen.pendown()
for _ in range(4):
pen.forward(shape["size"])
pen.left(90)
shapes = [create_shape((-120, 0), 50, "blue"),
create_shape((0, 0), 80, "orange")]
original = shapes[0]
shapes[0] = move_shape(shapes[0], 0, 50)
assert original["position"] == (-120, 0)
pen = Turtle()
pen.hideturtle()
pen.speed(0)
for shape in shapes:
draw_shape(pen, shape)
print("使用色:", sorted({s["color"] for s in shapes}))
done()
実習:データの形と操作の関係を確かめる
すでに学んだリスト・辞書・タプル・集合を,1つの図形作品で使い分けます.各例題は単独で実行できます.選んだ構造がどの操作を簡単にするかを説明してください.
実習21-1 まず,残したい情報を言葉で列挙する
図形の作品を作るとき,いきなりリストや辞書を選ぶ前に,何を覚える必要があるかを書き出します.
例えば「左下の位置」「辺の長さ」「線の色」「描く順番」です.次に,どの操作で何が変わるかを考えます.
移動なら位置,拡大なら辺の長さ,色の変更なら線の色が変わります.図形を選ぶ操作では,表示されている図形を特定するための目印も必要になるかもしれません.
情報を列挙したら,1個の図形に属する値と,作品全体に属する値へ分けます.
辺の長さは各図形が持ちますが,図形の並び順は一覧側の情報です.ここを混ぜると,図形自身に「私は3番目」と保存し,一覧を並べ替えるたびに全図形の番号を直すような設計になります.
元のデータから求められる値を二重に保存する前に,その必要があるかを考えましょう.
最初の表現は,変数だけでも構いません.
そこから図形を増やしたときにどこが煩雑になるかを観察し,まとまりを導入します.データ構造は,文法問題の正解を当てるために選ぶのではなく,実際に行いたい操作を簡単に,間違いにくく表すために選びます.
同じ作品でも,保存する場面と編集する場面で別の表現へ変換することがあります.
以下では1個の図形を辞書へまとめています.
キーの名前だけで,値の意味を読み取れるか確認してください.sやcという短いキーでも動きますが,あとで読む人が意味を調べる負担が増えます.
一方,すべての値へ長い説明を付ける必要もありません.位置はposition,大きさはsizeのように,教材内で名前をそろえることから始めます.
shape = {"position": (10,20), "size": 40, "color": "blue"}
print("位置", shape["position"])
print("面積", shape["size"] ** 2)
図形にnameキーを追加し,describe(shape)を作ってください.『青い四角: 位置(10, 20) 面積1600』という文字列を返し,nameキーがない図形ではValueErrorを出します.図形の順番はこの辞書へ保存せず,一覧側で管理する理由も説明してください.
実習21-2 座標は,2つで1組の値として扱う
xとyを別々のリストへ保存すると,追加や削除を片方だけ行って対応が崩れることがあります.
1個の座標は(x,y)というタプルにまとめ,一覧は座標のリストにすると,対応を保ちやすくなります.タプルは要素をその場で変更しないまとまりなので,位置を変える際は新しいタプルを作って置き換えます.
位置という1組の値を丸ごと更新する発想です.
タプルの展開を使うと,x,y = positionと書けます.
このとき,右側の値が2個であることが前提です.3個の値が入っていたら,座標の形式が違うことに気付けます.
添字だけを使う場合も,position[0]がx,position[1]がyという意味をそろえてください.同じ種類のデータで順序を入れ替えないことが大切です.
移動量も(dx,dy)という組で表せますが,位置と意味は違います.
位置は基準点からどこにあるか,移動量はどれだけ変えるかです.どちらも2個の数値なので,Pythonの型だけでは区別できません.
変数名と関数の引数名で意味を示します.計算できるというだけで正しい組み合わせとは限らないことは,単位の異なる値を足す問題と似ています.
以下の例では,元の位置と移動量から新しい位置を求めます.
元のタプルは変わりません.2回の移動を行う場合,1回目の結果を2回目の入力へ渡す必要があります.
毎回最初の位置へ移動量を足すと,移動が積み重なりません.どの値を次の処理へ渡すかを,具体的な数値で追って確認してください.
def moved(position, delta):
x,y = position
dx,dy = delta
return x+dx, y+dy
start = (10,20)
next_position = moved(start, (5,-3))
print(start, next_position)
assert start == (10,20)
(10,20)から(5,-3),続けて(-2,8)だけ移動し,結果が(13,25)になることを確認してください.最初の位置startは変更しません.2回目にstartを渡すとどの結果になるかも予測します.
実習21-3 描画順は,重複を許すリストで表す
作品の図形は,リストへ追加した順に描くことにします.
リストは順序を持ち,同じ値も複数回入れられます.重なった位置へ青と赤の図形を描く場合,どちらを後に描くかで見た目が変わります.
このため,図形を集めるだけだからという理由で集合へ置き換えると,作品の意味が失われる場合があります.必要なのは,重複の除去ではなく順序の保持です.
同じ形と同じ位置の図形が2個あっても,作品の仕様によっては別の要素として扱います.
例えば,片方だけを選んで移動する操作を想定するなら,値が同じでも別の図形であることを区別したいでしょう.この問題は後でIDを導入して扱います.
まず,リストに同じ内容を入れられることと,個々の要素をどう特定するかは別の話だと理解します.
描画順を反転する場合,reversedやスライスを使えます.
元の一覧を変えずに逆順で読む方法と,reverseで一覧自体を変更する方法を区別してください.一時的な表示だけを逆順にしたいなら,元の一覧を保つほうが扱いやすい場合があります.
編集操作として前後関係を変えたいなら,作品の一覧自体を変更する必要があります.
以下では色の名前だけを使い,順番をコンソールで観察します.
同じblueが2回あることも確認してください.絵を描く前に,出力される順番でデータの性質を調べられます.
次の演習では,先頭と末尾を入れ替えます.空のリストや1個だけのリストも想定すると,添字を使う前に長さを確認する必要があることが分かります.
colors = ["blue", "red", "blue"]
for color in colors:
print(color)
print("逆順", list(reversed(colors)))
print("元の一覧", colors)
先頭と末尾を入れ替えた新しいリストを返す関数を作ってください.空と1個の場合もそのまま返し,元のリストは変更しません.[青,赤,緑]なら[緑,赤,青]になることを確認します.
実習21-4 辞書の形をそろえる約束を作る
辞書は自由にキーを追加できますが,図形の一覧で使う場合は,必要なキーをそろえないと処理が複雑になります.
ある図形はposition,別の図形はposというキーを使っていると,描画関数で毎回違いを調べなければなりません.今回の図形はposition,size,colorを必須とする,という形式の約束を先に決めます.
このようなデータの構造の約束は,保存や交換をする場合にも重要です.
必須の値と省略できる値も区別します.
例えばnameは省略したら空文字列にする,という設計が考えられます.一方,sizeがない場合に勝手に0へ置き換えると,図形が見えない原因を分かりにくくします.
必要な情報が欠けている場合は,早い段階で知らせるほうがよいこともあります.getを使えば常に安全,というわけではありません.
生成関数を用意すると,キーの名前と既定値を1か所で決められます.
図形を作る側は,毎回同じ辞書の形を書き直さずに済みます.ただし,渡した位置が数値の組か,大きさが正かなどの検査は別に必要です.
まず形式をそろえる役割を生成関数へ集め,実習21-13で値の検査を加えます.責任を少しずつ増やし,どの約束が守られるか確かめます.
以下では,同じ関数から2個の図形を作ります.
片方のsizeを変更しても,もう片方の辞書は変わりません.関数が呼ばれるたびに新しい辞書を作るためです.
もし関数の外に作った1個の辞書を毎回返したら,図形同士が共有されます.生成関数は,値をそろえるだけでなく,どこで新しい入れ物を作るかも明確にします.
def create(position, size, color="blue"):
return {"position":tuple(position),"size":size,"color":color}
a = create((0,0),30)
b = create((50,0),40,"red")
a["size"] = 60
assert b["size"] == 40
print(a,b)
createにnameという省略可能な引数を追加し,既定値を空文字列にしてください.名前を省いた図形にもnameキーが存在する形にします.既存の3引数での呼び出しが引き続き使えることを確認します.
実習21-5 番号と,変わらないIDを区別する
リストの添字は,現在の並びの中での位置です.
先頭の図形を削除すれば,後ろの図形の添字は変わります.そのため,選択中の図形を「2番目」とだけ覚えると,削除や並べ替えの後に別の図形を操作してしまうことがあります.
図形そのものを特定したい場合は,並び順と独立したIDを持たせる方法があります.IDは名前とは別にし,同じ表示名の図形も区別できるようにします.
IDに整数を使うか文字列を使うかは自由ですが,作品内で重複しない約束が必要です.
この節ではa,b,cという文字列を使います.番号が短いからといって,リストの添字と同じ意味にしないでください.
IDは図形の識別に使い,描画順はリストの順序で表します.役割が違う情報を分けておくと,後から並べ替えても選択対象を保てます.
検索の結果が見つからない場合の扱いも決めます.
選択した図形が削除された後には,そのIDに対応する図形は存在しません.この例ではNoneを返し,利用側で選択を解除するなどの処理を行えるようにします.
常に存在するはずだと考えて先頭要素を取り出すと,空の結果でIndexErrorになるため注意してください.
例題では一覧を逆順にした後も,IDがbの図形を探します.
添字を使う場合との違いを観察しましょう.将来,作品を保存して読み戻す際にも,IDを維持するか新しく付け直すかを決める必要があります.
この段階では1つの作品内での識別に範囲を絞り,重複と未登録の2つのケースを扱えるようにします.
shapes = [{"id":"a","size":10},{"id":"b","size":20},{"id":"c","size":30}]
def find(items,key):
for item in items:
if item["id"] == key:
return item
return None
shapes.reverse()
print(find(shapes,"b"))
assert find(shapes,"missing") is None
IDの重複があるかを調べる関数を作ります.IDの個数と,集合にした後の個数を比較してください.空の一覧は重複なし,同じIDが2個なら重複ありとします.表示名が同じでもIDが違う図形は許します.
実習21-6 集合は,使用色や所属の問い合わせに使う
描画順にはリストを使いますが,「何色を使っているか」を重複なしで知りたい場合は集合が適しています.
青い図形が10個あっても,使用色としてはblueが1つあればよいからです.同じ作品のデータでも,操作の目的によって取り出す構造が変わります.
元の図形一覧を集合へ置き換えるのではなく,必要な情報を取り出して別の集計結果を作ります.
集合は,ある色が使われているかをinで調べる場面にも使えます.
ただし,集合の表示順は作品の順序を表しません.コンソールへ一定の順で表示したいなら,sortedで並べてから使います.
順序が必要なのは表示の都合なのか,作品の意味として必要なのかを区別してください.使用色の一覧を並べ替えても,図形の描画順は変えません.
2つの作品を比較する場合には,集合の積・和・差を使えます.
共通して使う色は積,一方だけの色は差,どちらかで使う色は和です.作品同士を比べる分析へ,データ構造の操作がつながります.
同じ色の図形が何個あるかは集合だけでは分からないので,個数を調べたいときは次の集計を使います.重複を捨てることには,情報を失う側面もあります.
例題では2つの色一覧を集合へ変換しています.
共通色と片方だけの色を予測してください.結果を表示するときはsortedを使い,比較しやすくします.
次の課題では空の作品を含めて確認します.空集合はset()と書き,{}は空の辞書になることも復習してください.
形が似た記号ほど,具体的な操作で違いを確かめます.
first = {"blue","red","blue"}
second = {"red","green"}
print("共通", sorted(first & second))
print("最初だけ", sorted(first - second))
print("全部", sorted(first | second))
使う書き方:リスト内包表記の [式 for 要素 in リスト] を {式 for 要素 in リスト} にすると,重複を除く集合内包表記になります.例えば {n % 2 for n in [1, 2, 3]} は {0, 1} です.空のリストからは空集合ができます.
図形の辞書のリストから使用色の集合を作る関数を定義してください.空の作品では空集合を返します.2つの作品の共通色がredだけになる例を作り,assertで確かめます.
実習21-7 色ごとの個数を辞書へ集計する
使用色の集合だけでは,青い図形が1個なのか10個なのかは分かりません.
個数を知りたい場合は,色をキー,個数を値とする辞書を作ります.まだ登場していない色の個数は0と考え,図形を1個読むたびに1を足します.
第18章で学んだ頻度の集計を,作品のデータへ適用する例です.
get(color,0)を使うと,キーがまだない場合を0として扱えます.
ここでは0が「まだ1個も数えていない」という自然な初期値なので,getの既定値が仕様に合っています.一方,必須のsizeがない場合にも0を使うと欠落が見えなくなる,という前の節の話と比較してください.
同じ文法でも,既定値の意味が適切かどうかは用途によります.
集計用の辞書は関数の中で新しく作ります.
前回の集計結果を使い回すと,もう一度集計しただけで個数が増える不具合が起こります.元の図形一覧は読み取るだけにし,結果を別の辞書で返します.
この約束を守れば,同じ入力で何度呼んでも同じ結果になります.後の自動テストでも比較しやすい性質です.
集計が正しいかを確認する方法として,全色の個数の合計が図形の総数と一致するかを調べられます.
個別の色の期待値だけでなく,全体の関係もテストします.ただし,合計が合っていても色を取り違える可能性はあるため,両方の確認が必要です.
小さな具体例と,データ全体で成り立つ関係を組み合わせて検証しましょう.
def count_colors(shapes):
counts = {}
for shape in shapes:
color = shape["color"]
counts[color] = counts.get(color,0) + 1
return counts
shapes = [{"color":"blue"},{"color":"red"},{"color":"blue"}]
counts = count_colors(shapes)
assert sum(counts.values()) == len(shapes)
print(counts)
空の一覧,同じ色が3個,2色が混ざる一覧でcount_colorsをテストしてください.同じ入力で2回呼んでも同じ結果になり,入力のリストが変わらないことを確認します.
実習21-8 色ごとの図形一覧へグループ分けする
個数を数えるだけでなく,同じ色の図形をまとめて処理したい場合があります.
このときは,色をキー,図形のリストを値とする辞書を作ります.リストの中に辞書があり,さらに辞書の中にリストがある構造になりますが,一度に全体を暗記する必要はありません.
外側の辞書は色を選び,内側のリストはその色の図形を順に取り出す,と役割を分けて読みます.
初めて登場した色には空のリストを作り,そこへ図形をappendします.
全色に同じ空リストを使い回すと,青と赤のグループに同じ図形が入る不具合になります.色ごとに独立したリストが必要です.
一方,内側へ入れる図形の辞書は,元の作品の図形を共有するか,新しくコピーするかを仕様から決めます.この例は元の図形を参照する索引として共有します.
索引は,元データを別の観点から探しやすくしたものです.
しかし,図形の色をあとから変更したら,すでに作った色別の索引は古くなります.編集のたびに作り直す方法もあれば,変更時に索引も更新する方法もあります.
小さい作品なら,必要なときに作り直すほうが単純です.同じ情報を複数の場所へ保持する場合は,整合性を保つ負担があることを覚えてください.
例題では,グループごとのIDを表示します.
元のリストの順番が,各グループの中でも保たれることを確認してください.辞書は色を引くために使い,リストは同じ色の図形の順序を保つために使っています.
構造が複雑に見えても,それぞれの入れ物の役割を説明できれば,読み解きやすくなります.
def group_colors(shapes):
groups = {}
for shape in shapes:
color = shape["color"]
if color not in groups:
groups[color] = []
groups[color].append(shape)
return groups
source = [{"id":"a","color":"blue"},{"id":"b","color":"red"},{"id":"c","color":"blue"}]
for color,items in group_colors(source).items():
print(color,[s["id"] for s in items])
青2個,赤1個の一覧をグループ分けし,blueのIDが[a,c],redが[b]になることをassertで確かめます.青グループのリストへ要素を追加したとき,赤グループの個数が変わらないことも確認してください.
実習21-9 選び出す操作では,元の一覧を保つ
大きい図形だけを描く,青い図形だけを選ぶ,画面内にある図形だけを調べる,といった操作は,条件を満たす要素の選び出しとして表せます.
元の作品から図形を削除したいのか,一時的に一部を表示したいのかで,データの扱いは違います.表示を絞るだけなら,元の一覧を保ち,条件に合う要素の新しい一覧を作るほうが自然です.
forとappendで書いても,リスト内包表記で書いても,同じ選択を表せます.
短さだけを優先せず,まず条件が正しいかを確認してください.大きさが50以上なら50を含み,50より大きいなら50を含みません.
日本語の仕様にある境界の意味を,演算子へ対応させることが重要です.
選び出した一覧の要素は,元の図形の辞書を参照しています.
新しいリストを作ったからといって,図形の属性まで独立したわけではありません.選択した図形を編集して作品へ反映したいなら,この共有は役立ちます.
別の作品として変更したいなら,図形のコピーも必要です.選択と複製を別の操作として考えると,意図が明確になります.
例題ではsizeが50以上のIDだけを表示します.
元の一覧の長さも確認してください.条件を満たす図形がない場合,新しい一覧は空になります.
それを異常とする必要があるかは用途によりますが,今回は「表示する図形がない」という正常な結果として扱います.空の場合も同じfor文で処理できる設計を選びます.
shapes = [{"id":"a","size":30},{"id":"b","size":50},{"id":"c","size":80}]
selected = [s for s in shapes if s["size"] >= 50]
print([s["id"] for s in selected])
assert len(shapes) == 3
sizeが30以上70以下の図形だけを返す関数を作ります.境界30と70を含み,29と71を含まないことを確認してください.元の順番を保ち,入力を変更しません.
実習21-10 並べ替えの基準と,同点の扱いを決める
図形を大きさの順に並べるときは,辞書そのものの大小を比較するのではなく,sizeを比較の基準にします.
sortedのkeyには,各図形から比較する値を取り出す関数を渡します.この関数は図形を描いたり変更したりせず,比較に使う値だけを返すようにします.
並べ替えの規則と図形の処理を分ける考え方です.
同じsizeの図形がある場合,どう並べるかも考えます.
Pythonのソートは,同じキーの要素について元の順序を保ちます.元の描画順を維持したいなら,sizeだけをキーにすればよいでしょう.
名前も使って順番を決めたいなら,(size,name)のようなタプルをキーへできます.どちらの仕様かを,期待する一覧で示してから実装します.
sortedは新しいリストを返し,元のリストを変更しません.
list.sortはその場で並べ替え,戻り値はNoneです.並べ替えた結果を変数へ入れたい場合,この違いを忘れると一覧の代わりにNoneを受け取ってしまいます.
どちらの操作を選ぶかは,作品の順序自体を編集したいのか,一時的な表示順を作りたいのかで決めます.
例題ではsizeが同じ2個の図形を含めています.
昇順と降順にしたとき,どちらも同点内の順序が保たれることを観察します.その後でIDを第2のキーにすると,結果がどう変わるか予測してください.
テストは数値の一覧だけでなく,IDの順番でも確認すると,どの図形がどこへ移ったか分かります.
shapes = [{"id":"b","size":20},{"id":"a","size":20},{"id":"c","size":10}]
def order_key(s):
return s["size"]
ordered = sorted(shapes, key=order_key)
print([s["id"] for s in ordered])
assert [s["id"] for s in shapes] == ["b","a","c"]
sizeの昇順,同じsizeならIDの昇順に並べてください.例題の結果は[c,a,b]になります.元の一覧が[b,a,c]のままであることも確認します.
実習21-11 変更する関数と,新しい値を返す関数
移動を表す関数には,受け取った図形を直接変更する設計と,移動後の新しい図形を返す設計があります.
どちらも実装できますが,使う側が違いを理解していないと不具合になります.変更する関数なら戻り値を使わずに呼ぶことが多く,新しい値を返す関数なら結果を受け取らないと移動が反映されません.
名前だけで判断せず,説明とテストで約束を明示します.
この教材のmove_shapeは,元の辞書を保ち,新しい辞書を返す方式です.
移動前と移動後を並べて比較でき,履歴を扱う際にも分かりやすくなります.ただし,大量のデータではコピーの費用も考える必要があります.
初学者の段階では性能を先回りして複雑にせず,何が変更されるかを追いやすい設計を選びます.
新しい辞書を作るときは,変更しない属性も引き継ぎます.
位置だけを返すと,sizeやcolorが失われて描画できなくなります.{**shape, ...}は元の辞書の内容を展開してから指定したキーを置き換える書き方です.
元の属性が増えた場合にも,引き継ぐ処理を毎回書き直さずに済みます.ただし,浅いコピーであることは次の節で確認します.
例題では移動後の辞書だけでなく,元の辞書の位置も表示します.
戻り値を受け取った変数と元の変数が別の辞書であることをisで調べられます.数値が期待どおりでも元の値が変わっていたら,この関数の仕様には反しています.
変更されるべき値と,維持すべき値の両方をテストしましょう.
def moved(shape,dx,dy):
x,y = shape["position"]
return {**shape,"position":(x+dx,y+dy)}
a = {"position":(0,0),"size":20,"color":"blue"}
b = moved(a,10,30)
assert a["position"] == (0,0)
assert b["position"] == (10,30)
print(a,b)
scaled(shape,factor)を作り,sizeだけを倍率倍にした新しい辞書を返してください.元の辞書,位置,色は変えません.倍率2なら辺が2倍,面積が4倍になることを確認します.
実習21-12 入れ子のデータは,どこまで共有するか確かめる
図形にタグのリストを追加すると,辞書のコピーだけでは十分でない場合があります.
dict(shape)や{**shape}は外側の辞書を新しくしますが,中のtagsリストは共有したままです.コピー側へタグを追加すると,元の図形からも同じタグが見えます.
外側の辞書が別であることと,すべての内部データが独立していることは別です.
独立したタグが必要なら,tagsもlist(shape['tags'])で作り直します.
データの構造が決まっていて浅い場合は,どの部分を複製するか明示するほうが分かりやすいことがあります.さらに複雑な入れ子にはcopy.deepcopyも使えますが,何でも深くコピーすればよいわけではありません.
共有したい部品や,複製すべきでない道具もあるからです.
この作品では,位置は数値のタプル,sizeは数値,colorは文字列としていました.
これらはその場で中身を書き換えない値なので,外側の辞書を新しくする方式で扱いやすかったのです.新しい属性を追加すると,それまで十分だったコピーの方法を見直す必要が生じることがあります.
機能追加時はコードだけでなく,データの共有関係も再点検します.
例題では,浅いコピー側からタグを追加して共有を観察します.
これは意図的な実験であり,正しい作品の複製方法として勧めているわけではありません.次の課題で,元の図形を変更しない複製に直してください.
まず現象を再現し,どの入れ物が共有されているかを説明してから修正する流れを練習します.
original = {"position":(0,0),"tags":["四角"]}
shallow = dict(original)
shallow["tags"].append("青")
print(original["tags"],shallow["tags"])
assert original["tags"] is shallow["tags"]
外側の辞書とtagsのリストを両方作り直してください.複製側へ『青』を追加しても,元のtagsは[四角]のままにします.位置のタプルはそのまま共有して構いません.
実習21-13 計算の入口で,前提を検査する
正しい形の辞書でも,sizeが負なら今回の図形の仕様には合いません.
描画関数の奥まで進んでから不思議な絵が出るより,生成や読み込みの入口で理由を知らせるほうが調べやすくなります.必須キーの存在,値の型,値の範囲を分けて検査します.
何を調べているかを分けると,エラーメッセージも具体的にできます.
型を調べるだけでは範囲は保証されません.
整数でも負の大きさはあり得ます.また,Pythonではboolがintの一種なので,Trueを数値の大きさとして許したくない場合には注意が必要です.
この章では整数のsizeだけを許す約束にし,type(value) is intで検査します.後で小数も許す場合は,その仕様に合わせて検査を変更します.
検査に失敗した場合,図形を追加する前に例外を出します.
途中まで作品へ追加してから失敗すると,利用側がどこまで変更されたか判断しにくくなります.できるだけ検査と更新を分け,すべての条件がそろってから状態を変えます.
小さい作品でも,操作の失敗後に元の状態を保つ約束を考えることは重要です.
例題ではsizeを検査する関数だけを取り出しています.
描画をしないので,境界値を何個もすぐに試せます.正常値のほかに,0,負の数,文字列,Trueを試してください.
例外が出ることを確認するテストでは,出なかった場合に失敗させるelseも必要です.tryに続けて書くelse:は,tryの中で例外が起きなかったときだけ実行されます.
ただexceptで何でも受け止めるだけでは,検査が働いていないことを見逃します.
def check_size(value):
if type(value) is not int:
raise ValueError("大きさは整数")
if value <= 0:
raise ValueError("大きさは正")
return value
print(check_size(30))
for bad in [0,-1,"30",True]:
try:
check_size(bad)
except ValueError as error:
print(error)
使う書き方:raise AssertionError("説明") は検証の失敗を知らせます.例外が起きるはずの処理が成功してしまった場合は,try の else でこの例外を起こします.assert False, "説明" と同じ目的で使えます.
整数1〜200だけを許す関数へ変更します.1と200は成功,0,201,文字列,Trueは失敗することを確かめます.不正な値を自動で0へ置き換えず,理由を知らせる仕様にしてください.
実習21-14 描画せずに,作品の広がりを計算する
作品を画面へ収めるには,どこからどこまで図形があるかを知りたくなります.
四角形の左下を(x,y),辺をsizeとするなら,右端はx+size,上端はy+sizeです.すべての図形について左端の最小,右端の最大,下端の最小,上端の最大を求めると,作品全体を囲む長方形を計算できます.
描画の命令を実行しなくても,データだけから求められる情報です.
この計算は,図形の表現の約束に依存します.
positionが中心なら,左端はx-size/2になります.同じキー名を使っていても意味が違えば式も変わります.
データの形式を説明するときは,型だけでなく,座標が図形のどこを指すか,単位は何かまで書く必要があります.保存した作品を別のプログラムで復元する場合にも重要です.
空の作品には,左端や右端がありません.
minやmaxへ空の一覧を渡すとエラーになるため,今回はNoneを返す仕様にします.幅0の長方形を返す設計も考えられますが,原点にある点と空の作品を区別したいならNoneが分かりやすいでしょう.
戻り値の意味を決めてから,利用側の分岐を考えます.
以下の例で,2個の図形を囲む範囲を紙に描いて確認してください.
コードの結果と手計算を比べることで,xとyの取り違えや,右端へsizeを足し忘れる不具合を見つけられます.次の課題では,負の座標と1個だけの場合を含めて確認します.
画面の見た目だけに頼らず,数値で作品の形を理解する練習です.
def bounds(shapes):
if not shapes:
return None
left = min(s["position"][0] for s in shapes)
bottom = min(s["position"][1] for s in shapes)
right = max(s["position"][0]+s["size"] for s in shapes)
top = max(s["position"][1]+s["size"] for s in shapes)
return left,bottom,right,top
shapes = [{"position":(-20,10),"size":30},{"position":(40,-10),"size":20}]
print(bounds(shapes))
boundsの結果を,空ならNone,左下(-20,-10)で辺30の1個なら(-20,-10,10,20)として確かめてください.さらに原点で辺10の図形を加えても全体の範囲が変わらないことを確認します.
実習21-15 データの流れを図にしてから組み合わせる
ここまでの関数を組み合わせると,生成した図形一覧から,選び出し,並べ替え,移動,集計,描画へ進めます.
それぞれの処理について,受け取る値と返す値を書いてみましょう.たとえば選び出しは図形のリストから図形のリスト,集計は図形のリストから色別個数の辞書です.
形が違う結果を,次の関数へ誤って渡さないようにします.
処理をつなぐ際は,どの時点のデータを使うかも重要です.
移動前に求めた範囲は,移動後の作品にはそのまま使えません.一方,平行移動だけなら使用色や面積は変わりません.
操作の前後で何が変わり,何が変わらないかを考えると,計算の順序を決めやすくなります.必要以上に集計結果を保存せず,必要な段階で元データから求める設計も検討します.
最初は中間結果へ名前を付けて,1段階ずつprintで確認します.
長い内包表記を重ねて1行へ押し込むより,どこで予測と違ったかを調べやすくなります.正しく動いた後に短くするかどうかを考えても遅くありません.
読みやすさは文字数の少なさだけでなく,値の意味と変化を追えるかで判断します.
以下の例は,大きい図形を選び,sizeの順に並べ,右へ10動かしています.
最後のIDと位置を予測してから実行してください.元の一覧を保つ仕様なので,元の位置も確かめます.
この結果をdraw_shapeへ渡せば,選んだ設計がそのまま作品の見た目へつながります.データを扱う処理と描画を分離した利点を,ここで確認しましょう.
shapes = [{"id":"a","position":(0,0),"size":20},{"id":"b","position":(30,0),"size":50},{"id":"c","position":(80,0),"size":40}]
selected = [s for s in shapes if s["size"] >= 30]
def order_key(s):
return s["size"]
ordered = sorted(selected, key=order_key)
shifted = [{**s,"position":(s["position"][0]+10,s["position"][1])} for s in ordered]
print([(s["id"],s["position"]) for s in shifted])
assert shapes[1]["position"] == (30,0)
sizeが40以上の図形を大きい順に並べ,左へ5,上へ10移動した新しい一覧を作ってください.結果のIDが[b,c]で,位置が(25,10),(75,10)になることを確認します.
21.3 仕様を変えても描画関数を保つ
最後の for の前で,shapes を size の降順に並べた新しいリスト ordered を作り,ordered の順に描いてください.shapes 自体の順番と位置は変えません.
- size を返す関数を def で作って key に渡し,sorted(shapes, key=…, reverse=True) で ordered を作る.draw_shape は書き換えない.
- これもやりましょう:ordered を作る前に,shapes.append(create_shape((120, -60), 30, "blue")) で3個目の図形を足す.そのうえで,ordered の size が [80, 50, 30] の順であることを assert で確かめる.
- shapes 自体は変わっていないこと(色の順が ["blue", "orange", "blue"],先頭の位置が移動後の (-120, 50))を assert で確かめる.
- 使用色の数を集合から求めて「色の数: 2」と表示する(blue が2個あっても1種類と数える).集合では描画順(どれを後に描いて上に重ねるか)を表せない理由を,コメントで1行書く.
必須(すべて満たして正しく動けば 8)
- size を返す関数を key に渡し,大きい順に並べた新しいリスト ordered の順で描いている(draw_shape は変えていない)
- 3個目の図形 (120, -60)・30・blue を足し,ordered の size が [80, 50, 30] であることを assert で確かめている
- shapes の色の順番(blue, orange, blue)と先頭の位置 (-120, 50) が変わっていないことを assert で確かめている
- 使用色の数 2 を集合から求めて表示し,集合では描画順を表せない理由をコメントで書いている
発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)
- 同じ size の図形があるときの並び順を,2番目のキーで決めている
- 小さい順に描いた場合と見比べ,大きい順に描く理由(小さい図形が隠れない)を説明している
必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6
size が60以上の図形だけを move_shape で右に40,上に20移動し,それ以外はそのまま入れた新しいリスト shifted を作って描いてください.shapes は変更しません.
- 最後の for の前で shifted を作り,shifted の順に描く.出発点では shapes[0] は size 50 で位置 (-120, 50),shapes[1] は size 80 で位置 (0, 0) なので,shifted[0] の位置は (-120, 50) のまま,shifted[1] の位置は (40, 20) になる.これを assert で確かめる.
- shapes[1] の位置が (0, 0) のままであることを assert で確かめる.
- これもやりましょう:この処理を関数 shift_large(items, limit, dx, dy) にする.items の図形のうち size が limit 以上のものだけを (dx, dy) 移動した新しいリストを返す.shifted は shift_large(shapes, 60, 40, 20) で作る.さらに,shift_large(shapes, 40, 0, -30) では2個とも下に30動いて位置が (-120, 20) と (0, -30) になること,shift_large([], 60, 40, 20) が [] を返すことも assert で確かめる.
必須(すべて満たして正しく動けば 8)
- move_shape を使い,size が60以上の図形だけを (40, 20) 移動した新しいリスト shifted を作って描いている
- shifted[1] が (40, 20),shifted[0] が (-120, 50) のまま,shapes[1] が (0, 0) のままであることを assert で確かめている
- 関数 shift_large(items, limit, dx, dy) を作り,条件と移動量を変えた呼び出し(limit 40,(0, -30))の結果も assert で確かめている
- 空のリストを渡すと空のリストを返すことを確かめている
発展(必須に加えて 1 つ以上で 9,学んだ範囲を大きく超える作り込みで 10)
- 移動前と移動後の使用色の集合が同じであることも確かめている
- 条件を「size が limit 以下」にも切り替えられるようにし,使い分けを説明している
必須が 1〜2 つ足りない → 7 / 動かない・未着手が複数 → 6
振り返り
座標を2本の別々のリストにした場合と比べて,辞書にまとめるとどの間違いを防げますか.『生成』『更新』『描画』の入出力を1行ずつ説明し,次章ではその説明をテストに変えます.
設計メモには,各キーの名前だけでなく,値の型,意味,許す範囲も書いてください.positionが左下なのか中心なのか,sizeが辺なのか半径なのかが曖昧だと,別の人が描画関数を作れません.同じ作品を説明文だけから再現できるかを確かめると,データの約束に足りない情報を見つけられます.
復習:虫食いで確かめる
コードの ____ を埋めてください.実行前に結果を予測し,実行後に問題文の条件と照合しましょう.assert は条件が成り立つかを調べ,成り立たないとエラーになります.
位置は変更しない組としてタプルにします.リストから変換する命令を補ってください.
position = ____([10, 20]) assert position == (10, 20) print(position)
図形の大きさを size,色を color というキーで保存し,青い図形の大きさ40を表示してください.
shape = {"position": (0, 0), ____: 40, ____: "blue"}
print(shape["size"])
assert shape["color"] == "blue"
元の図形を変えず,新しい図形の位置だけ更新します.コピーするメソッドを補い,二つの assert を確認してください.
original = {"position": (10, 20), "size": 40}
moved = original.____()
moved["position"] = (30, 20)
assert original["position"] == (10, 20)
assert moved["position"] == (30, 20)
print(original, moved)
座標を dx,dy だけ移動した新しい辞書を返します.(15, 18) になり,元の位置が変わらないことを確認します.
def move(shape, dx, dy):
x, y = shape["position"]
return {**shape, "position": (____, ____)}
shape = {"position": (10, 20), "size": 40}
assert move(shape, 5, -2)["position"] == (15, 18)
assert shape["position"] == (10, 20)
print(move(shape, 5, -2))
図形をリストの順に読みます.赤のあとに青が表示されるよう,繰り返す対象を補ってください.
shapes = [{"color": "red"}, {"color": "blue"}]
for shape in ____:
print(shape["color"])
集合内包表記で重複のない色の集合を作ります.assert で赤と青の二色だけになることを確認してください.
shapes = [{"color": "red"}, {"color": "blue"}, {"color": "red"}]
colors = {shape[____] for shape in shapes}
assert colors == {"red", "blue"}
print(sorted(colors))