CodeZine(コードジン)

特集ページ一覧

Pythonを始めよう

最近普及が進みつつあるスクリプト言語Pythonに挑戦してみよう

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2007/09/12 14:00

目次

イテレータ関連の処理

 先に説明したように、Pythonでは繰り返し処理にイテレータを用います。イテレータが言語仕様に盛り込まれていることもあり、ライブラリや関数の戻り値など、至る所でイテレータが登場してきます。ここでは、イテレータに関わる処理で面白い部分について見ていきます。

リストを、mapとfilterで変換する

 まずは、mapfilterを紹介します。

 mapは、リスト内の要素を変換し、新しいリストを作って返します。他の言語のmapを知っていれば、おなじみの動作です。Pythonは全てがオブジェクトとして扱われますので、組み込み関数であっても通常のオブジェクトとして引数に渡せます。また、メソッドも適用することができます。

全ての要素に関数を適用
>>> # len() 関数を適用して文字列長を調べる
>>> map(len, ["Python", "Perl", "Java", "Haskell"])
[6, 4, 4, 7]

>>> # 文字列型のupper()メソッドにより、大文字にする
>>> map(str.upper, ["Python", "Perl", "Java", "Haskell"])
['PYTHON', 'PERL', 'JAVA', 'HASKELL']

 もちろん、ユーザが定義した関数も適用できます。ここでは、lambda文を利用して生成した無名関数を適用させてみます。lambda文は、lambda 引数: 計算式、という記法で関数を表します。

無名関数を適用
>>> import math  # 数学関数のimport
>>> map(lambda (a, b): math.sqrt(a ** 2 + b ** 2), [(3, 4), (0, -2), (-1, -1)])
[5.0, 2.0, 1.4142135623730951]

 さて、次はfilterです。これはPerlではgrep関数に相当します。引数はmapと同様に関数とリストですが、渡した関数が真になる要素だけからなるリストを返します。

条件に当てはまる要素だけを取り出す
# アルファベットと数字だけからなるものだけを取り出す
>>> filter(str.isalnum, ["100$", "abc123", "dyndns.hiratara.org"])
['abc123']

 mapfilterを入れ子にして組み合わせることもできます。

mapとfilterの組み合わせ
>>> map(lambda a: a ** 2, filter(lambda a: a > 0, [-1, 2, -3, 4, -5, 6]))
[4, 16, 36]

map、filter系の処理を簡潔に書く

 mapfilterを用いる処理は、多くの場面で利用されます。そのため、これらの処理を直感的に利用出来るような記述法が用意されています。それが、内包表記と呼ばれるものです。

 さっきの処理を内包表記で書くと、以下のようになります。

内包表記
>>> [ a ** 2 for a in [-1, 2, -3, 4, -5, 6] if a > 0 ]
[4, 16, 36]

 余分な関数記述が消え、ずいぶんとすっきりした感じになりました。for ~ in ~の形で対象となる要素一覧を表し、if ~filterをかけます。一番左に書いた式( a ** 2 )mapし、表示します。

 このfor文は入れ子にできます。3つのリストの直積を取るような内包表記は、以下のようになります。

forを入れ子にした内包表記
>>> [(x, y, z) for x in range(3) for y in range(3) for z in range (3)]
[(0, 0, 0), (0, 0, 1), (0, 0, 2), (0, 1, 0), (0, 1, 1), (0, 1, 2), (0, 2, 0), 
 (0, 2, 1), (0, 2, 2), (1, 0, 0), (1, 0, 1), (1, 0, 2), (1, 1, 0), (1, 1, 1), 
 (1, 1, 2), (1, 2, 0), (1, 2, 1), (1, 2, 2), (2, 0, 0), (2, 0, 1), (2, 0, 2), 
 (2, 1, 0), (2, 1, 1), (2, 1, 2), (2, 2, 0), (2, 2, 1), (2, 2, 2)]

 さらに、mapfilterの場合に渡す関数は、引数が1個である必要がありましたが、内包表記ではそのような制限がありません。

内包表記での関数の利用
>>> # 母音を全て削除する
>>> import re
>>> [re.sub(r'[aiueo]', r'', s) for s in ["Python", "Perl", "Java", "Haskell"]]
['Pythn', 'Prl', 'Jv', 'Hskll']

 ここで出てきたreは、正規表現モジュールです。re.sub(pattern, repl, string)は、string内のpatternreplで置換するコマンドです。これにより、リスト内の文字列の全てを置換したリストを作っています。

 また、r'文字列'の形式は、raw stringと呼ばれるもので、普通の文字列と違って\nなどのエスケープシーケンスが展開されません。正規表現を記述する場合にはエスケープシーケンスが展開されない方が便利なので、この記述を使うことが多いです。

enumerate()とzip()

 さらに、かゆいところに手が届く関数を2つ紹介します。

 まず、enumerate()です。イテレータ処理時に、ループ内で連番が欲しくなって自分でカウンターを作ってインクリメントするというのはよくある話ですが、これはその処理をやってくれます。リストへenumerate()を噛ませると、リストの値は、(連番, リストの値)という要素に変換されます。カウンター処理と要素の取り出し処理が両方同時にでき、すっきりした記述が可能です。

enumerate()関数
>>> # リストに番号をつけて表示する
>>> for i, item in enumerate(['Python', 'Perl', 'Java', 'Haskell']):
...     print "%d: %s" % (i + 1, item)
... 
1: Python
2: Perl
3: Java
4: Haskell

 次に、zip()関数です。この関数を使うと、複数のリストから同時に値を取り出すことができます。zip()関数にlist1とlist2を与えると、ループごとにそれぞれのリストから値が一つずつ返ってきます。例えば、2つのリストに辞書型のキーと値にしたいような値が含まれているときは、この関数を使うと便利です。

zip()関数
>>> names = ['Takeshi', 'Satoshi', 'Yoshio', 'Kazuki']
>>> ages  = [       22,        30,       28,       32]
>>> for name, age in zip(names, ages):
...     print "%s は %d 歳です" % (name, age)
... 
Takeshi は 22 歳です。
Satoshi は 30 歳です。
Yoshio は 28 歳です。
Kazuki は 32 歳です。

 forでカウンタ変数を使わないPythonにとって、これらの関数が可読性の高いコードを書くためにとても有効なツールとなっています。

イテレータを作る

 ここまで、リストを有効に利用するための内包表記や各関数について紹介してきましたが、これらの事項はイテレータに対しても適応できます。イテレータは、繰り返し処理の開始時にはどのくらい繰り返すのか決定しにくい(できない)ような処理で大きな威力を発揮します。ファイルの行単位での読み込み処理がいい例です。ファイルを最後まで読まない限りファイルが何行あるのかはわからないため、ループ開始時には何回処理を繰り返さなければならないのかがはっきりしません。このような場合に、イテレータが活躍します。

 さて、Pythonでイテレータとは、イテレータプロトコルに準拠したオブジェクトのことです。このプロトコルに準拠さえすれば、どのようなオブジェクトもイテレータとなり得ます。しかし、例えば、素数を返すイテレータ作るために、いちいちイテレータプロトコルに準拠したクラスを書くのは面倒なことです。

 そこで、もっと直感的にイテレータを生成する方法が用意されています。それが、ジェネレータ関数という仕組みです。ジェネレータ関数は普通の関数と同じような記述で宣言をしますが、returnの代わりにyeildという句を使います。yeildは値を関数の外へ返しますが、この時にreturnと違って関数の内部状態を維持します。そして、次にまた値が必要とされた時には、前回yieldした続きから処理が始まります。

 ここでは、例として1が出るまでサイコロを降り続けるイテレータを作成してみます。サンプルコードで利用しているrandomモジュールは名前の通り乱数のモジュールで、さいの目を作成するのに使っています。

ジェネレータ
>>> # 1 が出るまでサイコロの目を返し続けるイテレータ
>>> import random
>>> def throw_dice():
...     n = 0
...     while n != 1:
...             n = random.randint(1, 6)
...             yield n
... 
>>> # さいころをふってみる
>>> for dice in throw_dice():
...     print "さいころの目は、 %d でした" % dice
... 
さいころの目は、 2 でした
さいころの目は、 3 でした
さいころの目は、 6 でした
さいころの目は、 6 でした
さいころの目は、 1 でした

 throw_dice()関数の戻り値がイテレータになっていることがわかるでしょうか? yieldを使うだけで、こんなに簡単にイテレータが作れてしまいます。

 当然、このイテレータにmap()を適用することもできます。

イテレータにmap()を適用
>>> # さいころの目を二乗にする
>>> map( lambda n: n ** 2, throw_dice() )
[9, 16, 16, 4, 1]

 また、内包表記でイテレータを作ることもできます。リスト内包表記では先に説明したように大括弧[]を使いましたが、これを丸括弧()に変えると、リストではなくイテレータが生成されます。()による内包表記はジェネレータ式と呼ばれ、リストの内包表記とは違って式が遅延評価されます。扱う要素が多い場合は、()によるジェネレータ式の方が有利です。

ジェネレータ式によるイテレータの作成
>>> # さいころの目を二乗にする
>>> for n in (dice ** 2 for dice in throw_dice()):
...     print n
... 
4
16
9
9
4
1

 このように、Pythonではイテレータを簡単に作成する手段と、それを至る所で利用できるようにする仕組みが整っています。ここまでに述べた処理を使いこなせば、一つ間違えば単調になりがちな繰り返し処理を、ダイナミックかつスマートに記述することができます。


  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • hiratara(ヒラタラ)

    1977年に苫小牧市で生まれる。北海道大学理学部数学科卒。小学生の頃、両親に買い与えられたMZ-2500でプログラミングを始めた。学生時代、CGIの自作に没頭し、それ以降WEB開発の魅力に憑かれる。社会人になっても数学好きは変わらず、専門書を買い集めるのが最近の趣味。 id:hirataraに...

あなたにオススメ

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5