7 辞書とデータの構造化

本章では辞書型を取り上げます。辞書を使うと、データのアクセスや整理を柔軟に管理できます。前章で学んだリストと組み合わせて、チェス盤をモデル化するデータ構造を作成します。

辞書型

リストと同じように、辞書はミュータブルな複数の値の集合です。しかし、リストのインデックスとは異なり、辞書のインデックスには整数に限らずいろいろなデータ型が使えます。辞書のインデックスはキーと呼ばれ、値と関連付けられたものはキーと値のペア(key-value pair)と呼ばれます。

コードでは、波かっこ({})で辞書を作成します。以下の式を対話型シェルに入力してみてください。

>>> my_cat = {'size': 'fat', 'color': 'gray', 'age': 17}

これは辞書を変数my_catに代入します。この辞書のキーは'size'、'color'、'age'です。そのキーに対応する値はそれぞれ'fat'、'gray'、17です。キーを指定すれば値にアクセスできます。

>>> my_cat['size']
'fat'
>>> 'My cat has ' + my_cat['color'] + ' fur.'
'My cat has gray fur.'

辞書を使うと、何らかの対象についての複数のデータを一つの変数に格納できます。変数my_catは、私のネコについての3つの異なる文字列を格納しています。この辞書を引数や返り値として使えるので、3つの別々の変数を作らなくてすみます。

辞書は、リストのインデックスのように、キーに整数値を使うことができますが、0で始める必要性はなく、どの数値でも使えます。

>>> spam = {12345: 'Luggage Combination', 42: 'The Answer'}
>>> spam[12345]
'Luggage Combination'
>>> spam[42]
'The Answer'
>>> spam[0]
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
KeyError: 0

辞書にあるのはキーで、インデックスはありません。この例では、辞書spamは整数のキー12345と42を持っていますが、リストのように0から41のインデックスがあるわけではありません。

辞書とリストの比較

リストとは異なり、辞書の中に入っている要素に順番はありません。spamという名前のリストの最初の要素はspam[0]ですが、辞書には「最初の」要素という発想がありません。2つのリストが同じかどうかを決めるには順番が関係しますが、辞書のキーと値のペアはどの順番で作成しても同じです。以下の式を対話型シェルに入力してみてください。

>>> spam = ['cats', 'dogs', 'moose']
>>> bacon = ['dogs', 'moose', 'cats']
>>> spam == bacon  # リストの要素は順番が関係ある
False
>>> eggs = {'name': 'Zophie', 'species': 'cat', 'age': '8'}
>>> ham = {'species': 'cat', 'age': '8', 'name': 'Zophie'}
>>> eggs == ham  # 辞書のキーと値は順番が関係ない
True

辞書eggsとhamは、キーと値のペアを違う順番で作成したにもかかわらず、同じ値です。辞書には順番がありませんから、シーケンス型ではなく、リストのようにスライスを作成することはできません。

辞書に存在しないキーにアクセスしようとすると、KeyErrorエラーになります。リストのIndexErrorエラーと似ています。対話型シェルに以下のコードを入力して、'color'キーが存在しないためにエラーメッセージが表示されるのを確認してください。

>>> spam = {'name': 'Zophie', 'age': 7}
>>> spam['color']
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    spam['color']
KeyError: 'color'

辞書には順番がありませんが、自由にキーを設定できるので、データを整理できます。友人の誕生日のデータを格納したいとしましょう。友人の名前をキー、その人の誕生日を値として辞書を作成します。新しいファイルエディタウィンドウを開いて、以下のコードをbirthdays.pyという名前で保存してください。

❶ birthdays = {'Alice': 'Apr 1', 'Bob': 'Dec 12', 'Carol': 'Mar 4'}

while True:
    print('Enter a name: (blank to quit)')
    name = input()
    if name == '':
        break

  ❷ if name in birthdays:
      ❸ print(birthdays[name] + ' is the birthday of ' + name)
    else:
        print('I do not have birthday information for ' + name)
        print('What is their birthday?')
        bday = input()
      ❹ birthdays[name] = bday
        print('Birthday database updated.')

このコードでは、最初に辞書を作成して、birthdaysという変数に保存しています(❶)。リストと同じように、inキーワードで、入力した名前のキーが辞書の中に存在するかを確かめられます(❷)。その名前が辞書の中に存在するなら、関連付けられた値に角かっこでアクセスできます(❸)。その名前が辞書の中に存在しないなら、同じく角かっこと代入演算子で値を追加できます(❹)。

このプログラムを実行すると、次のようになります。

Enter a name: (blank to quit)
Alice
Apr 1 is the birthday of Alice
Enter a name: (blank to quit)
Eve
I do not have birthday information for Eve
What is their birthday?
Dec 5
Birthday database updated.
Enter a name: (blank to quit)
Eve
Dec 5 is the birthday of Eve
Enter a name: (blank to quit)

もちろん、入力したデータはプログラム終了時に失われてしまいます。ハードドライブのファイルにデータを保存する方法は第10章で説明します。

キーと値を返す

keys()、values()、items()という3つの辞書メソッドがあります。それぞれ、辞書のキー、値、キーと値のペアをリストのような形式で返します。これらのメソッドが返す値は本物のリストではありません。修正できませんし、append()メソッドもありません。しかし、これらのデータ型(それぞれdict_keys、dict_values、dict_items)は、forループに使えます。対話型シェルでこれらのメソッドの動作を確認してみましょう。

>>> spam = {'color': 'red', 'age': 42}
>>> for v in spam.values():
...     print(v)

red
42

forループが辞書spamの値を一つずつ反復しています。forループで、キーの反復もキーと値の反復もできます。

>>> for k in spam.keys():
...     print(k)

color
age
>>> 'color' in spam.keys()
True
>>> 'age' not in spam.keys()
False
>>> 'red' in spam.values()
True
>>> for i in spam.items():
...     print(i)

('color', 'red')
('age', 42)

keys()、values()、items()メソッドを使うときに、forループは辞書のキー、値、キーと値のペアを反復します。ある値が辞書のキーまたは値の中に存在するかどうかをin演算子とnot in演算子で調べられます。items()メソッドが返す値dict_itemsの中に入っているのは、キーと値のタプルであることに注意してください。

辞書そのものにin演算子またはnot in演算子を使うと、ある値がキーに存在するかを調べることになります。keys()メソッドを使うのと等しい結果になります。

>>> 'color' in spam
True
>>> 'color' in spam.keys()
True

これらのメソッドが返す値から本物のリストがほしければ、これらのメソッドが返すリストのような値をlist()関数に渡してください。以下の式を対話型シェルに入力してみてください。

>>> spam = {'color': 'red', 'age': 42}
>>> spam.keys()  # リストのようなdict_keys値を返す
dict_keys(['color', 'age'])
>>> list(spam.keys())  # 本物のリストを返す
['color', 'age']

list(spam.keys())の行はkeys()から返されたdict_keys値をlist()に渡しています。そうすれば['color', 'age']というリスト値が返されます。

forループ内でキーと値を別々の変数に代入するときには、多重代入が使えます。以下の式を対話型シェルに入力してみてください。

>>> spam = {'color': 'red', 'age': 42}
>>> for k, v in spam.items():
...     print('Key: ' + str(k) + ' Value: ' + str(v))

Key: color Value: red
Key: age Value: 42

このコードでは、キーが 'color'と'age'、値が'red'と42である辞書を作成しています。forループではitems()メソッドが返す('color', 'red')と('age', 42)のタプルを反復処理します。2つの変数kとvには、このタプルから最初の値(キー)と2番目の値(値)がそれぞれ代入されます。ループの本体ではキーと値のペアからkとvを表示しています。

キーに使える値はたくさんありますが、リストと辞書をキーとして使うことはできません。リストと辞書はハッシュ不可能です(本書ではこれ以上詳しく扱いません)。辞書のキーとしてリストを使いたいと思ったら、タプルを使うようにしてください。

キーの存在確認

キーを使って値にアクセスする前にそのキーが辞書の中に存在するかどうかを確認するのは面倒です。幸いなことに、辞書にはget()メソッドがあります。キーとデフォルト値の2つの引数を取って、キーが存在すればそのキーに対応する値を、キーが存在しなければデフォルト値を返してくれます。

以下の式を対話型シェルに入力してみてください。

>>> picnic_items = {'apples': 5, 'cups': 2}
>>> 'I am bringing ' + str(picnic_items.get('cups', 0)) + ' cups.'
'I am bringing 2 cups.'
>>> 'I am bringing ' + str(picnic_items.get('eggs', 0)) + ' eggs.'
'I am bringing 0 eggs.'

辞書picnic_itemsにキー'eggs'は存在しませんから、get()メソッドはデフォルト値の0を返します。get()を使わなければ、以下のように、このコードはエラーになります。

>>> picnic_items = {'apples': 5, 'cups': 2}
>>> 'I am bringing ' + str(picnic_items['eggs']) + ' eggs.'
Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    'I am bringing ' + str(picnic_items['eggs']) + ' eggs.'
KeyError: 'eggs'

キーを使って値にアクセスする前にそのキーの存在確認をすれば、プログラムがエラーでクラッシュするのを防げます。

デフォルト値の設定

辞書の中に特定のキーが存在しなければそのキーに対して何らかの値を設定する、という必要に迫られる場面がよくあります。次のようなコードを書こうとするでしょう。

>>> spam = {'name': 'Pooka', 'age': 5}
>>> if 'color' not in spam:
...     spam['color'] = 'black'
...
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}

setdefault()メソッドを使うとこれを1行で表現できます。第一引数はキー、第二引数はそのキーが存在しない場合に設定するデフォルト値です。キーが存在すれば、setdefault()メソッドはそのキーに対応する値を返します。以下の式を対話型シェルに入力してみてください。

>>> spam = {'name': 'Pooka', 'age': 5}
>>> spam.setdefault('color', 'black')  #  キー'color'に値'black'を設定する
'black'
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}
>>> spam.setdefault('color', 'white')  # 何もしない
'black'
>>> spam
{'name': 'Pooka', 'age': 5, 'color': 'black'}

最初にsetdefault()を呼び出したときは、辞書spamが{'color': 'black', 'age': 5, 'name': 'Pooka'}へと変化します。このメソッドは、今回キー'color'に設定した'black'という値を返します。次にspam.setdefault('color', 'white')を呼び出したときは、そのキーの値は'white'へと変わることがありません。spamには'color'という名前のキーがすでに存在するからです。

setdefault()メソッドは、キーを確実に存在させられる簡便な書き方です。文字列の中にある各文字の出現回数をカウントする短いプログラムを書いてみましょう。次のコードをcharacterCount.pyという名前で保存してください。

message = 'It was a bright cold day in April, and the clocks were striking thirteen.'
count = {}

for character in message:
    count.setdefault(character, 0) ❶
    count[character] = count[character] + 1 ❷

print(count)   

このプログラムは、変数messageに格納されている文字列を1文字ずつループして、それぞれの文字が何回出現するかをカウントします。setdefault()メソッドを呼び出しているので(❶)、そのキーが辞書countに存在することを保証できます(デフォルト値は0)。よって、このプログラムのcount[character] = count[character] + 1の行でKeyErrorエラーになることはありません(❷)。このプログラムを実行すると、次のように出力されます。

{'I': 1, 't': 6, ' ': 13, 'w': 2, 'a': 4, 's': 3, 'b': 1, 'r': 5, 'i': 6,
'g': 2, 'h': 3, 'c': 3, 'o': 2, 'l': 3, 'd': 3, 'y': 1, 'n': 4, 'A': 1,
'p': 1, ',': 1, 'e': 5, 'k': 2, '.': 1}

出力から、小文字のcが3回出現し、スペースが13回出現し、大文字のAが1回出現することがわかります。このプログラムは、変数messageに格納されている文字列が何であっても、数百万文字であっても、動作します。

データ構造を用いた現実世界のモデル化

インターネット登場以前でも、世界の反対側にいる人とチェスの対局を行うことはできました。 両プレイヤーが自宅にチェス盤を用意し、駒の動きを書いたハガキを郵送し合いました。そのためには、プレイヤーが盤面と駒の動きを一義的に記述する方法が必要でした。

図7-1に示すように、代数式記譜法では、チェス盤のマス目を数字とアルファベットの座標で特定します。

図 7-1:代数式記譜法によるチェス盤の座標

チェスの駒は文字で表現します。キングはK、クイーンはQ 、ルークはR 、ビショップはB、ナイトはNです。動きを記述するには、駒を表す文字と移動先を表す座標が必要です。これらを用いて1回の手番を記述します(白が先手です)。例えば、"2. Nf3 Nc6"は、2手目で、白のナイトがf3に移動し、黒のナイトがc6に移動したことを示します(訳注:チェスでは先手と後手がそれぞれ1回ずつ駒を動かして1手と数えます)。

代数式記譜法にはもう少し細かいルールがありますが、チェス盤に向かい合っていなくてもチェスの対局を一義的に記述できることが要点です。世界の反対側にいる人とでも対局できます。記憶力がよければ物理的なチェス盤と駒がなくても構いません。郵送物に書かれた動きを読み取り、頭の中で盤面を更新します。

コンピュータは記憶力がいいです。現代のコンピュータなら、'2. Nf3 Nc6'のような文字列を、何十億でもたやすく保存できます。このようにして、コンピュータは物理的なチェス盤なしにチェスを対局します。チェス盤を表すデータをモデル化し、そのモデルでチェスの対局をシミュレートするコードを書きます。

ここでリストと辞書を使えます。例えば、Pythonの辞書{'h1': 'bK', 'c6': 'wQ', 'g2': 'bB', 'h5': 'bQ', 'e3': 'wK'}が図7-2の盤面を表すような記譜法が考えられます。

図 7-2:辞書{'h1': 'bK', 'c6': 'wQ', 'g2': 'bB', 'h5': 'bQ', 'e3': 'wK'}がモデル化するチェス盤

このデータ構造を使って、対話的なチェスプログラムを作成しましょう。

プロジェクト 1:対話的なチェス盤シミュレーター

最初期のコンピュータでも人間より計算が速かったです。しかし、当時の人々は、チェスが知性を示すと考えていました(訳注:今ではチェスで人間がコンピュータに勝つことはできませんが、当時はチェスでコンピュータが人間に勝てないことが人間の知性を示していると考えられていたということです)。ここでチェス対局プログラムを作成するつもりはありません(それだけで一冊の本になってしまいます)。これまでに学んできたことを活用して、対話的なチェス盤プログラムを作成します。

このプログラムではチェスのルールを知らなくても大丈夫です。チェスでは、8×8の盤面で、白と黒の駒を使うということを知っていれば十分です。駒には、ポーン、ナイト、ビショップ、ルーク、クイーン、キングがあります。盤の左上と右下のマス目が白です。このプログラムでは出力ウィンドウの地の色が黒だと想定します(紙では地の色が白ですが)。このチェス盤プログラムには駒を配置できます。駒の動き方は問いません。図7-3に示すように、チェス盤を「描画」するのにテキストを使います。

図 7-3:テキストベースのチェス盤プログラムの出力

グラフィックスがあればきれいで見やすくなるでしょうが、ここでは見た目にこだわりません。テキストベースにすればprint()関数だけでプログラムを表現でき、Pygameのようなグラフィックライブラリ(Invent Your Own Computer Games with Python [No Starch Press, 2016]で紹介しました)をインストールする必要がなくなります。

チェス盤と駒の配置を表現するデータ構造の設計から始めます。前節の例が使えます。チェス盤は、マス目を表す文字列の'a1'から'h8'をキーとするPythonの辞書で表現できます。マス目を表す文字列は必ず2文字であり、アルファベットは小文字で、そのあとに数字が続くことに注目してください。この特徴が重要で、あとで示すコードで活用します。

駒を表すのにも2文字を使います。1文字目は小文字の'w'か'b'で、白(先手)か黒(後手)を示します。2文字目は大文字の'P'、'N'、'B'、'R'、'Q'、'K'で、駒の種類を示します。図7-4に駒とその表し方を示しました。

図 7-4:チェスの駒を表す2文字の文字列

Pythonの辞書のキーがチェス盤のマス目を示し、値がそのマス目にある駒を示します。辞書にキーが存在しなければ、そのマス目に駒はありません。辞書はこうした情報を保存するのに適しています。辞書で同じキーは一度しか使えないのが、チェス盤で同じマス目には1つの駒しか配置できないことに対応します。

ステップ 1:プログラムの設定

このプログラムの最初の部分では、exit()関数を使うためのsysモジュールと、copy()関数を使うためのcopyモジュールをインポートしています。チェスの対局開始時には、白と黒のプレイヤーはそれぞれ16個の駒を持っています。定数STARTING_PIECESが対局開始時の駒の配置を記述した辞書を保持します。

import sys, copy

STARTING_PIECES = {'a8': 'bR', 'b8': 'bN', 'c8': 'bB', 'd8': 'bQ',
'e8': 'bK', 'f8': 'bB', 'g8': 'bN', 'h8': 'bR', 'a7': 'bP', 'b7': 'bP',
'c7': 'bP', 'd7': 'bP', 'e7': 'bP', 'f7': 'bP', 'g7': 'bP', 'h7': 'bP',
'a1': 'wR', 'b1': 'wN', 'c1': 'wB', 'd1': 'wQ', 'e1': 'wK', 'f1': 'wB',
'g1': 'wN', 'h1': 'wR', 'a2': 'wP', 'b2': 'wP', 'c2': 'wP', 'd2': 'wP',
'e2': 'wP', 'f2': 'wP', 'g2': 'wP', 'h2': 'wP'}

(このコードを入力するのはやや面倒なので、https://autbor.com/3/chessboard.pyからコピーしてください。)チェス盤を対局開始時の状態にリセットする必要が生じたら、copy.copy()関数でSTARTING_PIECESをコピーできます。

ステップ 2:チェス盤テンプレートの作成

変数BOARD_TEMPLATEはチェス盤のテンプレートの働きをする文字列を格納します。このプログラムでは、チェス盤を表示する前に、それぞれの駒を表す文字列を挿入します。三重引用符を使うと、コードの複数行にまたがる複数行文字列を作成できます。複数行文字列は三重引用符で終わります。このPythonの構文を利用すると、\nエスケープ文字を使って全部を1行で書くよりも見やすいです。複数行文字列については次章でさらに詳しく説明します。

BOARD_TEMPLATE = """
    a    b    c    d    e    f    g    h
   ____ ____ ____ ____ ____ ____ ____ ____
  ||||||    ||||||    ||||||    ||||||    |
8 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
  ||||||____||||||____||||||____||||||____|
  |    ||||||    ||||||    ||||||    ||||||
7 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
  |____||||||____||||||____||||||____||||||
  ||||||    ||||||    ||||||    ||||||    |
6 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
  ||||||____||||||____||||||____||||||____|
  |    ||||||    ||||||    ||||||    ||||||
5 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
  |____||||||____||||||____||||||____||||||
  ||||||    ||||||    ||||||    ||||||    |
4 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
  ||||||____||||||____||||||____||||||____|
  |    ||||||    ||||||    ||||||    ||||||
3 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
  |____||||||____||||||____||||||____||||||
  ||||||    ||||||    ||||||    ||||||    |
2 ||{}|| {} ||{}|| {} ||{}|| {} ||{}|| {} |
  ||||||____||||||____||||||____||||||____|
  |    ||||||    ||||||    ||||||    ||||||
1 | {} ||{}|| {} ||{}|| {} ||{}|| {} ||{}||
  |____||||||____||||||____||||||____||||||
"""
WHITE_SQUARE = '||'
BLACK_SQUARE = '  '

波かっこは'wR'や'bQ'のような駒を表す文字列を挿入する場所を表します。マス目が空白の場合は、WHITE_SQUAREまたはBLACK_SQUAREを挿入します。これについては、print_chessboard()関数の箇所で詳しく説明します。

ステップ 3:現在のチェス盤の状態を表示する

チェス盤を表す辞書を取り、画面上にチェス盤を駒とともに表示するprint_chessboard()関数を定義します。文字列BOARD_TEMPLATE について、文字列メソッドformat()を呼び出します。このメソッドに文字列のリストを渡します。format()メソッドは、BOARD_TEMPLATEの波かっこ({})をリストで渡した文字列に置換した新しい文字列を返します。format()については次章で詳しく説明します。

print_chessboard()のコードを見ていきましょう。

def print_chessboard(board):
    squares = []
    is_white_square = True
    for y in '87654321':
        for x in 'abcdefgh':
            #print(x, y, is_white_square)  # デバッグ用に座標を表示する

チェス盤には64マスあり、文字列BOARD_TEMPLATEには64箇所の{}があります。これらの{}を置換する64個の文字列のリストを作ります。そのリストを変数squaresに格納します。このリストの文字列は、'wB'や'bQ'のように駒を表すか、空白マスを表すかのどちらかです。空白マスが白マスか黒マスかに応じて、WHITE_SQUARE('||')とBLACK_SQUARE(' ')を使い分けます。変数is_white_squareにブール値を格納して、どのマスが白でどのマスが黒かを判別します。

入れ子になったforループでチェス盤の64マスすべてをループします。左上のマスから始めて右に進んでいき、下へと進んでいきます。左上のマスは白マスですから、is_white_squareはTrueで始めます。forループはrange()で取得できる整数でも、リストの値でも、文字列の文字でもループできることを思い出してください。ここでの2つのforループでは、変数yとxが文字列'87654321'と'abcdefgh'の各文字になります。print(x, y, is_white_square)行のコメントを外してプログラムを実行すれば、このコードがループする順番(とそのマスの色)を確認できます。

forループの内側では、適切な文字列でリストsquaresを組み立てます。

            if x + y in board.keys():
                squares.append(board[x + y])
            else:
                if is_white_square:
                    squares.append(WHITE_SQUARE)
                else:
                    squares.append(BLACK_SQUARE)
            is_white_square = not is_white_square
        is_white_square = not is_white_square

    print(BOARD_TEMPLATE.format(*squares))

ループ変数のxとyの文字列を結合して、2文字のマス目を表す文字列を作ります。例えば、xが'a'でyが'8'なら、x + yは'a8'になり、x + y in board.keys()でその文字列がチェス盤を表す辞書のキーに存在するかを調べます。存在すれば、チェスの駒を表す文字列をリストsquares のそのマスに追加します。

存在しなければ、空白マスを表す文字列を追加します。is_white_squareの値に応じてWHITE_SQUAREかBLACK_SQUAREのどちらかになります。1つのマスの処理が終われば、is_white_squareのブール値を反対の値にトグルします(隣り合うマス目の色は異なるので)。外側のforループが終わり、1つの行の処理が終われば、この変数の値をもう一度トグルします。

ループ処理が終われば、リストsquaresには64個の文字列が入っています。ただし、文字列メソッドformat()は1つのリストを引数に取るのではなく、{}ごとに1つの文字列を引数に取ります。squaresの横にアスタリスク(*)をつけると、そのリストの値を一つずつの引数として渡すことになります。これは微妙な違いです。spam = ['cat', 'dog', 'rat']というリストを例に説明します。print(spam)を呼び出すと、角かっことクォートとカンマのあるリスト値を表示します。これに対し、print(*spam)を呼び出すと、print('cat', 'dog', 'rat')を呼び出したのと同じになるので、cat dog ratを表示します。私はこれをスター構文と呼んでいます。

print_chessboard()関数は、チェス盤を表現するためにここで使っている特定のデータ構造で機能するように書かれています。'a8'のようなマス目を表す文字列をキー、'bQ'のような駒を表す文字列を値とする、Pythonの辞書というデータ構造です。データ構造を変えたとしたら、この関数も変えなければなりません。print_chessboard()はチェス盤をテキストで表現しますが、チェス盤の描画にPygameのようなグラフィックライブラリを使うとしても、このPythonの辞書でチェス盤の配置を表現できるでしょう。

ステップ 4:チェス盤の操作

これでチェス盤をPythonの辞書で表現できるようになり、その辞書に基づいてチェス盤を表示する関数が用意できましたから、辞書のキーと値を操作してチェス盤で駒を動かすコードを書きましょう。defブロックのprint_chessboard()関数のあとに、対話型のチェス盤プログラムの使い方を説明するテキストを表示するプログラムのメイン部分が来ます。

print('Interactive Chessboard')
print('by Al Sweigart al@inventwithpython.com')
print()
print('Pieces:')
print('  w - White, b - Black')
print('  P - Pawn, N - Knight, B - Bishop, R - Rook, Q - Queen, K - King')
print('Commands:')
print('  move e2 e4 - Moves the piece at e2 to e4')
print('  remove e2 - Removes the piece at e2')
print('  set e2 wP - Sets square e2 to a white pawn')
print('  reset - Resets pieces back to their starting squares')
print('  clear - Clears the entire board')
print('  fill wP - Fills entire board with white pawns.')
print('  quit - Quits the program')

このプログラムでは、辞書を変更することにより、駒の移動と除去とマス目への配置ができ、チェス盤のリセットとクリアができます。

main_board = copy.copy(STARTING_PIECES)
while True:
    print_chessboard(main_board)
    response = input('> ').split()

まず、変数main_boardは辞書STARTING_PIECESのコピーを受け取ります。これは対局開始時の駒の配置を表す辞書です。プログラム実行は無限ループに入り、ユーザーの命令を待ちます。例えば、ユーザーがinput()呼び出しにmove e2 e4と入力すれば、split()メソッドがリスト['move', 'e2', 'e4']を返し、それが変数responseに格納されます。リストresponseの最初の要素であるresponse[0]は、ユーザーが実行したい命令です。

    if response[0] == 'move':
        main_board[response[2]] = main_board[response[1]]
        del main_board[response[1]]

ユーザーがmove e2 e4と入力したら、response[0]は'move'になります。移動前のマス目(response[1])から新しいマス目(response[2])へとmain_boardの駒をコピーすることにより、駒をあるマス目から別のマス目へと動かします。それから、main_boardの古いマス目のキーと値のペアを削除します。こうすることで、駒を移動させたかのように見えます(もう一度print_chessboard()を呼び出すまでは変化が見えませんが)。

この対話型のチェス盤シミュレーターは、チェスのルールに沿った動きであるかどうかはチェックしません。ユーザーの命令をそのまま実行します。ユーザーがremove e2と入力すると、プログラムはresponseに['remove', 'e2']を設定します。

    elif response[0] == 'remove':
        del main_board[response[1]]

main_boardからresponse[1]をキーとするキーと値のペアを削除することにより、その駒を盤面から消しています。ユーザーがset e2 wPと入力すると、白のポーンをe2に配置します。プログラムはresponseに['set', 'e2', 'wP']を設定します。

    elif response[0] == 'set':
        main_board[response[1]] = response[2]

キーをresponse[1]、値をresponse[2]とする新しいキーと値のペアをmain_boardに作成し、その駒を盤面に配置します。ユーザーがresetと入力すると、responseはシンプルに['reset']となり、辞書STARTING_PIECESをmain_boardにコピーして対局開始時の配置に戻します。

    elif response[0] == 'reset':
        main_board = copy.copy(STARTING_PIECES)

ユーザーがclearと入力すると、responseはシンプルに['clear']となり、main_boardを空辞書にすることで盤面からすべての駒を消します。

    elif response[0] == 'clear':
        main_board = {}

ユーザーがfill wPと入力すると、responseは['fill', 'wP']となり、64マスすべてに文字列'wP'を設定します。

    elif response[0] == 'fill':
        for y in '87654321':
            for x in 'abcdefgh':
                main_board[x + y] = response[1]

入れ子のforループはすべてのマス目を反復し、キーx + yにresponse[1]を設定します。64個の白のポーンをチェス盤に配置する理由なんてありませんが、どのようなことでもチェス盤のデータ構造をいじれば簡単に実現できることがわかります。最後に、ユーザーがquitと入力したら、プログラムが終了します。

    elif response[0] == 'quit':
        sys.exit()

命令を実行してmain_boardを変更した後には、プログラム実行がwhileループの最初に戻り、変更後のチェス盤を表示して、ユーザーからの新しい命令を待ち受けます。

この対話型のチェス盤プログラムは、チェスのルールに縛られずに駒を配置します。チェス盤上の駒を表すのに辞書を使い、その辞書をチェス盤の見た目で画面上に表示する関数があります。データ構造を設計してそのデータ構造を操作する関数を書くことにより、現実世界の対象や過程をモデル化できます。データ構造でゲームをモデル化する例に興味がありましたら、The Big Book of Small Python Projects (No Starch Press, 2021)という私の別の本をご覧ください。○×ゲームなどが収録されています。

辞書やリストの入れ子

複雑な事物をモデル化しようとすると、辞書やリストを入れ子にしなければならないことがあります。リストは値を順番に保持するのに適していて、辞書はキーと値を対応づけるのに適しています。例えば、以下のプログラムは、入れ子の辞書を使って、ゲストがピクニックに持っていく物を管理します。total_brought()関数がこのデータ構造を読み取り、それぞれの持ち物の総数を計算します。次のコードをguestpicnic.pyという名前で保存してください。

all_guests = {'Alice': {'apples': 5, 'pretzels': 12},
              'Bob': {'ham sandwiches': 3, 'apples': 2},
              'Carol': {'cups': 3, 'apple pies': 1}}

def total_brought(guests, item):
    num_brought = 0
  ❶ for k, v in guests.items():
      ❷ num_brought = num_brought + v.get(item, 0)
    return num_brought

print('Number of things being brought:')
print(' - Apples         ' + str(total_brought(all_guests, 'apples')))
print(' - Cups           ' + str(total_brought(all_guests, 'cups')))
print(' - Cakes          ' + str(total_brought(all_guests, 'cakes')))
print(' - Ham Sandwiches ' + str(total_brought(all_guests, 'ham sandwiches')))
print(' - Apple Pies     ' + str(total_brought(all_guests, 'apple pies')))

total_brought()関数内では、forループがguestsのキーと値のペアを反復処理しています(❶)。ループ内では、ゲストの名前がkに代入され、ピクニックの持ち物を表す辞書がvに代入されます。get()メソッドのパラメータとして渡される持ち物がその持ち物を表す辞書のキーに存在すれば、その値(個数)をnum_broughtに加えます(❷)。キーに存在しなければ、get()メソッドが0を返し、これがnum_broughtに加えられます。

このプロブラムを実行すると、次のように出力されます。

 Number of things being brought:
 - Apples 7
 - Cups 3
 - Cakes 0
 - Ham Sandwiches 3
 - Apple Pies 1

このピクニックの持ち物の個数は単純であり、モデル化してプログラムを書く必要なんてないと思われるかもしれませんが、total_brought()関数は持ち物が数千個あるゲストの人数が数千人になってもそのまま使えます。そのような状況であれば、情報をデータ構造に保持し、total_brought()関数で処理すれば、大いに時間を節約できます。

プログラムで正しく処理できる形にしさえすれば、データ構造を使って事物を好きなようにモデル化できます。プログラミングを始めてしばらくの間は、モデル化の「正しい」方法をそこまで気にしないでください。経験を積めば、効率的なモデル化の方法がわかってきます。データを処理して目的を達成できるプログラムを書くのが大切です。

まとめ

この章では辞書について説明しました。リストと辞書は複数の値を持つことのできる値です。辞書やリストを入れ子にすることもできます。リストは単純に値を順番に格納するのに対し、辞書はある項目(キー)を別の項目(値)に結びつけることができるので便利です。リストと同様に、角かっこで辞書の中に入っている値にアクセスできます。整数のインデックスの代わりに、辞書では、整数、浮動小数点数、文字列、タプルなど、いろいろなデータ型のキーを持てます。プログラムで利用する値をデータ構造に整理すると、本章でチェス盤をモデル化したように、現実世界の対象を表現できるようになります。

練習問題

  1. 空辞書を作成してください。

  2. キーが'foo'で値が42の辞書を作成してください。

  3. 辞書とリストの主な違いは何ですか?

  4. spamが{'bar': 100}であるときに、spam['foo']でアクセスしようとするとどうなりますか?

  5. spamに辞書が格納されているとして、'cat' in spamと'cat' in spam.keys()はどう違いますか?

  6. spamに辞書が格納されているとして、'cat' in spamと'cat' in spam.values()はどう違いますか?

  7. 以下のコードを短く書いてください。

if 'color' not in spam:
    spam['color'] = 'black'

  8. 辞書の値を整形して出力(pretty-print)するのに使えるモジュールと関数は何ですか?

練習プログラム

以下の練習プログラムを書いてください。

チェス辞書の検証

本章では、チェス盤を表すのに{'h1': 'bK', 'c6': 'wQ', 'g2': 'bB', 'h5': 'bQ', 'e3': 'wK'}のような辞書値を使いました。辞書を引数に取り、チェス盤として正しいかどうかに応じてTrueまたはFalseを返すisValidChessBoard()という名前の関数を書いてください。

正しいチェス盤には、白のキングと黒のキングが必ず1つずつ存在します。各プレイヤーは最大16個の駒を盤上に持てます。ポーンは8個までです。駒が存在できるのは'1a'から'8h'までのマス目だけです。'9z'のようなマス目に駒は存在できません。駒の名前は、白または黒を表す'w'か'b'のどちらかで始まり、'pawn'、'knight'、'bishop'、'rook'、'queen'、'king'のいずれかが続きます。この関数はバグにより不適切なチェス盤になっていることを検知します(上記の説明は正しいチェス盤の条件を完全に網羅しているわけではありませんが、ここでの練習ではそれで十分です)。

ファンタジーゲームの所持品一覧

中世のファンタジーゲームを作っているとします。プレイヤーの所持品一覧を辞書でモデル化しています。キーは所持品の文字列で、値はそのプレイヤーが所持している個数です。例えば、{'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12}という辞書値は、そのプレイヤーがロープを1つ、トーチを6つ、金貨を42枚、短剣を1つ、矢を12本持っていることを意味します。

所持品一覧を取り、以下のように表示するdisplay_inventory()という名前の関数を書いてください。

Inventory:
12 arrow
42 gold coin
1 rope
6 torch
1 dagger
Total number of items: 62

ヒント:辞書のキーをループ処理するのにforループが使えます。

stuff = {'rope': 1, 'torch': 6, 'gold coin': 42, 'dagger': 1, 'arrow': 12}

def display_inventory(inventory):
    print("Inventory:")
    item_total = 0
    for k, v in inventory.items():
        # この部分のプログラムを書く
    print("Total number of items: " + str(item_total))

display_inventory(stuff)

戦利品のリストから辞書への変換

このファンタジーゲームでは、ドラゴンを退治した戦利品を次のようなリストで表すとします。

dragon_loot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby']

add_to_inventory(inventory, added_items)という関数を書いてください。パラメータinventoryは、先に見たように、そのプレイヤーの所持品一覧を表す辞書です。パラメータadded_itemsは、dragon_lootのようなリストです。add_to_inventory()関数はプレイヤーの最新の所持品を表す辞書を返します。リストadded_itemsには同じ所持品が複数含まれる可能性があることに留意してください。コードは次のようになります。

def add_to_inventory(inventory, added_items):
    # ここにコードを書く

inv = {'gold coin': 42, 'rope': 1}
dragon_loot = ['gold coin', 'dagger', 'gold coin', 'gold coin', 'ruby']
inv = add_to_inventory(inv, dragon_loot)
display_inventory(inv)

このコードを実行してから、前のプログラムのdisplay_inventory()関数を呼び出すと、以下のように出力します。

Inventory:
45 gold coin
1 rope
1 ruby
1 dagger

Total number of items: 48