Essaytial Machine Learning

機械学習は随想だ

クラス継承による自然数の構成

 配列は甘え。

概要

 ノイマンによる自然数の構成は「全ては集合である」という立場から自然数を具体的に与える手続きを示したものだ。となれば「全てはオブジェクトである」を標榜する Pythonian としては抽象的なオブジェクトとその機能だけを使って自然数を構成してみたくなる。ただしZF公理系に当てはめようとすると無理が生じるのでその辺は簡単に避けつつ、後者関数を継承で実現できるようなクラスを考える。

公理的集合論からの視点

 クラスは人間種、インスタンスは個人等と説明されるように、クラスとインスタンスは集合と要素に似た構造を持つ。ではクラスを上手く定義することで公理的集合論を実現できるんじゃないかと思ってしまうが、そう簡単には行かない。公理的集合論空集合を起点としてそれを「拡大」するように集合を与えていくのに対し、プログラミングのクラスはサブクラス、インスタンスを取り出して「縮小」する方向に構成する差のためだ。

 要するにオブジェクト=要素 x が与えられた時、公理的集合論では x が属する集合を生成することができるが、プログラミングでは x のクラスが x より先に存在していないといけない。キャストで処理できないこともないが、その辺は実装と仕様が面倒なので次回以降に回す。

方針

 出来るだけ「原始的な」機能を使って実装したい*1自然数の構成には後者関数  \mathrm{suc}: n \mapsto n+1 による「隣り合う数」の表現が必要だが、最も単純に実現する方法はクラス継承だろう。つまり

  1. 適当なクラスを作り  0 とする、
  2.  0 クラスを継承して  1 を作る、
  3. 作られた  n に対してそれを継承した  n+1 を作る、

という手続きだ。

 ただしこのままでは後者の数が一意に定まらないので*2__eq__オーバーロードして等号判定を再定義する必要がある。ここでは  0, 1, \dots の個々の数自体がクラスになるのでメタクラスでそれを実現する。 n = m \Leftrightarrow \mathrm{suc}(n) = \mathrm{suc}(m) なので前者=基底クラスの比較結果を再帰的に取り出すようにすれば、 0 かどうかの判定問題に帰着する。 0 の判定も  0 の基底クラスを適当に固定しておけば変に処理を増やさなくて済む。なお、後述の大小比較を先に実装しておけば  n = m \Leftrightarrow n \leq m \land m \leq n による一般形が使えて本記事でもそうしている。

 ついでに後者関数、前者関数、大小比較、各種演算もメタクラスメソッドとして定義しておくといいかもしれない。int 型への変換関数があってもいろいろと便利だろう。どれも  0 に対する処理を記述して後者または前者を使った再帰的定義をすればいい。

実装

 実装を示す。たまに「a+ba.__add__(b) と等価」等と言われることもあるが正確には cls.__add__(a, b)cls = a.__class__)と等価になる。a がクラスの場合、a.__add__インスタンスメソッドではなくクラスメソッドとして呼ばれるので、例えば下の __le__再帰部分を return self.pre().__le__(x.pre()) とすると引数が足りないと怒られる。

class PythonianNumber(type):
    # 0 の基底クラス
    base = object
    def __new__(cls, name):
        return super().__new__(cls, name, (PythonianNumber.base, ), {})
    
    # 0 判定
    def is_zero(self):
        return self.__base__ is PythonianNumber.base
    
    # self を継承した PythonianNumber クラスインスタンスを返す後者関数
    def suc(self, name=None):
        if name is None: name = 'Suc'+self.__name__
        return super().__new__(self.__class__, name, (self, ), {})
    # 基底クラスを返す前者関数
    def pre(self):
        if self.is_zero(): raise ValueError()
        return self.__base__
    
    # 大小比較は1つ書けばあとは等号判定も含め一般形で実装できる
    def __le__(self, x):
        if self.is_zero(): return True
        if x.is_zero(): return False
        return PythonianNumber.__le__(self.pre(), x.pre())
    def __ge__(self, x):
        return PythonianNumber.__le__(x, self)
    def __lt__(self, x):
        return not PythonianNumber.__le__(x, self)
    def __gt__(self, x):
        return not PythonianNumber.__le__(self, x)
    def __eq__(self, x):
        return PythonianNumber.__le__(self, x) and PythonianNumber.__le__(x, self)
    def __ne__(self, x):
        return not PythonianNumber.__eq__(self, x)
    
    # 和積演算
    def __add__(self, x):
        if x.is_zero(): return self
        return PythonianNumber.__add__(self, x.pre()).suc()
    def __mul__(self, x):
        if x.is_zero(): return x
        return PythonianNumber.__add__(PythonianNumber.__mul__(self, x.pre()), self)
    
    # int への変換
    def __int__(self):
        if self.is_zero(): return 0
        return int(self.pre())+1

 続いて動作確認。別々にインスタンス化した  0 から生成した自然数同士でも比較・演算できる。

zero = PythonianNumber('Zero')
one = zero.suc('One')
two = one.suc('Two')
three = two.suc('Three')

rei = PythonianNumber('Rei')
ichi = rei.suc('Ichi')
ni = ichi.suc('Ni')
san = ni.suc('San')

# True False True
print(two<=san, three<=ni, three<=san)

# False True False
print(two>san, three>ni, three>san)

# False False True
print(two==san, three==ni, three==san)

# 5 5 6
print(int(two+san), int(three+ni), int(three+san))

# 6 6 9
print(int(two*san), int(three*ni), int(three*san))

*1:厳密には決めないが list 等のプリミティブ型やインスタンス変数の機能は極力使わない。

*2: 0 を継承するクラスはいくらでも作れる。