2008年9月7日日曜日

Ruby における演算子の再定義と、Python の演算子オーバーロード - コンテナの要素を添え字で参照するためメソッド

1. Ruby における演算子の再定義

Ruby の配列参照演算子

Ruby では、

  1. 要素を保持するオブジェクトに対して、
  2. 配列の要素を指定するように書くことができる。

これを「配列 参照 演算子」という。

たのしいRuby」を読んだときに、この書き方は、すぐに忘れると思った。

配列参照演算子とは、配列やハッシュで用いられる [i] と [i]=x のことです。これらは、それぞれ [ ] と [ ]= という名前で定義できます。

(第1版, p348)

当時、プログラミング言語は Java くらいしか知らなかったので、

「二項演算子を定義するには、演算子をメソッド名としてメソッドを定義」 (同上, p346)

できるということには驚いた。また、

def +(other)

とメソッドに書けることに、違和感さえ覚えた。

クラス/メソッドの定義 - Rubyリファレンスマニュアル によると、

演算子式において、「再定義可能な演算子」に分類された演算子の実装はメソッドなので、定義することが可能です。

演算子式 - Rubyリファレンスマニュアル には、再定義可能な演算子として、以下のものが挙げられている。

|  ^  &  <=>  ==  ===  =~  >   >=  <   <=   <<  >>
+  -  *  /    %   **   ~   +@  -@  []  []=  `

最後に、配列参照演算子も書かれている。

 

配列参照演算子の例

で定義した、Peson クラスの集合を保持する Group クラスに、配列参照演算子を定義してみる。

class Person
  include Comparable
  attr_accessor :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

class Group
  include Enumerable
  def initialize
    @persons = []
  end
  def add(person)
    @persons << person
    self
  end
  def each
    @persons.each do |person|
      yield person
    end
  end
  def size
    @persons.size
  end
  def [](idx)
    @persons[idx]
  end
  def []=(idx,val)
    @persons[idx] = val
  end
end

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

p aGroup[0].name
p aGroup[1].name

for i in 0..aGroup.size-1
  p aGroup[i].name
end

aGroup[1].name = "Hanao"
p aGroup[1].name

上記では、for ループにおいて、Group オブジェクトで管理している Person の数を取得する必要があるので、Group#size を定義しておいた。

 

配列参照演算子の書き方が、覚えにくい理由

配列参照演算子が覚えにくい理由は、普通のメソッドの書き方と全く異なるため。

普通、

def メソッド名(引数…)

が定義されていたら、

メソッド名(引数…)

により、メソッドを呼出す。

しかし、配列参照演算子では、メソッド名は、

[], []=

となる。実際にメソッドとして、定義するときは、

[](idx), []=(idx, val)

と書かなければならない。配列参照演算子の呼出しは、

[idx], [idx]=val

普通のメソッド呼出しとは、かなり違った書き方。特殊で例外的。

 

2.  Python の演算子オーバーロード

__XXXXX__() という形のメソッド

Python で、最初に違和感を感じたのは、一連の

__メソッド名__()

という形のメソッド。

  • __init__
  • __str__
  • __iter__
  • __cmp__

(cf. Python のイテレータ )

接頭・接尾が

__

であるのはいいのだけれど、それはどこで規定されているのだろうか?

例えば、 __str__ 。このメソッドを知ったのは str()  関数を使ったとき。

str 関数のドキュメントには、

str([object])

オブジェクトをうまく印字可能な形に表現したものを含む文字列を返します。文字列に対してはその文字列自体を返します。repr(object) との違いは、str(object) は常に eval() が受理できるような文字列を返そうと試みるわけではないという点です; この関数の目的は印字可能な文字列を返すところにあります。引数が与えられなかった場合、空の文字列 '' を返します。

(2.1 組み込み関数 より)

上記を読み、感じた疑問は

「では、オブジェクトに対して、どのようなメソッドを実装すればいいのか?また、それはどのオブジェクトで規定されているのか?」 (@_@;)

ということ。ドキュメントを読んでも、解説を見つけられない。

 

Ruby の inspect

例えば、Ruby において、Python の str() 関数に相当するメソッドは、

p オブジェクト

組み込み関数 - Rubyリファレンスマニュアル を読めば、

p(obj, [obj2, ...])
obj を人間に読みやすい形で出力します。以下のコードと同じです。 (Object#inspect参照)

どこを参照すれば良いか分かる。

Object - Rubyリファレンスマニュアル を参照すると、

inspect
オブジェクトを人間が読める形式に変換した文字列を返します。
組み込み関数 p は、このメソッドの結果を使用してオブジェクトを表示します。

これにより、クラスに inspect メソッドを実装すればいいことがわかる。

 

Java の toString()

同様に、Java において、Python の str() 関数に相当するメソッドは、

System.out.println(オブジェクト)

PrintStream (Java Platform SE 6) には、以下のようにドキュメントが書かれている。

public void println(Object x)
Object を出力して、行を終了します。このメソッドは、最初に String.valueOf(x) を呼び出して、出力されたオブジェクトの文字列値を取得します。次に、print(String) を呼び出してから println() を呼び出すのと同じように動作します。

次に、String (Java Platform SE 6) を読むと、

public static String valueOf(Object obj)
Object 引数の文字列表現を返します。
パラメータ:
obj - Object
戻り値:
引数が null の場合は、"null" に等しい文字列。そうでない場合は、obj.toString() の値

そして、Object (Java Platform SE 6) を読むと、

public String toString()
オブジェクトの文字列表現を返します。通常、toString メソッドはこのオブジェクトを「テキストで表現する」文字列を返します。この結果は、人間が読める簡潔で有益な情報であるべきです。すべてのサブクラスで、このメソッドをオーバーライドすることをお勧めします。

これにより、クラスに toString() を実装すれば良いことがわかる。

 

Haskell の show

Haskell で、Python の str() 関数に相当するメソッドは、

print 変数

Prelude を読むと、

 print :: Show a => a -> IO ()

型変数 a は、Show クラスのインスタンスであることが必要なのが分かる。

そこで、Show を見ると、Prelude に class Show a where と記述があり、

Conversion of values to readable Strings.

Minimal complete definition: showsPrec or show.

そして、show を見れば、

show :: a -> String

A specialised variant of showsPrec, using precedence context zero, and returning an ordinary String.

これにより、show を実装すれば良いことが分かる。

 

Python のプロトコル

Python の cmp 関数も、str 関数と同じ。ドキュメントを読んでも、何をどう実装すればいいのか分からない。

このため、

Python は、どこかに関数に応答するための密約が書かれているのだろうか?

と思った。ただし、 iter() のドキュメントは、問題なかった。

iter(o[, sentinel])

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

(太字は引用者による、2.1 組み込み関数 より)

Python では、Java のようにインターフェイスと言わずに、プロトコルと呼ぶようだ。

__XXXXX__()

というメソッドの形式は、「特殊メソッド名」と呼ぶ。以下に、いくつか例が示されている。

2.1 組み込み関数 を見ると、関数とプロトコルの関係が示されている。

  • callable(object)    : __call__()
  • iter(o, [sentinel]) : __iter__(), __getitem__()
  • reversed(seq)       : __len__(), getitem__()
  • unicode([object[, encoding [, errors]]]) : __unicode__()

しかし、問題は、プロトコルが Python という言語で、ユーザも利用できる枠組みで提供されている機能ではないこと。

 

Python の特殊メソッド

3.3 特殊メソッド名 によると、

特殊な名前をもったメソッドを定義することで、特殊な構文 (...) 特定の演算をクラスで実装することができます。これは、個々のクラスが Python 言語で提供されている演算子に対応した独自の振る舞いをできるようにするための、演算子のオーバロード (operator overloading) に対する Python のアプローチです。

特殊メソッドは、Ruby の「再定義可能な演算子」に対応する、Python のやり方ということ。しかし、Ruby のように言語として、スッキリとしていない。

 

要素へアクセスするための特殊メソッド

上記の組み込み関数の中で述べられている

__getitem__()

3.3.5 コンテナをエミュレートする によると、

__getitem__(self, key)

self[key] の値評価 (evaluation) を実現するために呼び出されます。(...)

key が不適切な型であった場合、TypeError を送出してもかまいません; (負のインデクス値に対して何らかの解釈を行った上で) key がシーケンスのインデクス集合外の値である場合、 IndexError を送出しなければなりません。 注意: for ループでは、シーケンスの終端を正しく検出できるようにするために、不正なインデクスに対して IndexError が送出されるものと期待しています。

上記ページには、その他にいくつかのメソッドが記載されているので、併せて実装してみる。

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

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

    def __cmp__(self, other):
        result = cmp(self.age, other.age)
        if result == 0:
            return cmp(self.name, other.name)
        else:
            return result
    

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

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

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

    def __getitem__(self, key):
        return self.persons[key]

    def __setitem__(self, key, value):
        self.persons[key] = value
    
    def __len__(self):
        return len(self.persons)
    
    def __delitem__(self, key):
        del self.persons[key]
        
    def __contains__(self, item):
        return item in self.persons



if __name__ == "__main__":
    group = Group().add(Person("Tarou", 21)).add(
                        Person("Hanako", 15)).add(
                        Person("Jiro", 15))

    # __iter__() に対応
    for person in group:
        print person.name

    # __getitem__() に対応
    print group[1]
    
    # __setitem__() に対応
    group[2] = Person("Saburou", 30)
    print group[2]

    # __len__() に対応
    print len(group)

    # __del__() に対応
    del group[1]
    print [str(x) for x in group]
    
    # __contains__() に対応
    print Person("Saburou", 30) in group
    print Person("Saburou", 31) in group
    print Person("Saburouu", 30) in group

 

その他

Haskell では要素を取得するのに !! を使う。