2008年5月31日土曜日

Python のリスト内包表記 - Ruby や Haskell の書き方と比べながら

0. Python の気になる機能

 Python チュートリアル の目次をざっと眺め、目にとまったのは次の二つの機能。

両方とも耳馴れない言葉。 (@_@) まずは、リストの内包表記から調べることに。

 

1. リスト内包表記の基本

a. リストの各要素に関数を適用する

最初は、リスト内包表記の使い方を確認する。例として、「数値のリストの各要素を 2 倍する」

print [x*2 for x in [1,2,3,4,5]]

初めて見たとき、どこから読めばいいのか検討がつかなかった。

読む順番は、

  1. for によって、各要素を取り出し、
  2. 各々に対して、操作を加える。

というように、後ろから前へと読み進める。

 

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 おそれぞれの元の数の積である。080819-003

上記のリスト内包表記は、「直積」と考えればよい。数学の表記に似せてあることがわかる。

 

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, …} みたいな書き方。

[雑記] クエリ式とリスト内包 (C# によるプログラミング入門) より)

つまり、集合の要素を、個々に指し示すのではなくて、性質によって表わすということ。

しかし、数学的な表現と比べると、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 IEnumerable QuickSort(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)

個人的には、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 より、名前的にいいかも。

 

関連記事

2コメント:

匿名 さんのコメント...

qsort = lambda a: qsort([x for x in a[1:] if x <= a[0]]) + a[0:1] + qsort([x for x in a[1:] if x > a[0]]) if a[1:] else a

etc さんのコメント...

英語で合計を求める際、こんな感じで書くそうです。

sum, for i = 0 to 100

これは0から100の合計を求める例です。forがループに使われる理由もこれが元ネタなんですよね。
何が言いたいかというと、リスト内包表記は英語表記だとそのままだという事です。なので、英語に慣れ親しんでおけば、どれだけ長く書いたところで混乱しないんじゃないでしょうか。