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

2008年8月25日月曜日

Python のイテレータ (3)

以前、「Python のイテレータをジェネレータで作成」で、次のようなコードを書いた。

091126-005.png

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return self.name + " " + str(self.age)
        
class Group:
    def __init__(self):
        self.persons = []

    def add(self, person):
        self.persons.append(person)
        return self
    
    # ジェネレータ。__iter__(), next() の置き換え。
    def iter(self):
        for person in self.persons:
            yield person
        

group = Group().add(Person("Tarou", 21)).add(
                    Person("Hanako", 15)).add(
                    Person("Jiro", 15))

# iter() を呼出す
for person in group.iter():
    print person.name

for a in group.iter():
    print a.age

この書き方だと、イテレータを呼びだすときに、「Python のイテレータ」で __iter__(), next() を自前で書き、それに対して for ループを適用するときに比べて group.iter() と for ループで呼出さないといけないから、何だかなぁ~って思っていた。 (@_@;)

 

__iter__() に変更

ところで、イテレータとはそもそも何かと言えば、次のようにイテレータプロトコルに従っていればよい。

イテレータオブジェクト自体は以下の 2 のメソッドをサポートする必要があります。これらのメソッドは 2 つ合わせて イテレータプロトコル を成します:

__iter__()

イテレータオブジェクト自体を返します。このメソッドはコンテナとイテレータの両方をfor および in 文で使えるようにするために必要です。

next()

コンテナ内の次の要素を返します。もう要素が残っていない場合、例外 StopIteration を送出します。

(2.3.5 イテレータ型 より)

このことを考えると、上記のコードの場合、「Python のイテレータ」で __iter__(), next() を自前で書いていたときは Group クラスがイテレータプロトコルに従っており、Group クラス自体がイテレータオブジェクトということになる。しかし、上記で示したコードになった場合、Group#iter() の呼出しによりジェネレータが作成され、これがイテレータの役割をする。

ところで、ジェネレータは Python のジェネレータ (1) の最初の例で述べたように、

(…) yield を使って定義した関数が、あたかもオブジェクトのように振る舞っているように見える。さながら、 generator1() によってインスタンス化されたオブジェクトがあり、それが next() というメソッドを持っていて、呼出す度に yield で動作を止め、そのとき yield に渡された値を返し、そして最後の yield 呼出しが終ると次回 next() の呼出しで、StopIteration を投げる。

というようにイテレータプロトコルに従っている。つまり、イテレータオブジェクトと言えるのだろう。多分。 ^^;

話を戻して、上記に示したコードのようにジェネレータ関数を Group クラスに定義したということは、もはや Group クラスはイテレータプロトコルに従っているわけではなく、イテレータオブジェクトではなくなってしまっている。あくまでもイテレータオブジェクトなのは、Group 関数に定義したジェネレータ。当然ながら、Group クラスのインスタンスをそのまま for ループの中では適用できなくなっている。

これでは何かメリットが失われてしまったと感じるので、次のようにコードを修正した。修正箇所は簡単で、

def iter(self):

def __iter__(self): 

に変更するだけ。

091126-006.png

 

念のためコードを示す。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return self.name + " " + str(self.age)

class Group:
    def __init__(self):
        self.persons = []

    def add(self, person):
        self.persons.append(person)
        return self

    # ジェネレータ
    def __iter__(self):
        for person in self.persons:
            yield person


group = Group().add(Person("Tarou", 21)).add(
                    Person("Hanako", 15)).add(
                    Person("Jiro", 15))

for person in group:
    print person.name

for a in group:
    print a.age

これにより、for ループでの記述が以前のようにシンプルになった。

 

__iter__() の実装を変更

以前に、Beautiful Soup を使ったことがある。このソースコードの中では __iter__() が使われているだろうかと思って調べてみたら、 Tag クラスに定義されており、その実装は iter() 関数を呼出し、その引数に Tag クラスのインスタンス変数であるシーケンス型のオブジェクトが渡され、それが返されようになっていた。

return iter(self.contents)

2.1 組み込み関数 によると、

iter(o[, sentinel])

イテレータオブジェクトを返します。2 つ目の引数があるかどうかで、最初の引数の解釈は非常に異なります。2 つ目の引数がない場合、 o反復プロトコル (__iter__() メソッド) か、シーケンス型プロトコル (引数が 0 から開始する __getitem__() メソッド) をサポートする集合オブジェクトでなければなりません。…

(太字は引用者による)

これを真似するなら、上記の __iter__() の実装を変更する。

    def __iter__(self):
        return iter(self.persons)

こちらも一応全てのソースコードを示すと、

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return self.name + " " + str(self.age)

class Group:
    def __init__(self):
        self.persons = []

    def add(self, person):
        self.persons.append(person)
        return self

    def __iter__(self):
        return iter(self.persons)


group = Group().add(Person("Tarou", 21)).add(
                    Person("Hanako", 15)).add(
                    Person("Jiro", 15))

for person in group:
    print person.name

for a in group:
    print a.age

うーん、どちらがいいんだろう…。 (@_@;)

追記 (2009.11.26) : 変更可能なシーケンス型 は、シーケンス型プロトコルに多分従っているだろうから、iter 関数使った方が効率的。反復プロトコル、または、シーケンス型プロトコルに従っていないタイプで要素を保持する場合、yield を使ってジェネレータを生成するのが良いだろう。

 

関連記事

2008年3月30日日曜日

Ruby のイテレータ (2) – Enumerable と Comparable モジュール

Ruby のブロック付きメソッドとイテレータ - yield の様々な使い方」のつづき

1. 要素を保持する親クラスと、要素となる子クラス

080329-004次のような例を想定する。

  1. 「人」が「グループ」 に所属している。
  2. 「人」は `名前' と `年齢' を属性として持つ。

「グループ」クラスは親クラスで、「人」クラスは要素となる子クラス。

class Person
  attr_reader :name , :age
  def initialize(name,age)
    @name = name
    @age = age
  end
end

親クラスとなる「グループ」は、保持する「人」の集合に対して責務を持っており、「人」を「グループ」に追加する操作を持つとする。

class Group
  def initialize
    @persons = []
  end
  def add(person)
    @persons << person
    self
  end
end

 

2. Enumerable モジュールを親クラスにインクルード

「グループ」クラスには、「人」を追加する操作だけではなく、

  • 特定の「人」を抽出したり
  • 全ての人が、ある条件を満たすのか調べる

ようなメソッドも定義したい。 Ruby では、自分でそのような操作を実装しなくても、「グループ」に Enumerable モジュールをインクルードするだけで、便利なメソッドがいくつも追加される。

Enumerable モジュールを「グループ」にインクルードする方法は、

  1. 「グループ」クラス内に、include Enumerable と記述。
  2. 「グループ」に、「人」の集合の要素を順番に取り出す each メソッドを定義。

このような Enumerable モジュールの使い方を Mixin と呼ぶ。

each メソッドを定義する理由は、Enumerable - Rubyリファレンスマニュアル  によると、

(Enumerable クラスは) 繰り返しを行なうクラスのための Mix-in。このモジュールのメソッドは全て each を用いて定義されているので、インクルードするクラスには each が定義されていなければなりません。

 

each メソッドの実装方法

each メソッドの実装、要素を保持する親クラスから、要素をひとつづつ取り出し、その要素にブロック(関数)を適用すること。以下に、Group クラスの each メソッドの実装を示す。

class Group
  include Enumerable
  def each
    @persons.each do |person|
      yield person
    end
  end
end

each メソッドの中で、yield が使われている。この記述で、yield の呼び出しの際、「グループ」が保持している各々の person を、each メソッドに与えられるブロックに引き渡していることになる。

この書き方でわかりづらい点は、each メソッドの引数にブロックが与えられることが明示されていないこと。メソッドの中で、yield が使われてることで、メソッドにブロックが与えることを想定しているがわかる。

( cf. jutememo's gist: 1350439 — Gist )

 

each メソッドを利用してみる

以下の順に、実際に each メソッドを使ってみる。

  1. 「グループ」オブジェクトを生成し、
  2. 「人」オブジェクトを追加する。
  3. 「グループ」オブジェクトの each メソッドを呼出す。
aGroup = Group.new.
         add(Person.new("Tarou",21)).
         add(Person.new("Hanako",15)).
         add(Person.new("Jiro",15))
                                  

aGroup.each do |person|
  puts person.name
end

 

Enumerable モジュールに定義されている collect , sort を使ってみる

Enumerable を Mixin すると、Enumerable モジュールで定義されている collect, sort などのメソッドを呼出すことができる。この2つのメソッドを試しに使ってみる。

p aGroup.collect{|person| person.age + 10}

aGroup.sort{|a,b| a.age <=> b.age}.each do |person|
  print person.name, person.age, "\n"
end

上記において、sort メソッドの呼び出しのとき、ソートの方法をブロックで渡している。

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

sort {|a, b| ... } 全ての要素を昇順にソートした配列を生成して返します。 ブロックなしのときは <=> メソッドを要素に対して呼び、その結 果をもとにソートします。

ここでは、Person クラスにおいて、<=> を定義してないので、上記のように、要素を比較する方法を、ブロックを渡す必要があった。

 

3. オブジェクトを比較するクラスに Comparable モジュールをインクルード

Enumerable の Mixin の方法がわかったところで、今度は上記の sort メソッドで利用された、「人」を比較するためのメソッドを「人」クラスに追加してみる。「人」オブジェクトを比較可能にするには、Comparable モジュールを Person クラスに Mixin する。

一例として、「人」を比較するためルールを、以下のように決める。

  1. 「はじめに `年齢' で比較し、
  2. もし同じ年齢だったら、`名前' のアルファベット順で比較する」
class Person
  include Comparable
  attr_reader :name , :age
  def initialize(name,age)
    @name = name
    @age = age
  end
  def <=>(other)
    cmp = @age <=> other.age
    if cmp != 0
      return cmp
    else
      return @name <=> other.name
    end
  end
end

tarou = Person.new('Tarou',21)
jirou = Person.new('Jirou',15)
hanako = Person.new('Hanako',15)

p tarou > jirou
p tarou < jirou
p tarou == jirou

p jirou > hanako
p jirou < hanako
p jirou == hanako

Enumerable モジュールは、each メソッドを利用して、様々なメソッドが定義されている。Comparable モジュールでは、<=> を利用して、いくつかの操作が定義されている。

上記のように、Person に <=> を実装したので、Group に Mixin した Enumerable モジュールの sort メソッドを、ブロックなしで呼出せるようになった。

( cf. jutememo's gist: 1350439 — Gist )

 

4. モジュールは、Template Method パターンを使っている

このようなモジュールのインクルードの動作の仕組みは、「まつもと直伝 プログラミングのオキテ 第9回:ITpro」 に、次のように書かれている。

Rubyのクラス・ライブラリでTemplate Methodパターンを最も活用している部分は,EnumerableモジュールとComparableモジュールでしょう。...

Comparableモジュールは大小比較の基本となる「<=>」メソッドを用いて,各種比較演算を提供しています。「<=>」メソッドの仕様はレシーバと引数を比較して,レシーバの方が大きければ正の整数,等しければゼロ,小さければ負の整数を返すというものです。このメソッドを基礎にして, Comparableモジュールは「==」,「>」,「>=」,「<」,「<=」,「between?」の6つの比較演算を提供しています。

Ruby で書かれたデザインパターン (Template Method) の実装例を見たい場合は、以下を参照にすると良い。

前者は、Module を Mixin して、Template Method を実現しており、後者は、サブクラス化によって実現している。ただし、Mixin とは、

一般にTemplate Methodは継承とセットで語られますが,Enumerableのようにインクルードするだけで継承関係に関わりなく任意のクラスに機能を追加できるのは魅力的です。もっとも,Rubyのインクルードは一種の(制限された)多重継承なので,何の不思議もありませんが。

( cf. まつもと直伝 プログラミングのオキテ 第9回:ITpro )

 

5. Yield はどのように呼び出され、動作しているのか?

Enumerable モジュールは、C で実装されているらしい。

Nabble - [ruby-dev:32712] Re: Enumerable can't take multiple parameters によると、

たとえばEnumerable#collectをRubyで定義する時にどのように書くか、という問題です。現在は
module Enumerable
  def collect
    result = []
    self.each{|x| result.push(yield x)}
    result
  end
end 
と等価になるように定義しています。

先ほど Group クラスで定義した each メソッドを参照しながら、 Enumerable の collect メソッドを見てみる。頭が混乱してくる。。 パタッ(o_ _)o~†

以下のように collect メソッドの呼出しをイメージしてみる。

080330-001

うーん、ややこしい (@_@;)  どこが一番想像しにくいのだろうか?順に動作を追って考えることにした。

  1. Group クラスに Mixin した collect メソッドを、ブロック付きで呼出す。
  2. Enumerable モジュールの collect メソッドの中で、 Group メソッドで定義した each メソッドを呼出す。
  3. Group クラスに定義した each メソッドでは、Person 要素を一つずつ取り出し、each メソッドに渡されたブロックの処理を進める。

例えば、先ほどの実行例で考えると、先頭の要素 `Tarou' が取り出されたとき、渡されたブロックの引数を展開すると、次のような値にが与えられているとイメージする。

{|tarou| result.push (yield tarou)}

ここでややこしいのは、yield が呼ばれていること(4)。

展開すると、こんな感じになるだろうか。

{|tarou| tarou.age + 10}

動作がわかりにくので、上記二つをまとめると、

{|tarou| result.push (tarou.age + 10)}

これを Person の要素分だけ繰り返して、 結果を 配列 result に詰め込んでいく。

 

Enumerable が関知していること

上記のように、個々の動作を追っていくと、何がなんだかわからなくなる。むしろ、Enumerable というモジュールの機能レベルで理解をしていく方がいいかもしれない。

Enumerable とは、 動詞 enumerate (列挙する、数え上げる) に由来する。「数え上げる」とは、対象がどのようなものであっても、複数の要素に対して行う行為を指す。

collect という操作であるなら、

「要素に対して、何らかの操作した結果の、集合を返す」

役割を持つと考える。要素を取り出す仕事は each が行う。

collect の中で呼出されている、each メソッドに渡されるブロック変数 x は、

「 x の中身が何なのか自分は関知しない」

ということを表わしている。その何だかよくわからない任意のものを、自分が呼出されたときに、与えられたブロックに渡し、「何らかの処理」を行わせた結果を受け取る。「何らかの処理」については、 Enumerable 自身は関知しない。ただ、「何らかの処理」の結果を受け取って、配列に詰め込むだけの役割を持つ。

 

6. 他の言語との比較

Haskell の map 関数と比較

Haskell にも、Ruby の Enumerable モジュールの collect に相当する map 関数がある。

map 関数の定義を見てみと、

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

map::(a -> b) -> [a] -> [b]
map f []     = []
map f (x:xs) = f x : map f xs

なんとシンプルな。。 (@_@;)

対象が何であれ、

「要素に対して、関数を適用する」

ということが素直に示されている。この定義を理解しておいてから、 Ruby の Enumerable モジュールの collect メソッドを見ると、何となく同じような形に見えてくる。yield x という文が、要素 x に対して関数を適用していることに等しい。

Haskell が再帰によって、要素に関数を適用するというところと、 Ruby が each によって、個々の要素を取り出し、順にブロックを適用するというところが違うけれど。

 

Java との比較

自分の場合、プログラミングは、 Java から入ったので、 Ruby のモジュールを見たときは違和感があった。Java の視点から見ると、 interface が外面だけでなく、中身も持っている感じ。Ruby はモジュールがあるから、Java よりも抽象的な表現による、コーディング促進されるのだろうか?

Java でオブジェクトを比較する方法を実装する場合、

  1. インターフェイス Comparable の compareTo を実装することによって、オブジェクト同士が比較可能にする。
  2. Arrays.sort, Collections.sort を利用してソートする。
  3. ソートするときに、Comparator を実装した匿名クラスのオブジェクトを渡せば、ソートの方法を変更することができる。これは、Ruby でブロックつきで呼出しているようなものと考えればよい。

ただし、クロージャと匿名インナークラスとの違いについては、MF Bliki: Closure に指摘されている。、

if you're a Java programmer you probably think "I could do that with an anonymous inner class", a C#er would consider a delegate.These mechanisms are similar to closures, but there are two telling differences.(...)

その二点とは、

closures can refer to variables visible at the time they were defined ...

Languages that support closures allow you to define them with very little syntax.

Ruby のブロックと Proc」へつづく…

 

連想事項

2008年3月28日金曜日

Ruby のブロック付きメソッドとイテレータ - yield の様々な使い方

1. 要素を取り出す each メソッド

Ruby の「イテレータ」は、複数の要素を持つオブジェクトが、個々の要素を取り出し、何らかの処理を適用するときに使うと便利。

一番基本となるメソッドが each 。例えば、Array クラスの場合、each メソッドで、要素をひとつづづ取り出すことができる。

[1,2,3,4,5].each do |i|
  puts i
end

これはどういう仕組みで、このように書けるのだろうか?

 

2. イテレータの役割

オブジェクト指向スクリプト言語 Ruby」(p94) では、イテレータについて、次のように述べられている。

イテレータ (Iterator) はメソッドの一種で、もともとは繰り返しの抽象化のためのものでした。

しかし、はじめから 「繰り返しの抽象化のため」 という視点で、イテレータを理解しようとすると、わかりずらい。「繰り返しの処理のため」にあるというよりは、

関数 (処理) をメソッドに渡すための仕組み

と理解しておく方がよい。

 

3. yield の意味

イテレータに関して、最初に覚えておく単語は、

yield

これは、渡された関数に処理を移すための命令。もっと端的言うなら、yield が書かれたメソッドに渡されたブロックを、実行するための手段。Ruby では、関数と言わずに 「ブロック」 と呼ぶので、以降ブロックと呼ぶ。

ちなみに、Yahoo!辞書 - yield を調べると、その中の 1 つの意味として、

3 [III[名]([副])] …を放棄する, 手放す, (…に)明け渡す((up/to ...)); …

((~ -self))(誘惑などに)身を任せる, ふける((to ...));

[V[名](as)[名]] 身を任せて(…に)なる...

They yielded (up) the city [=the city (up)] to the enemy.

町を敵の手にゆだねた

Ruby の yield に当てはめて意味を考えると、

渡されたブロックに処理を「明け渡す、身を任せる」

と言った意味として捉えておけばいいかな。

 

4. 引数の数を変えてyield を呼び出してみる

引数なしで yield を呼び出す

渡されたブロックを処理する関数 test を定義してみる。

def test
  yield
end

test{puts "hoge"}

test を呼出すと、呼出したときに渡したブロックが yield によって呼出される。

 

yield に引数を1つ渡す

yield に引数を与えると、yield が書かれたメソッドに渡されたブロックの、ブロック変数に引数が渡される。

ブロック変数とは、ブロックの先頭で

|変数|

と記述されている変数のこと。

def test2
  yield "hoge"
end

test2{|x| puts x}

 

yield に複数の引数を渡す

yield に引数を複数渡すと、それに対応したブロック変数に渡される。

def test3
  yield "hoge", 100
end

test3{|x, y| print x, y, "\n"}

 

5. block_given? メソッドで、ブロックのある・なしに対応

これまでは、メソッドに必ずブロックがある場合を試した。これを、ブロックが渡される場合と、渡されない場合に対応できるメソッドに変更する。

def test4
  if block_given?
   yield
  else
   puts "no blcock"
  end
end

test4{puts "hoge"}
test4

 

6. 要素を取り出す、イテレータとして用いる yield

yield を複数回呼び出す

yield は、渡されたブロックを実行するための手段であることが実感できた。

次は、「要素を走査するイテレータ」の使い方を、yield を用いて書く。

def test5
  for i in [1,2,3]
   yield
  end
end

test5{puts "hoge"}

for ループにより、yield が 3 回実行されている。これにより、渡したブロックが 3 回される。

 

yield を複数回呼び出すとき、引数を一つ渡す

上記を少しだけ変更する。 yield を呼出す際、for ループで使われている変数 i の値を渡す。

def test6
  for i in [1,2,3]
   yield i
  end
end

test6{|x| puts x}

これにより、先ほどと同じく、渡されたブロックが 3 回実行されるが、その度にブロック変数に渡される値が異なる。

 

yield を複数回呼び出すとき、引数を複数渡す

yield を複数回呼び出す。今度は値を複数渡してみる。

def test7
  for i in [1,2,3]
   yield i, "hoge"
  end
end

test7{|x,y| print  x, y, "\n"}

 

7. 「引数」と「ブロック」を受け取るメソッドの定義

引数を受け取るメソッドを定義する。ただし、ブロックも受け取る。

以下のメソッドでは、引数の値に応じて、ブロックの呼出しを変化させている。

def test8(val)
  for i in [1,2,3,4,5]
   yield i, "hoge"  if i > val
  end
end

test8(0){|x,y| print x, y, "\n"}
test8(2){|x,y| print x, y, "\n"}

このように、Ruby では、直接関数を渡すのではなく、「ブロック」を利用するところに特徴がある。慣れないと、素直に関数を渡せた方がシンプルでいいのになと感じる。 ^^;

Ruby のイテレータ (2) – Enumerable と Comparable モジュール」へつづく…