ラベル 配列 の投稿を表示しています。 すべての投稿を表示
ラベル 配列 の投稿を表示しています。 すべての投稿を表示

2008年6月14日土曜日

Python の シーケンス型に慣れる

1. 配列の操作

プログラミングは、習うより慣れろ。

そう、避けるべきは黙読で、すべきは朗読です。文書は声に出して読まなければ身につきません。とはいっても、ソースコードまで音読していたらそれはそれで間抜けです。ソースコードは音読ではなく書き写す、すなわち写経するのがいいでしょう。

(本気でやるなら黙読は避けて朗読すべき: Days on the Moon  より)

新しく言語をはじめる場合、最初に確認する書き方の一つに「配列」がある。

配列操作の比較表: Ruby, Python, JavaScript, Perl, C++ - bkブログによると、

プログラムを書いていると、他のプログラミング言語の記憶とごっちゃになって、「配列の後ろに要素を追加するのは push だっけ、 append だっけ」などと混乱することがあります。

これを見ながら、写経することにした。 ( ̄人 ̄)ナムナム

a = [3,2,1]
b = [10,200,30]

# 大きさ
print "len(a) :", len(a)

# 空か?
if len(a) == 0: print "empty"
else:           print "not empty"
# 末尾に追加
a.append(4)
print a

# 挿入
a.insert(2, 100)
print a

# 末尾の要素を取り出す
print a.pop()
print a

# 先頭の要素を取り出す ※
print a.pop(0)
print a

# 別の配列を足す
a.extend(b)
print a

# 特定の値を削除する
a.remove(100)
print a

# 特定の位置にある要素を削除する
del(a[2])
print a
# 特定の値の数をカウントする
print a.count(3)
print a.count(0)

# 特定の要素を含んでいるか調べる
print 3 in a
print 4 in a
# 先頭の要素
print a[0]
# 末尾の要素
print a[-1]

# 特定の区間の要素
print a[1:3]    # 1 ~ 2 まで
# sorted は要素自体を並び換えない
print "sorted(a) :", sorted(a)
print a

# sort は配列自体を並び換える
print a
a.sort()
print a

# 要素を末尾から辿る
for elem  in reversed(a):
    print elem

# reversed は要素自体を操作しない
for x in reversed(a):
    print x
print a

# reverse は要素を自体を逆順にする
a.reverse()
print a
# 配列の要素を、特定の文字列でつなげる
print ', '.join("abc")
print ', '.join(str(x) for x in a)

# 要素を全てクリアする
b = a
a[:] = []
print a
print b

 

uniq

Python で、Ruby の Array#uniq に相当するものは、組み込み関数の set(), list() を使い、 list(set(リスト)) とする。

set([iterable])

集合を表現するset 型オブジェクトを返します。

list([sequence])

sequence の要素と同じ要素をもち、かつ順番も同じなリストを返します。

 

join

Python と Ruby の違うところは、 join が 文字列のメソッドとして定義されていること。

Python はなぜ String#join か - TokuLog 改めChumbyとどきました日記 によると

これ、理由は明解で、「join メソッドは文字列の処理だから」です。

それ以下でもそれ以上でもない。

文字列の処理をするメソッドを、どんなものでも入る Array Class のメソッドとしてはやすなんてとんでもない!

そう考えるのが Pythonista クオリティ

(via Python の join が str のメソッドになっている理由 - odz buffer)

 

2. Python は配列クラスに定義されているメソッドが少ない

こうやって見ると、Ruby は語彙が豊富で何でもありの言語だとすると、Python は正当な表記しか許さない律儀な言語。

「今の若者の言葉の乱れは...」

とでも言いそうな雰囲気を感じる。

認知的経済性から考えると、Python の方が言語としてシンプルで良い。独特の言い回しも許容する、言語の豊かさという視点から見たら、Ruby は使いやすい。

Python には、

「要素をリストの先頭に入れる」関数

に特別な名前がない。insert() による表現の一部になっている。これに対して、Ruby では unshift() という特別な名前が与えられている。

同様に Python では

「リストの先頭の要素を取り出す」関数

は pop() メソッドで代替する。Ruby では shift() という名称が与えられている。

「要素の取得」

においても、Ruby では .first, .last という特別な名前が与えられている。

Ruby は人が使う言葉に沿うようなメソッド名が付けられる傾向があるようように感じる。Python は、「あるもので間に合わせろ」ということか。

String クラスで定義されているメソッドの数を比較からも、そういう雰囲気が伝わってくる。

 

3. 文字列とリスト

Python では、文字列を for ループに投入すると、一文字ずつ処理が行われる。

for c in "hoge":
    print c

for c in u"ほげほげ":
    print c

これに対して、 Ruby では、hoge という一つの文字列として処理される。

for c in "hoge"
  puts c
end

一文字ずつ処理を行いたい場合は、split や scan を使って文字列を分割しなくてはならない。

"hoge".split(//).each do |c|
  puts c
end

"hoge".scan(/./).each do |c|
  puts c
end

# 文字コードが Shift_JIS の場合
"ほげほげ".split(//s).each do |c|
  puts c
end

 

3. シーケンス型

list() 関数に、次のような説明がある。

例えば、list('abc')['a', 'b', 'c'] および list(1, 2, 3)[1, 2, 3] を返します。

これを見て、 Haskell を思い出した。

ふつうのHaskellプログラミング によると、

実は、 Haskell では文字列もリストなのです。文字列は文字のリストとして表現されていて、特別な文字列は存在しません。ですから、リストの処理を覚えてしまえば文字列処理も身についたことになります。 (p36)

Haskell では、 "abc" は、['a', 'b', 'c'] と同じ。なぜなら、String 型は [Char] 型の別名に過ぎないため。

Python のライブラリリファレンスに、組み込み型の説明がされている。

ここに、リスト文字列の上位にある

「シーケンス型」

というものが定義されている。

上記で写経した sorted, reversed メソッドの位置付けも、これを見ると理解できる。

結構、イテレータ型が重要。

2008年3月26日水曜日

配列を「集計」するときの手順のわかりにくさ

1. 配列の要素を集計をするときに感じた違和感

配列の要素を集計をすることを考える。

例えば、1 ~ 5 までの整数に対して、

1 + 2 + 3 + 4 + 5

の答えを求める場合、次のような手順を踏む。

Ruby で書くなら、

ary = [1, 2, 3, 4, 5]

total = 0
for elem in ary
  total += elem
end

puts total

もちろん、慣れているので、この計算の方法に抵抗は感じない。

しかし、この手順を初めて見たとき、

「わかりにくい」

と感じた。極めてシンプルなコードなんだけれど、どこか頭が混乱するような、そんな感覚に陥ったことを覚えている。

では、その原因はどこにあったのだろうか?

 

2. わかりにくい理由は、「要素の走査」「計算」「変数の再代入」を行なっているから

頭が混乱する印象を受けたのは、以下の箇所。

total += elem

コードの意味を、わかりやすくするために、書きなおす。

total = total + elem

全体のコードで行なっていることは、

  1. for によって要素を走査し、
  2. その途中で計算を行う。

気になるのは、total の使い方。 配列を走査している最中に、

  1. 集計するための total から値を取り出し、
  2. それを計算の後、再び total へ設定する。

つまり、以下の 3 つの要素が絡み合っているため、何をやっているのか想像しにくい。

  • 要素の走査
  • 計算
  • 変数の再代入

 

3. 最初は集計の方法をイディオムとして覚えた

プログラミングを始めた当初は、厳密にどのように動いているかというよりも、

このようなイディオムによって集計を行うんだ

と、感覚的な理解をしていた。

今では、配列の要素を「集計」しようと考えたとき、自然と上記のようなコードを書く。しかし、これがどのように動くかを、頭の中で全て思い描けるかと言えば、実はあやしい。デバッガを動かし、それぞれの変数の動きを目で追い、「なるほどなぁ」と思う。それでもコードを見ると、だまされたような感覚に陥る。

ところで、自分の頭の容量は小さい。同時に色々なことを覚えておくことも、把握しておくこともできない。将棋の棋士は、同時に何手先も読むと言うが、あれはいったいどういう頭の仕組みをしているのだろうか。だから、自分の「集計」に対する理解は、まるで刺激に対する反応するようなものだと感じる。これが要求されたら、この方法をとる。まるで、条件付けされているパブロフの犬のようだ。

 

4. 計算の様子をイメージすると、わかりにくい理由がより明確になる

集計をするコードに戻る。

変数 total が、全体を理解する上で、やっかいな存在。 最初に変数が宣言されているのが、for ループの外にある。つまり、要素を走査することとは、別の文脈に存在する。それが要素を走査している文脈に絡んでくるからわかりにくい。

「いつ、どこで、何をしているのか?」

が把握しにくい。

特に、total に elem の値を加算した後、自分自身に再代入しているので、

「誰がどうなったの?」

って感じる。 (@_@;)

イメージしにくいので、絵を描いてみる。そうするば、見通しが立てやすくなる。

080326-001

  1. 配列の要素を elem に割当てる。
  2. total と elem を加算する。
  3. 上記の結果を total に割当てる。

1 ~ 3 を要素ごとに繰り返す。

絵を描いてみると、「加算」を行っている文脈と、 total が存在する文脈が異なっていることがはっきりする。しかし、コード上では、

total = total + elem

のように、一文で簡潔に表現しされている。

この点が、自分のように容量の小さい頭には、イメージするのが難しい所。複数の文脈が一箇所に集約されていることが混乱の元になっている。

 

5. 計算の手順を分離し、各々役割をクラスに与える

では、これを理解しやすくするには、どうすればいいのだろう?

「要素の走査」「計算」「変数の再代入」を、次のように二つに分けて考えることにした。

  • 「要素の走査」
  • 「計算」「変数の再代入」

下図のようなイメージした。

080326-002

集計をするための Total クラスを作り、集計をするための役割を与えた。つまり、「計算」「変数の再代入」の操作を Total クラスにカプセル化。 (まぁ、普通こんなことはしないけれど ^^;)

# 集計の値を保持するクラス
class Total   attr_reader :value  # 集計の値   # 初期化   def initialize    @value = 0   end   # 集計の値に val を加算   def add(val)    @value += val   end
end

add メソッドの中身を見ると、「与えられた値を、これまでの値に加算する」という、極めてシンプルな役割を持っていることがわかる。

このクラス使うには、以下のようにする。

ary = [1, 2, 3, 4, 5, 6]

total = Total.new
for elem in ary
  total.add(elem)
end

puts total.value

for ループの中身を見ると、要素を走査し、Total に「要素の値を追加してね☆」とお願いしているだけになる。

繰り返すが、普通こんなコードは書かない。ただし、自分のようにワーキングメモリの小さい脳みそにとって、このような表現の方が、動作の理解はしやすいと感じる。

 

6. Enumerable の inject を使う場合

ところで、Ruby を使っているなら、イテレータを使って、次のように書くことができる。

total = 0
[1, 2, 3, 4, 5, 6].each do |elem|
  total += elem
end
puts total

Enumerable の inject メソッドを使えば、より簡潔に、

puts [1, 2, 3, 4, 5, 6].inject{|result, item| result + item}
ただし、シンプルだけど、動作をイメージしにくい。

Enumerable - Rubyリファレンスマニュアル によると、

inject([init]) {|result, item| ... }

最初に初期値 initself の最初の要素を引数にブロックを実行します。2 回目以降のループでは、前のブロックの実行結果と self の次の要素を引数に順次ブロックを実行します。そうして最後の要素まで繰り返し、最後のブロックの実行結果を返します。 ...

初期値 init を省略した場合は、最初に先頭の要素と 2 番目の要素をブロックに渡します。この場合、要素が 1 つしかなければブロックを実行せずに最初の要素を返します。要素が空なら nil を返します。

inject の意味は、Yahoo!辞書 - inject によると、

1 …を(…に)注入する((into ...));…に(…を)注入[導入]する, 入れる((with ...))

inject a tank with water [=inject water into a tank] タンクに水を注ぎ入れる

ブロックで実行した結果を、再注入するという意味合いなのだろうか?

あぁ~、それにしても動作を想像しにくい。 パタッ(o_ _)o~†

とりあえず、絵に描いておこう。

080326-001

  1. 先頭から 2 つ要素を取り出し、ブロック変数へ入れる。
  2. ブロックで実行した結果と、次に要素を取り出し、ブロック変数へ入れる。

これを末尾まで繰り返す。

 

7. 「再帰」で集計するには

for ループを使った計算は、再帰的な定義で置き換えることができる。

ary = [1, 2, 3, 4, 5, 6]

def total(ary)
  return ary[0] if ary.size == 1
  return ary[0] + total(ary[1..ary.size-1])
end

puts total(ary)

これは、次のように集計の手順を考えていると言える。

合計 = 要素の先頭 + 2 番目以降の要素の合計

要素が一つしかない場合は、当然、次のようになる。

合計 = 要素の先頭

イメージとしては、

080326-002

「先頭と、それ以降、という構造」が、リストの至るところで見られると見なし、順次関数を適用していく方法。

この方法では、「要素の走査」という部分が、「再帰的な関数の適用」にすりかわり、「変数の再代入」が消失し、「計算」のみが残っている。しかし、これが直観的にわかりやすいかどうかと問われたら、微妙。 ^^;

慣れの問題なのかな?