Hatena::Groupbugrammer

蟲!虫!蟲!

Esehara Profile Site (by Heroku) / Github / bookable.jp (My Service)
過去の記事一覧はこちら

なにかあったら「えせはら あっと Gmail」まで送って頂ければ幸いです。
株式会社マリーチでは、Pythonやdjango、また自然言語処理を使ったお仕事を探しています

 | 

2013-06-25

[] DCIもどきみたいなものをPythonで実装してみる 21:51

 たまには、サービス改善はお休みして、プログラミングネタを。

 一時期、ScalaからRuby界隈で、「DCI」について議論されていた印象があります。で、自分がよく使う言語はPythonなのですが、Pythonでは、あんまりDCI的なアプローチを試験的に実装してみよう、という試みは余り見なかったように思われます。

 DCIについては、自分の下手な解説より、下のドキュメントを読んだほうが、正確な理解が得られるでしょう。

DCIアーキテクチャ - Trygve Reenskaug and James O. Coplien - Digital Romanticism

 自分の(たぶん間違っているであろう)理解によれば、何かしらの「オブジェクト」の振舞を「状況」によって、「役割」を与えることで決定しよう、というところにあると思っている。また、「そのオブジェクトが必要であるが、しかしそれがどのような役割を持ちうるかということが決定しずらいとき」にも有効なのかな、と思うけれど、その辺は詳しくないので、識者に頼みたいところ。

 また、現実的な実務としては、オブジェクトに対して「ありうるメソッド」を組み込んだり、継承で深くしたりするよりも、「役割」という単位で分割したほうが、ソースの見通しが良くなるというメリットも存在するとも考えれるようです。

 そういう感じで、自分はDCIを理解しているわけですが、今回の目的は、DCIを正確に理解することではなく、とりあえず上記の「ゆるふわな」理解を元に、ではPythonで実装するとするならば、どのようになるか、ということを考えていきたいと思います。

その前に具体的なイメージを

 比喩というのは、往々にして間違った理解を生みやすいのですが、とりあえず、今から実装されるストーリを考えます。

 目の前に二人の村人がいます。この村人は、とりあえずいることはわかっているのですが、その人たちがどんな人かはわかりません。しかし、その中の一人が風呂敷を広げて商売を始めました。彼は「商人」として振舞い始めたわけですね。もう一人が商人として振る舞ったわけですから、もう一人はお客さんとして、財布を見て、いくらくらいなら買えるのか確かめる、といった感じです。

まずベースになるclassを定義する

 そこで、最初にDCI的な機能をもつベースのclassを定義してみましょう。

# -*- coding: utf-8 -*-
import re


class MethodProvider(object):

    not_use_method = re.compile('^_')

    @classmethod
    def bind(cls, target):
        instance = cls()
        use_methods = instance._find_method()
        print use_methods
        for method in use_methods:
            setattr(
                target,
                method,
                getattr(instance, method))

    def _find_method(cls):
        return_method = []
        method_list = dir(cls)
        for method in method_list:
            if cls.not_use_method.match(method) is None:
                return_method.append(method)
        return return_method

 個人的には、かなり「黒いコード」だとは思うのですが、解説していくと、targetが「何らかのメソッド組み込みたい対象のインスタンス」であり、そしてそのクラスの属性/メソッド一覧に関しては、dir関数で取れるので、そこから、Python紳士協定であるところの先頭アンダーバープライベートメソッドをのぞき、あとは、それらをsetattrとgetattrでとにかく組み込んでいくという形になります。setattrとgetattrを使っているのは、文字列を使ってメソッドを引っ張って来れるからですね :)。

 さて、このclass継承した形で、次のような役割を与えるclassを作ってみましょう。

class Trader(MethodProvider):

    def sell(self):
        for goods in self.has_goods:
            print goods[0], goods[1]


class Visiter(MethodProvider):

    def buy(self):
        print "i have %d money." % self.has_money

 さらに、これらの役割が与えられる「人間」を定義してみましょう。

class Person(object):

    def __init__(self):
        self.has_goods = [
            (u'ひのきのぼう', u'10G'),
            (u'やくそう', u'5G')]
        self.has_money = 100

 では、実際にPersonに対してTraderの役割を与える関数も定義してみましょう。

def test():
    person = Person()
    Trader.bind(person)
    person.sell()

 これらを組み合わせたものは、gistにアップしてあるので、実際に実行してみるといいでしょう。

https://gist.github.com/esehara/5857997#file-dci_test_error-py

 しかし、これを実行してみるとエラーが起きてしまいます。

Traceback (most recent call last):
  File "test_dci_error.py", line 57, in <module>
    test()
  File "test_dci_error.py", line 54, in test
    person.sell()
  File "test_dci_error.py", line 32, in sell
    for goods in self.has_goods:
AttributeError: 'Trader' object has no attribute 'has_goods'

 これはいったいどういうことなのでしょうか。

他のインスタンスから引っ張ってきたメソッドは、引っ張ってきた元のコンテキストを保持する

 これは一つの罠(というより多分仕様なのですが)、他のインスタンスからメソッドを引っ張ってきても、selfのコンテキストは変わりません。つまり、この場合のselfはMethodProviderを参照にしているわけです。証拠に、下のように改造してみましょう。

class MethodProvider(object):
    not_use_method = re.compile('^_')
    has_goods = (
        (u'てつのけん', '500G'),
        (u'はがねのけん', '1000G'))
    @classmethod
    def bind(cls, target):
        instance = cls()
        ....

 has_goodsをMethodProviderに追加するとどうなるか。

https://gist.github.com/esehara/5857997#file-dci_badfix-py

$ python test_dci_badfix.py 
てつのけん 500G
はがねのけん 1000G

 確かに、selfがMethodProviderのほうに向いています。これはよろしくない。我々の目的は、メソッドをはやして、あたかも「Person」のメソッドのように取り扱うことです。つまり、selfの参照先はMethodProviderではなく、Personであるべきなのです。

解決

 これで、どうしたものかと考えていたら、d:id:nishiohirokazu さんがチャットで解決方法を教えてくれました(Thanks!!)。要するに下のようにするといいとのことです。

class MethodProvider(object):

    not_use_method = re.compile('^_')

    @classmethod
    def bind(cls, target):
        instance = cls()
        use_methods = instance._find_method()
        for method in use_methods:
            setattr(
                target,
                method,
                getattr(instance, method).im_func.__get__(target))

    def _find_method(cls):
        return_method = []
        not_use_method = ['bind', 'not_use_method']
        method_list = dir(cls)
        for method in method_list:
            if (not method in not_use_method and
                    cls.not_use_method.match(method) is None):
                return_method.append(method)
        return return_method

 どうやら、このim_funcという属性がポイントになるようです。どういうことか。そこで、あえてテストコードを下のように書いてみます。

# -*- coding: utf-8 -*-

class TestClass(object):

    def test(self):
        pass

    def func_print(self):
        print self.test
        print self.test.im_func

if __name__ == "__main__":
    test = TestClass()
    test.func_print()

 この結果として得られる出力が下の通りになります。

$ python im_func_test.py
<bound method TestClass.test of <__main__.TestClass object at 0x7f46af202e50>>
<function test at 0x7f46af206a28>

 つまり、self.testの場合は、それがどのクラスのメソッドとして束縛されているのか、というのを示すのに対して、im_funcを使うと、そのメソッド関数として取り出すことが可能になるっぽいです。(ちなみに、これはPython2.x系の実装らしく、Python3になると__func__になるとかならないとか)

 さて、メソッド関数として取り出せたのはわかったのですが、じゃあどうやってそれを他のクラスに埋め込むのか。そこで、使われるのが__get__メソッドのようです。上記のコードを次のように改造してみましょう。

# -*- coding: utf-8 -*-


class BoundTest(object):
    pass


class TestClass(object):

    def __init__(self):
        self.bound_test = BoundTest()
        self.bound_test.test = self.test.im_func.__get__(self.bound_test)
    def test(self):
        pass

    def func_print(self):
        print self.test
        print self.test.im_func
        print self.bound_test.test

if __name__ == "__main__":
    test = TestClass()
    test.func_print()

 そうすると、下のような出力が得られます。

$ python im_func_test.py
<bound method TestClass.test of <__main__.TestClass object at 0x7fdcd2a65e90>>
<function test at 0x7fdcd2a69b18>
<bound method ?.test of <__main__.BoundTest object at 0x7fdcd2a65f10>>

 というわけで、最終的なコードは下のようになります。

https://gist.github.com/esehara/5857997#file-dci_goodfix-py

 これを実行すると、無事、下のような出力が得られます。

$ python test_dci_goodfix.py
[Trader]
ひのきのぼう 10G
やくそう 5G

[Visitor]
i have 100 money.

 こうして、クラスとは別に、メソッドのselfの参照を、個別のインスタンスへと向け直すことが出来ました。

まとめ

 正直、このようにコードを書いてみましたが、いまいちDCIのメリットがどのようなものなのか、あるいはDCIが目指しているものとは何なのか、ということについては、まだまだ曖昧なところではありますが、このアプローチは便利だなあと思うところもある一方で、ちょっと「黒いかな?」という気持ちも拭えません。とはいえ、便利そうな雰囲気はあるので、機会があれば使ってみたいなと思うところです。

謝辞

 この方法を試している最中に、下の方々から助言を頂きました。改めてお礼を言いたいと思います。

 |