第4回第5回でPythonの基本的な型について学びました。今回はその続きとなる回です。今回扱うのは必ずしも使う必要はないものの、知っていると便利な型です。具体的には「タプル」、「セット」、「辞書(マップ)」、「関数型」となります。

タプル

リストという型がなぜ存在するのか覚えていますか。ひとつの型のなかに任意の数の複数のデータを格納できると便利だからでしたね。たとえば「生徒たちの成績を格納する」といった目的で利用されます。タプルもリストと似ていて、ひとつの型のなかに複数のデータを格納します。ただ、その目的は異なっていて、「決まった数の複数のデータがひとつの意味を持つもの」にタプルは使われます。

例をあげて説明しましょう。お店の会員情報というものをデータで表現することを考えてみます。単純に文字列としてすべてを含めてしまってもいいのですが、以下のような複数の要素で表現したほうがプログラムで使いやすそうですね。

  • 氏名
  • 生年月日
  • 住所

文字列の中から生年月日を抜き出したりするよりも、「Aさんの住所 -> 東京都…」というようにパッと取り出せる方が便利です。

タプルは上記のような複数のデータをひとつにまとめるための型です。同じことをリストでも実現できますが、リストは「可変長(長さが変わる)」なので、複数のデータが合わさってはじめて意味を持つ場合の利用は本来の用途ではなく、それよりも同じデータをいくつも格納する用途で使われます。

以下にリストとタプルの違いを図にまとめます。

リストとタプルの違い

タプルがどういうものか感覚的にわかっていただけたと思うので、具体的にどのように使うのか説明しましょう。タプルの作成はタプルの要素となる値を () で囲むことで作成できます。type関数は型を確認するために利用する関数です。

>>> a = ('taro','1986','tokyo')
>>> type(a)
<type 'tuple'>

上記は先ほどの会員情報のタプルですね。その要素への参照はリストに似ています。

>>> a = ('taro','1986','tokyo')
>>> a[0]
'taro'
>>> len(a)
3
>>> for i in a:
……   print(i)
taro
1986
tokyo

ただし、リストと異なり一度作成されたオブジェクトは変更することはできません。

>>> a = ('taro','1986','tokyo')
>>> a[0] = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

少しトリッキーですが、タプル内の要素を一気に取得することもできます。

a = ('taro','1986','tokyo')
(b,c,d) = a
print(b)   # taro
print(c)   # 1986
print(d)   # tokyo

タプルはC言語でいう「構造体」やJavaでいう「クラスのメンバ関数」の簡易版として使うのが主な用途です。ただし、構造体やメンバ関数はそれぞれ「変数名」を持っているのに対して、タプルは「何番目にある要素か」ということを基準にしてデータを管理します。当然ながらプログラムの修正などでタプルの構造を変更する際には気をつけてください。

最後にタプルの便利な使い方を紹介します。あまり機会は多くないのですが、関数の返り値を2つ返したい場合がときどき発生します。一つひとつ別の関数に分けて値を取得するようにすることもできますが、たとえばfor文での探索のようなプログラムだと計算コストが高いため、無駄に2周するのは避けたほうがよいです。そのようなときにタプルをreturnで使うと便利です。たとえば、最小値と最大値を取得する関数だと、返り値は2つ返したいところです。そのような場合は以下のようにすれば大丈夫です。

def get_min_max(a):
    mi = a[0]
    ma = a[0]
    for i in a:
        if(i < mi): mi = i
        if(ma < i): ma = i
    return (mi, ma)

a = [5,8,1,4,10,3,7]
(mi, ma) = get_min_max(a)
print("min: " + str(mi))
print("max: " + str(ma))

返り値をタプルにした際は、タプルの構造に気をつけて利用してくださいね。なんでもかんでもタプルにするのはやめたほうがいいです。

セット

次は「セット」です。セットを一言で説明すると、「集合」という概念を実現するための型です。たとえば、ある空の集合にAを追加すると、その集合にはAがあります。さらにBを追加するとA、Bの2つが存在します。しかし、ここにさらにAを追加しても、「A、A、B」とはならずに「A、B」のままです。そして集合は順序を持たないので「A、B」も、「B、A」も同じ意味を持ちます。

セットの内部実装には非常に重要な概念があるのですが、とりあえず使い方を説明してしまいます。空のセットのデータ(オブジェクト)の作成はset()関数を使います。もしくはリストのデータをset関数に渡すことで、中身のあるセットを作ることもできます。

>>> a = set()
>>> b = set([1,3,5,7])
>>> print(b)
set([1, 3, 5, 7])

要素の追加にはadd、削除にはremove関数を使います。

>>> b.add(2)
>>> b.remove(5)
>>> print(b)
set([1, 2, 3, 7])

リストのデータ追加の関数名(正確にはメソッド名)を覚えていますか。addではなく、appendでしたね。少し細かい話となるのですが、日本語で言うと同じ「追加」であっても、addだと「集合に加える」という感じで、appendだと「末尾に加える」という意味合いになります。話を戻すと、remove関数を使うと指定した値をセットから取り除きます。存在しない値を指定するとエラーになるので注意してください。

最後に要素の存在の有無の確認方法と、データをリストにする方法を示します。

>>> a = set([1,3,5,7,9])
>>> 3 in a
True
>>> 10 in a
False

>>> list(a)
[1, 3, 9, 5, 7]

なんとなく使い方はわかっていただけたでしょうか。追加や削除、有無のチェックなどの機能を見ると、なんだかリストに似ているような気がしたかもしれませんが、仕組みはまったく異なっています。仕組みについては次項目で説明します。

[補足] ハッシュの仕組み

セットで使われている重要なコンピュータ技術に、ハッシュ(Hash)と呼ばれているものがあります。集合はハッシュを使わずにでも実現できるでしょうが、PythonのセットはJavaでいうところのHashSetに近いです。このハッシュの概念図を以下に示します。

ハッシュの概念図(Wikipedia「ハッシュ関数」より)

ハッシュは「ハッシュ関数」と呼ばれるものに特定の値(キー)を与えて「ハッシュ値」を得ることで実現されています。ハッシュ値はある範囲のなかの数値(一般的には0からN)のどれかとなり、同じキーから生成されるハッシュ値は常に同じです。

上記の図でいうと、キーとして「John Smith」をハッシュ関数にかけるとハッシュ値「02」が得られています。同様に「Lisa Smith」をハッシュ関数にかけると「01」となります。そしてハッシュ値の範囲は00から15ですね。この性質を考慮したうえで「ある集合に要素Xはあるか」ということをどのようにして実現するか想像してください。

ハッシュを使う場合、たとえば「John Smith」を集合に加える際には、ハッシュ値「02」の場所に「John Smith」を格納します。そして「John Smith」が存在するかどうかのチェックはハッシュ値「02」の場所に「John Smith」がいるかどうか確認すればいいのです。どうです、リストの探索が「先頭から末尾まで順に「John Smith」かどうかを確認していく」ことであるのに比べると随分スマートだと思いませんか。実際、ハッシュを使った要素の探索は非常に高速です。

ただ、ハッシュも使い方を間違えると効率が悪くなります。もう一度図を見てください。よく見ると「John Smith」と「Sandra Dee」は同じハッシュ値に割り当てられていますね。これはいわゆる「ハッシュの衝突」と呼ばれており、これが多発すると探索のスピードが遅くなります。なぜなら「Sandra Dee」の有無の確認をする際に「01」を見にいって、そこに「John Smith」やほかの要素がたくさん入っていると、「01」のなかで「リストの探索」のようにして全部をチェックしていかないといけないからです。

この問題を防ぐためにハッシュ値の範囲は十分な広さを持たせる必要があります。たとえば今回のように00から15などという範囲は狭すぎるので、これをもっと広げます。そうすると確率的には衝突は発生しにくくなります。まぁ、普通はこんなことを気にしなくてもPythonがよしなに処理してくれるので大丈夫です(笑)。

辞書型(マップ)

辞書型は、別名で連想配列やマップとも呼ばれている型です。簡単にいってしまえば、重複が許されないキー(Key)とその値(Value)が対応付けられたデータ型です。「キー」という名前からわかるように、これも内部的にハッシュを使っています。

例をあげて説明しましょう。果物(Key)と色(Value)の辞書オブジェクトを作るとすると、

  • りんご(Key) : 赤色(Value)
  • レモン : 黄色
  • ぶどう : 紫
  • さくらんぼ : 赤色

というペアが作れます。辞書型を使うと「りんご」と指定すれば「赤色」が得られ、「ぶどう」と指定すれば「紫」が帰ってきます。先ほどのセットと同じように「りんご」というキーは重複が許されずにひとつしか存在することができないため、「りんご : 緑色」というペアを改めて登録すると昔のデータは上書きされてなくなってしまいます。ただ、例にある「りんご」と「さくらんぼ」を見ればわかるように値(Value)の重複は許されています。

勘のいいかたであれば辞書型のしくみの想像がついたかもしれませんが、簡単にいってしまうと、セットにおけるハッシュの使い方に「Valueも追加」しているだけですね。

辞書型のしくみ

「John Smith」をキーとして指定するとハッシュ関数で「02」が得られ、「02」のなかから「John Smith」のValueを取得してきます。

それではさっそく辞書型を利用するサンプルプログラムを書いてみます。まずは辞書オブジェクトの生成です。

>>> a = dict()
>>> type(a)
<type 'dict'>

>>> b = {}
>>> type(b)
<type 'dict'>

>>> c = {"apple":"red", "lemon":"yellow"}
>>> type(c)
<type 'dict'>

辞書オブジェクトの生成にはdict()関数を使う方法と、リストにおける [] に近い {} 記号を使うという方法があります。 {} を使う場合はその内部で

key:value

という組み合わせをコンマ区切りで列挙すると、そのペアが追加された辞書オブジェクトが得られます。リストの生成に似ていますね。

次に辞書オブジェクトを操作してみます。

>>> c = {"apple":"red", "lemon":"yellow"}
>>> c['apple']
'red'

>>> c['apple'] = 'green'
>>> c['apple']
'green'

>>> c['banana']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'banana'

>>> c['banana'] = 'yellow'
>>> c['banana']
'yellow'

キーを使った値の取得、キーと値のペアの登録を行っています。リストにおけるインデックス番号がキー名に変わっているだけですね。"辞書オブジェクト[キー]" としてキーを指定することで、対応する値(Value)を参照します。存在しないキーを参照しようとすると当然エラーとなります。

キーの存在の確認はセットと同様です。

>>> c = {"apple":"red", "lemon":"yellow"}
>>> 'apple' in c
True
>>> 'banana' in c
False

関数を使ったアクセスもできます。

>>> c.has_key('apple')
True
>>> c.get('apple')
'red'

ほかにも、キー一覧の取得などもよく使います。値一覧の取得はそれほど使わないかもしれないです。

>>> c.keys()
['lemon', 'apple', 'banana']
>>> c.values()
['yellow', 'red', 'yellow']

ほかにも辞書型の使い方にはいろいろありますが、まずはこのあたりさえ使いこなせれば十分でしょう。

関数型

今回の最後の型は関数型です。「え、関数は関数であって型じゃないでしょ?」と思われたかたもいらっしゃるかもしれませんが、Pythonでは関数も型の一種です。論より証拠で、さっそく例を示しましょう。

>>> def test():
……   return 1
…… 
>>> type(test)
<type 'function'>
>>> 
>>> test2 = test
>>> type(test2)
<type 'function'>

型が'function'とでていますね。関数型です。しかし、関数も型だとわかったところで、実際にどのように使うか想像がつかないかもしれませんね。初心者の域を超えてしまうかもしれないですが、知っておいてもらいたい内容ですのでお話します。

まず最初に関数渡しと呼ばれる関数型の使い方です。

def fun1(fun, x):
    fun(x)

def fun2(x):
    print('fun2 : ' + str(x))

fun1(fun2, 5)  # "fun2 : 5" と表示される

関数fun1はひとつ目の引数で関数を受け取ります。fun1 の中でその受け取った関数がどのように使われているか書かれていますね。見てわかるように引数をひとつ受け取っています。そのため、fun1の第一引数に与える関数は引数をひとつ受け取る関数である必要があります。

呼び出し"fun1(fun2, 5)"を見るとわかるように、fun1の第一引数に関数fun2を渡しています。つまりfun1のなかでfun2がfunとして利用され、そのfunがfun1の第二引数の5を受け取り実行されています。そしてfunはfun2なので、fun2のprint文が実行されるわけです。

このような簡単な例だと関数渡しのメリットはあまりないのですが、自作や一般的なフレームワークの処理の一部をカスタマイズするような場合に利用すると便利です。このような関数を引数とする関数は「関数型の言語」でよく利用され、高階関数と呼ばれたりしています。たとえばHttpサーバーを書く際に、「要求を受け取って処理」というのをフレームワークにし、具体的に何の要求か、その要求に対応するアクションは何かをカスタマイズするという用途などが考えられます。

次にクロージャについて説明します。クロージャの概念自体は初級者には難しいのですが、そのイメージとしては「テンプレートとなる関数からカスタム関数を生成する」ようなものとなります。まぁ、ほかにも使い方はあるのでしょうが。

これも例を使って説明しましょう。

def adder(x):
    def fun(y):
        return x + y
    return fun

adder5 = adder(5)
print(adder5(10)) # 15

adder7 = adder(7)
print(adder7(10)) # 17

関数adderは内部で関数funを作成し、それをreturn文で返しています。注目して欲しいのはadderの引数xを、内部で作成しているfunの中で利用しているということです。このxには関数adderに与えられた値が入っています。具体的には"adder(5)"とした場合は5です。その際、内部の関数生成のコードは実質的に次のようなものとなっています。

    def fun(y):
        return 5 + y

これがreturn文で返され、変数adder5に格納されます。当然ながらadder5は関数として利用可能で、その実行結果は関数生成の行の次に示されたとおりです。adderのなかの関数がテンプレートで、adder5がカスタム関数にあたります。どうです、面白いでしょ。クロージャの本質について興味がある人は調べてみるといいかもしれません。


演習1

リストから重複を取り除く関数をセットを使って作成してください。要素の順序は問いません。

演習2

標準入力を使って生徒の成績を管理するツールを作ってください。"save 生徒名 点数"とするとその生徒の点数を保存します。"get 生徒名"とするとその生徒の点数を表示します。生徒が登録されていない場合は"Error"と出力させて処理を継続させます(※ 終了はCtrl-Dなどで強制終了してかまいません)。

演習3

英語の文章の単語出現数をカウントするプログラムを書いてください。たとえば'hello python hello world'というテキストを与えると{'hello':2, 'python':1, 'world':1} が返ってきます。なお、当然ながら辞書オブジェクト内のキーの順序は問いません。

※解答はこちらをご覧ください。


次回はテキスト処理とファイル処理について学びます。

執筆者紹介

伊藤裕一(ITO Yuichi)

シスコシステムズでの業務と大学での研究活動でコンピュータネットワークに6年関わる。専門はL2/L3 Switching とデータセンター関連技術およびSDN。TACとしてシスコ顧客のテクニカルサポート業務に従事。社内向けのソフトウェア関連のトレーニングおよびデータセンタとSDN関係の外部講演なども行う。

もともと仮想ネットワーク関連技術の研究開発に従事していたこともあり、ネットワークだけでなくプログラミングやLinux関連技術にも精通。Cisco社内外向けのトラブルシューティングツールの開発や、趣味で音声合成処理のアプリケーションやサービスを開発。

Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009年度 IPA 未踏プロジェクト採択

詳細(英語)はこちら