メソッドの後付けを python で考える

以前に、

後付け言語 post を考えてみる | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/5866

というのを考えていたのだけど、実は pytohn でそれができることをつい最近知った。

和訳 なぜPythonのメソッド引数に明示的にselfと書くのか | TRIVIAL TECHNOLOGIES 4 @ats のイクメン日記
http://coreblog.org/ats/translation-of-why-explicit-self-has-to-stay/

Raspberry Pi を触っていると Python コードに触れる機会が多いはずなのだが、Windows IoT Core を乗せたり、mono + F# という組み合わせでやっているうちに遠ざかってしまった(苦笑)ので、全然書いたことがなかった。
機械学習がやりたいわけではないけど(画像の特定ぐらいはやらないと駄目なんだが)、

Raspberry Pi Cookbook for Python Programmers [Kindle edition] by Tim Cox
https://www.amazon.co.jp/gp/product/B00JQEJE12/ref=oh_aui_d_detailpage_o01_?ie=UTF8&psc=1

という、ちょうど RPi と Python が学べそうな本があるので、買ってみた。これ、早くに駆っておくべきでしたよ。これだったら Raspbian で電子工作ができそうです。自分が結構躓いたところが丁寧に解説してありました。
RPi のほうは、手元にある Orange Pi も含めてもう少し突っ込んでやるために、Python を使えるようになろうかなと考えているところですが、その前にあの「self」って何なんだろう、に対する答えの記事にちょうど当たりました。初回にあたったのは自分的に結構ラッキーだったかなと思って、書き残しておきます。

後付けでメソッドを拡張する

ちなみに、ごく最近学び始めたばっかりなので、python2の事情は知りません。pytohn3だけを対象にしていきます。あちこちにサンプルコードがあるのですが、それが2なのか3なのかよくわからんけど、まあ、目的は PPi や組み込み Linux でロボット制御あたりに使うことなので、どっちでもいいのです。
たぶん、似たようなことは javascript の prototype でも作れるだろうけど、演算子のオーバーロードとかラムダ式の書き方とかが、自分にしっくりしてきる(というか F# に似ている)ので、python のほうが相性がよさそうです。

# 何もないAクラス
class A:
    pass
# calc メソッドを追加する
def calc(self, x):
    return x*x
A.calc = calc

a = A()
print("ans. " + str(a.calc(10)) )

# 後付けで上書きする
def calc2(self, x):
    return x*x*x
A.calc = calc2

a = A()
print("ans. " + str(a.calc(10)) )

何もメソッドやプロパティを持たない A クラスがあって、後付けでメソッドを拡張します。いわゆる拡張メソッドなのですが、最初の「def calc(self, x):」なところは、普通にグローバルな関数宣言ですよね。グローバルなクラスのメソッドのstaticメソッドとも言えます。F# なら、

let calc self x = 
    x*x

とするところです。このグローバル関数の self 引数は余分なのですが、Python の場合、後付けで A.calc メソッドを付けるときに効いてきます。

def calc(self, x):
    return x*x
A.calc = calc

C++ ならば、A クラスの calc 関数ポインタにグローバル関数を割り当てる、というイメージですよね。C++ の場合、型チェックがきついので、実際は A:calc ポインタになってしまい、グローバル関数の calc を割り当てることはできません。std::function テンプレートを使って、うまいことをやります。
C# や F# の場合も関数ポインタにして用意することもできるのですが、固定であるならば拡張メソッドを使ったほうが楽です。

type A() =
    do ()
let calc self x = 
    x*x
type A with
    member x.Calc n = calc x n 

空の A クラスを作っておいて、グローバルな calc メソッドを、A.calc メソッドから呼び出します。引数の self が無駄になっているように見えますが、実は、member x.Calc の x な部分に対応しています。つまり、F# であっても、ちょうど self/this な部分を参照できるような仕組みになるわけです。

似たようなコードを C# で書くともっと明確になります。

class A {}

class Global {
    static int Calc( A a, int x ) {
        return x*x;
    }
}

class AEx {
    static int Calc( this A a, int x ) {
        return Global.Calc( a, x )
    }
}

C# の場合、グローバルな関数を置けないので、Global クラスを作って static メソッドにしておきます。このとき、無駄な A クラスへの参照を置くわけですが、拡張メソッドを AEx.Calc として作るときに、this として A クラスのオブジェクトを置きます。つまり、明示的に this が使われていることが分かります。

こうなると、Python のメソッドは、実は C# で言うところの「拡張メソッド」にあたるのではないか?という類推ができます。

class B:
    def calc(self, x):
        return x*x

Python のクラスのメンバ関数にいちいち置かれている self は、実はグローバル関数の self 引数なのかもしれません。
そう思うと、グローバル関数と、クラスのインスタンスメソッド、クラスに後付けされるメソッドの3つの関数が同じ型を持っていることに納得がいきます。なるほど、あいまいな型のままにメソッドが呼び出されているわけではなくて、むしろ型チェックのために「self」が置かれている、と考えることができます。いや、実動作はわからないのだけど、論理的にはそう考えられるという訳です。

それらの関数が、常に関数ポインタとして働き、入れ替え可能であるという点で、もういちど、A.calc に別のメソッドを割り当てることも可能になります。

def calc2(self, x):
    return x*x*x
A.calc = calc2

実行してみると、クラスに割り当てたインスタンスメソッドがきちんと動作します。

class A:
    pass

# calc メソッドを追加する
def calc(self, x):
    return x*x
A.calc = calc

a = A()
print("ans. " + str(a.calc(10)) )

# 後付けで上書きする
def calc2(self, x):
    return x*x*x
A.calc = calc2

a2 = A()
print("ans. " + str(a2.calc(10)) )
print("ans. " + str(a.calc(10)) )

この順番で実行してみると、

ans. 100
ans. 1000
ans. 1000

のように、最初の a.calc の値と2回目の a.calc の値が異なります。同じオブジェクト(インスタンス)であっても、違う結果が出るところが面白いですよね。バグの温床になりそうですが(苦笑)。

もちろん、javascript の prototype プログラムのように、オブジェクトを対象にしてメソッドを追加することもできます。

a.calc = lambda x: x*x*x*x
print("a  ans. " + str(a.calc(10)) )
print("a2 ans. " + str(a2.calc(10)) )

こんな風にラムダ式で a オブジェクトのほうにだけ calc メソッドを上書きしてやると、a オブジェクトのほうだけ計算結果が変わります。

a  ans. 10000
a2 ans. 1000

実際は、こんな風に無造作に後付けで拡張してしまうとえらいことになってしまいそうですが、オブジェクト内の変数を弄ってクラスの振る舞いを変えるのではなく、メソッドそのものを切り替えてしまうのは面白い発想です。ダックタイピングを、クラスの利用者側から強制できるし、メソッド名から類する動作をユーザー側から決められるということでもありますね。先の例で言えば、利用するときに A クラスにあらかじめ calc メソッドがあればそれを使うし、無ければ利用者側が追加できるというイメージです。また、提供側の calc が気に入らなkれば、利用者側から書き替えてしまう。その変更は、提供側の動作にも影響を与えるわけですが、calc メソッドとしての動作が理屈にあっていれば(契約プログラミング的に)入れ替えることも自由なわけです。
利用側から内部に影響を与えるという話は、ライブラリが堅牢であることの根拠を崩すことになる(ライブラリは手をつけられない、privateな空間であるということ。または、一定の委譲ルールでしか触れないということ)のですが、実際は、利用者がライブラリを使う以前にライブラリが存在していなければならず、ライブラリの制作者未来を見通すことができない(完全にはできないので、処方箋を示すことしかできない)ので、利用者が使うときには「未知」にならざるを得ない。ならば、一定のルールでもって、ライブラリ自体に介入することを、利用者側から許してもいいのではないか?というのが「後置型」の発想のもとで、先の python のメソッドの後付けが利用できるなと思ったところです。アスペクト指向やDIよりも、もっとライブラリ内部に踏み込んでよい、という発想です。
このあたりは、もうちょっと python を弄って試してみたいところですね。

カテゴリー: 開発, F#, python パーマリンク