LoginSignup
0
1

More than 5 years have passed since last update.

pythonでgoの埋め込み(embedded)を模倣するときにやってること

Posted at

はじめに

pythonでgoの埋め込みを模倣するときにやってること、というか継承より委譲と言うやつです。

継承が辛いという話

継承が辛いという理由の1つは呼ばれたメソッドがどれによるものかがわからなくなる点です。

例えば、以下のような継承関係のあるCについて

class A:
    def f(self):
        return "f"

    def g(self):
        return "g"


class B:
    def h(self):
        return "h"

    def i(self):
        return "i"


class C(A, B):
    def m(self):
        return (self.f(), self.i())

C#m() の処理の挙動を把握するのが面倒です。もちろん通常はもうすこし名前にこだわりを持つとは思うので、ここまでひどくはならないと思いますが。

一方で委譲になっている場合はまだマシです。

class C:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def m(self):
        return (self.a.f(), self.b.i())

内部的に抱えているインスタンス変数のどのメソッドがよばれているか明示されることになるからです。

goの埋め込み

ここでgoの埋め込みです。これは委譲を半ば自動的にやってくれる機能と見る事もできます。

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}

ReadeWriterはReaderとWriterの機能を持っていますが。実質的には内部に持っている値に委譲してあげているだけです。上の例はinterfaceの例でしたがstructの方でも同様です。

type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

ここで var rw *ReadWriter となるような変数rwに対して、 rw.Read() などやると、 rw.Reader.Read() などが呼ばれます。

pythonで埋め込みの模倣

pythonに戻ります。はじめの例で継承より委譲の方が良いということを言っていましたが。とは言え完全に委譲で解決したわけではありません。AやBのメソッドをCから直接呼び出したい場合があります。また、Cを使う箇所で直接 c.a.f() などのようにしてしまうと実質利用できる値がCのインスタンスに固定されてしまいます。

そんなわけで、c.f() と呼び出しつつも内部的には c.a.f() が呼ばれて欲しいということが起きます。とは言え、都度都度必要なメソッドの呼び出しを明示的に書くのは辛いです。自動でやってくれたらうれしいですね。つまりgoの埋め込みの模倣です。

ここからが本題なのですが、pythonでgoの埋め込みを模倣したい場合には、個人的には以下の様にしています。

class C:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def m(self):
        return (self.a.f(), self.b.i())

    def __getattr__(self, name):
        return getattr(self.a, name)

これで、 c.f が自動的に c.a.f になります。これは完全にgoの埋め込みを模倣しているわけではないですし。存在しない属性が指定された場合に全てself.aに委譲されてしまいます。メタプログラミングなどを使えばもう少し明示的にあるいはもっと限定的にできるようになりますが。今のところはこれで十分だと思っています。

複数の継承元が存在する場合について

元々の例では複数の継承元が存在していました。先程の例では継承元が1つの場合にしか対応できていません。複数の場合についても対応して見ることにしましょう。注意点はgetattrの使い方です。getattrの第3引数にdefault値を指定する事ができますが。Noneではなく何らかの特殊な値を返してあげるようにしましょう。もし仮にNoneが設定されていた場合にも次の探索先を探してしまうからです(逆にNoneがあったら別の候補を探すということにしたいならそうやっても良いのですが。挙動を把握するのが難しくなるのでオススメはしません)。

missing = object()


class C:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def m(self):
        return (self.a.f(), self.b.i())

    def __getattr__(self, name):
        v = getattr(self.a, name, missing)
        if v is not missing:
            return v
        return getattr(self.b, name)

これで c.i()c.b.i() を、 c.f()c.a.f() が呼ばれる様になります。

ところで

ところで元々の例ではA,Bを複数継承した値を最後の例では複数の移譲先に対応する例を書きましたが。実際のコードではこのように複数の移譲先に対する暗黙的な移譲を行うことはオススメしません。パット見て挙動を把握するのが難しくなるからです。なのでなるべく移譲先は一つだけに限定するというようにしたほうが良いです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1