0. Python の気になる機能
Python チュートリアル の目次をざっと眺め、目にとまったのは次の二つの機能。
両方とも耳馴れない言葉。 (@_@) まずは、リストの内包表記から調べることに。
1. リスト内包表記の基本
a. リストの各要素に関数を適用する
最初は、リスト内包表記の使い方を確認する。例として、「数値のリストの各要素を 2 倍する」
print [x*2 for x in [1,2,3,4,5]]
初めて見たとき、どこから読めばいいのか検討がつかなかった。
読む順番は、
- for によって、各要素を取り出し、
- 各々に対して、操作を加える。
というように、後ろから前へと読み進める。
b. リストの各要素から、条件にあったものを抽出する
次に、同じく数値のリストから、特定の条件に合う要素を抽出する。
print [x for x in [1,2,3,4,5] if x > 3]
これまた読みにくい。 (@_@;)
c. 要素に対する関数の適用と、抽出を組み合わせる
では、上記二つを組み合わせてみる。
print [x*2+100 for x in [1,2,3,4,5] if x > 2 and x < 5]
慣れないと、どこから読めばいいのやら、迷ってしまう。
追記(2008.6.1) : ちなみに、「Python の好きなところ - kwatchの日記」 によると、Python では、条件の記述において、
2 < x < 5
と書ける。よって、上記のリスト内包表記は、
[x*2+100 for x in [1,2,3,4,5] if 2 < x < 5]
と記述できる。
2. リストの要素を組み合わせる
リスト内包表記において、for を複数記述すれば、リストの要素を組み合わせたリストを生成できる。
print [[x,y,z] for x in [1,2,3] for y in [10,20,30] for z in ['a','b','c']]
上記のように、改行とタブで整形すれば、少し読みやすくなるかな。一行で書かれあるものなんて読みたくないなぁ。
もし、リストの要素を組み合わせるのに、リスト内包表記を使わないならば、
ary = [] for x in [1,2,3]: for y in [10,20,30]: for z in ['a','b','c']: ary.append([x, y, z]) print ary
for ループが 3 重にネストするため、ちょっと読みにくい。
追記(2008.8.19) : 「情報の論理数学入門」 (p14) によると、
「二つの集合 A, B の直積集合 (Cartesian product set) A × B は第 1 成分が集合 A の元で、第 2 成分が集合 B の元となる順序対のすべての集合である。 A, B がともに有限集合のとき、直積集合の元の数は A と B おそれぞれの元の数の積である。」
上記のリスト内包表記は、「直積」と考えればよい。数学の表記に似せてあることがわかる。
3. Ruby でリスト内包表記と同等のものを書く
上記のリスト内包表記で得られる結果と、同じものを Ruby で書いてみる。
puts [1,2,3,4,5].map{|i| i*2} puts [1,2,3,4,5].select{|i| i > 3} puts [1,2,3,4,5].select{|i| i > 2 and i < 5}.map{|i| i*2+100}
Ruby の方が、
「リストに対して条件を設定し、それに操作を加える」
という思考の流れと、コードの流れが一致しているので書きやすい。
ただし、Ruby にはリスト内包記法がないので、リスト要素の「組合せ」を、Python のように素直に表現できないようだ。
4. リスト内包表記の入れ子は、読みづらくなる
リスト内包表記は、ネストさせることができる。しかし、段々と暗号のように見えてくる。 (@_@;)
print [[y for y in [2,3,4,5,6] if y == x] for x in [3,4,5]] print [[y for y in [2,3,4,5,6] if y in x] for x in [[1,2,3],[3,4,7]]] print [[y for y in ['h','o'] if y in x] for x in ["hoge","piyo"]]
5. リスト内包表記の「内包」の意味
リスト内包表記における、「内包」の意味は、
元々は公理的集合論の用語で、
- 内包(intension)記法: {f(x) | x ∈ A} みたいな書き方。
- 外延(extension)記法: {a, b, c, d, e, …} みたいな書き方。
つまり、集合の要素を、個々に指し示すのではなくて、性質によって表わすということ。
しかし、数学的な表現と比べると、Python の表記は読みにくい。 for と if が、そもそも構造化プログラミングのための記法なので、それを流用して表現しているのでダメなのかな?
a. Haskell のリスト内包表記の書き方との比較
これに対して、 Haskell のリスト内包表記は読みやすい。
main = do print [x*2| x <- [1,2,3,4,5]] print [x| x <- [1,2,3,4,5], x > 3] print [x*2+100| x <-[1,2,3,4,5], x > 2, x < 5]
「ふつうのHaskellプログラミング」 (p148) によると、
リスト内包表記を使ったコード例としてはクイックソートを実装したqsort関数がよく挙げられます。
「About Haskell」 の「クイックソート」は、リスト内包表記が使われているので、シンプルに書かれている。
qsort [] = [] qsort (x:xs) = qsort elts_lt_x ++ [x] ++ qsort elts_greq_x where elts_lt_x = [y | y <- xs, y < x] elts_greq_x = [y | y <- xs, y >= x]
b. C# での書き方
「クイックソート対決(Haskell vs C#) - 医者を志す妻を応援する夫の日記」 には、C# による記述の方法が書かれている。
static IEnumerableQuickSort(IEnumerable a) { if (a.Count() == 0) return a; int p = a.First(); var xs = a.Skip(1); var lo = xs.Where(y => y < p); var hi = xs.Where(y => y >= p); return QuickSort(lo).Concat(new[] { p }).Concat(QuickSort(hi)); }
お~、こんな風に書けるのかぁ~ (@_@)
6. map(), filter(), lambda と リスト内包表記
5.1.4 リストの内包表記 によると、
リストの内包表記 (list comprehension) は、リストの生成を map() や filter() や lambda の使用に頼らずに行うための簡潔な方法を提供しています。
ということで、map(), filter(), lambda について調べてみる。
a. lambda
最初に lambda 。
4.7.5 ラムダ形式 によると、
キーワード lambda を使うと、名前のない小さな関数を生成できます。例えば "lambda a, b: a+b" は、二つの引数の和を返す関数です。
ちなみに、Ruby では、次のように lambda を使う。
def make_incrementor(n) return lambda{|x| x + n} end f = make_incrementor(42) puts f.call(0) puts f.call(1)
- cf. 組み込み関数 - Rubyリファレンスマニュアル, Rubyのlambda,proc関数の使い方が良く分かりません。 「こういうときに使うと便利」という例があれば教えてください。 - 人力検索はてな
個人的には、proc{ ... } や Proc.new { ... } よりも lambda { ... } の方が読みやすいかなぁ。
追記(2011.11.14) : Ruby 1.9 より、矢印ラムダで書くことができる。
def make_incrementor(n) return ->(x){ x + n } end
当初、Ruby はブロックの使い方が特徴的なので、同じように lambda { … } と書くほうが良いと感じた。しかし、無名関数くらいは、特別扱いした方が見やすいかな。
Python では、Ruby とは書き方が、微妙に異なる。
lambda a, b: a + b
Ruby のように、lambda がブロックとして、他同列の扱われ方をしているのではなく、無名関数として、特別な記法が前提とされている。
b. map, filter 関数
2.1 組み込み関数 には、map と filter 関数の説明が述べられている。
map(function, list, ...)
filter(function, list)
先ほどのコードを、map と filter を使って書き直すと、
print map(lambda x: x*2, [1,2,3,4,5]) print filter(lambda x: x>3, [1,2,3,4,5]) print map(lambda x: x*2+100, filter(lambda x: x>2 and x<5, [1,2,3,4,5]))
Python の組み込み関数は、Ruby と比べると、読みやすさの点では劣るかもしれない。
c. reduce 関数
ついでに、リスト操作として便利な、「5.1.3 実用的なプログラミングツール」 で挙げられていた reduce() を試す。
-
reduce(function, sequence[, initializer])
- sequence の要素に対して、シーケンスを単一の値に短縮するような形で 2 つの引数をもつ function を左から右に累積的に適用します。例えば、
reduce(labmda x, y: x+y, [1, 2, 3, 4, 5])
は((((1+2)+3)+4)+5)
を計算します。
なるほど、reduce は、「短縮する」という意味なのか。Ruby の inject より、名前的にいいかも。