読者です 読者をやめる 読者になる 読者になる

タオルケット体操

サツバツいんたーねっと

Pythonの表現力だって負けてへんぞみたいな感じでforwardableをRubyから移植する

こんばんわ。

最近、社内でにわかにRubyistが増えているせいで、Pythonの表現力がクソでRubyが最強みたいな不当なディスりを時々耳にします。何故人は争いの種を蒔き続けるのか、悲しいことですね。
僕はぶっちゃけどっちの言語も好きなんですけど、的外れなディスはあんま良くないとおもうのでPythonでも色々できるねんぞみたいなのを言うためにかっとなって書きました。

forwardableを選んだのは、実装が簡単そうだったのと、うまくいけば実用出来そうなのと、ググった感じあんまりネタかぶりしなさそうだったからです。
あとRuby力低いので他の便利が思いつかなかったんですよ。

レッツメタプログラミング

というわけで、Rubyに標準で入ってる便利ライブラリなforwardableを移植してみます。

これは委譲の定義を楽チンに出来ちゃう系の便利です。オブジェクト指向の基礎は継承よりも委譲なので便利ですね。
手書きで委譲なんてしてたら日が暮れてまた昇ります、Pythonでだって自動生成しましょう。

ちなみに、同じようなコンセプトのライブラリはすでにあります。以下のリンクです。
5long/forwardable · GitHub

ほぼ同じ記法を実現してますが、実装をみたところ、スタックフレームを遡ってクラス定義を取り出すみたいなダーティなことをしていたのでそっとタブを閉じました。

ということで、少し見た目はアレになりますが普通のPythonの機能だけを使って実装します。

実装

from operator import methodcaller

class Forwardable(type):
    def __init__(cls, name, bases, dct):
        super(Forwardable, cls).__init__(name, bases, dct)
        delegation_defs = [(methodname, v) for (methodname, v) in dct.iteritems() if isinstance(v, (def_delegator, def_delegators, ))]
        for methodname, delegator in delegation_defs:
            delattr(cls, methodname)
            delegator.apply_delegate_definition(cls, methodname)


class def_delegator(object):
    __slots__ = ('accesor', 'proxy', )

    def __init__(self, accesor, proxy=None):
        self.accesor = accesor
        self.proxy = proxy

    def apply_delegate_definition(self, cls, methodname):
        if self.proxy is None:
            setattr(cls, methodname, self.delegation_method_builder(methodname))
        else:
            setattr(cls, methodname, self.proxy_method_builder())

    def delegation_method_builder(self, methodname):
        accesor_name = self.accesor

        def _method(self, *args, **kw):
            delegate_target = getattr(self, accesor_name)
            accesor_method = methodcaller(methodname, *args, **kw)
            return accesor_method(delegate_target)
        return _method

    def proxy_method_builder(self):
        accesor_name = self.accesor
        methodname = self.proxy

        def _method(self, *args, **kw):
            delegate_target = getattr(self, accesor_name)
            accesor_method = methodcaller(methodname, *args, **kw)
            return accesor_method(delegate_target)
        return _method


class def_delegators(object):
    __slots__ = ('accesor', 'delegations', 'proxy', )

    def __init__(self, accesor, delegations):
        self.accesor = accesor
        self.delegations = delegations

    def apply_delegate_definition(self, cls, _=None):
        for defs in self.delegations:
            setattr(cls, defs, self.delegation_method_builder(defs))

    def delegation_method_builder(self, methodname):
        accesor_name = self.accesor
        def _method(self, *args, **kw):
            delegate_target = getattr(self, accesor_name)
            accesor_method = methodcaller(methodname, *args, **kw)
            return accesor_method(delegate_target)
        return _method

使い方

一つの場合はこんな感じです。

>>> from forwardable import Forwardable, def_delegator
>>> class Hoge(object):
...     __metaclass__ = Forwardable
...     startswith = def_delegator('aa')
...     endswith = def_delegator('aa')
...     balse = def_delegator('aa', 'replace')  # with proxy
...
...     def __init__(self):
...         self.aa = 'test hoge'

>>> h = Hoge()
>>> h.startswith('test')
True
>>> h.endswith('test')
False
>>> h.balse('test', 'is it greeeeet')
'is it greeeeet hoge'

複数同時

>>> from forwardable import Forwardable, def_delegators
>>> class Foo(object):
...     __metaclass__ = Forwardable
...     _ = def_delegators('aa', ('startswith', 'endswith', 'replace', ))
...
...     def __init__(self):
...         self.aa = 'test hoge'

>>> f = Foo()
>>> f.startswith('test')
True
>>> f.endswith('test')
False
>>> f.replace('test', 'is it greeeeet')
'is it greeeeet hoge'

実装の解説

メタクラスを使って、クラス生成時にメソッドを錬成してねじ込むというノーマルな実装になっています。
先のライブラリと比較してあまり黒いことはしていないので、複数の委譲を一気に生成する場合の記法が一つの場合と違ったりするあたりがちょっとダサいですけども、実用上は問題ないはずですきっと。

ディスクリプタ使ってなんかねじ込んだりすればもうちょっとなんとかなりそうな気もしたんですが、こんな単発ネタにいくらも時間かけてコード書くのもどうかなぁという感じなので今日はここまでですね。


僕はRubyあんま詳しくないんでアレなんですけども、Pythonも動的言語だしメタクラスあるし普通に表現力高いんですよね。推奨されていないだけで。
単に知らないのと、知っててやらないのとは大きな差がありますし、知らずにディスるのは本人も周りも誰も得しないのでやめましょうねとかなんかそういう感じです。

今回のソースは hachibeeDI/forwardable.py · GitHub に一応あげてあります。良ければ使ってみたり☆つけたりとかしてあげてください。