zope.interfaceを使うと、ちょっとだけ安全にクラスデコレータが使えるかも

まだこの使い方で合っているかわかんないけど。
昨日のコードにzope.interfaceを使ってみる。

したいこと(主機能と副機能の実装を分割したい)

本質的に必要な機能とそれ以外の機能の定義を分けたい。
例えば、ツリー構造のオブジェクトを定義するときに、その読みやすい出力を提供するメソッドは、そのオブジェクトに本質的に必要な機能じゃない。
これらを分割して定義したい。

分割して定義する方法は2種類

  • trait?のようなクラスを作ってmixin
  • クラスデコレータで装飾。

イメージとして、前者は概念と概念の合成で新しいオブジェクトを作るような感じ。(NewKlass = traitA + traitB)
一方で、作りたいオブジェクトにちょっとした装飾を加えるものならクラスデコレータを使うと良い気がする。(Klass' = Klass ± {α})

表にするとこんな感じ。

種別 説明 イメージ
mixin 概念と概念の合成 newKlass = traitA + traitB
クラスデコレータ オブジェクト定義に装飾を加える Klass' = Klass ± {α}

読みやすい出力を提供するメソッドを付け加えるというのは、
何かの機能を持っているもの(trait?)との合成(mixin)というわけではなく、そのクラス(定義したクラス)に対する装飾っぽい感じなのでデコレータが適切そう。

問題(デコレータでメソッドを追加するとき、前提をしらべたいんだけど。。。)

デコレータで定義する関数中でオブジェクトの状態を観測する必要があるときには、そのオブジェクトの状態が取得可能か調べられた方が良い。
(例えば、helpメッセージを返すメソッドを後に生やすなら、いろいろなそのオブジェクトの名前や概要を取得する必要がある)
もう少し言えば、デコレータで新しくメソッドを追加するときに、そのメソッドに必要な前提が満たされているか調べたくなる。

でも、クラスデコレータはオブジェクトの状態を観察することはできない。

def heyx(cls):
    # clsのオブジェクトがxという属性を持つことを仮定しているメソッドheyを定義したい。
    # 当然、xという属性を持っているかどうか調べたいわけだけれど。。。
    # 渡されるclsは定義したいクラスのオブジェクトではなく、クラス自体なので無理。
    if hasattr(cls, "x"):
        setattr(cls, "hey", lambda self : "hey: %s" %  self.x)
    else:
        print("error") #こちらが呼ばれてしまう。当たり前だけど
    return cls

@heyx
class B(object):
    def __init__(self, x):
        self.x = x

上の例は、Bにheyというメソッドを新しく追加しようとしたデコレータ。追加されるheyはself.xが取得可能なことを前提としている。
でも、まぁ、無理。

ここで、上の例のように「オブジェクトが想定した状態を取得可能か?」という発想ではなく、発想を変えて、「(オブジェクトを定義する)クラスが、想定した状態を持つ概念を実装するつもりか?」という風に考えると、クラスに対して、「状態xが必要何ですけどー?」ということを確認することができる。(何て言えば良いんだろう?)

例えば、上の例は以下のように書き換えられる。

import zope.interface as zi
class HasX(zi.Interface):
    x = zi.Attribute("""x""")

def heyx(cls):
    if HasX.implementedBy(cls): #self.x持つ予定だったよね?
        setattr(cls, "hey", lambda self : "hey: %s" %  self.x)
    else:
        print("error")
    return cls

@heyx
class B(object):
    zi.implements(HasX) #self.x持つよ!!

    def __init__(self, x):
        self.x = x

print B(10).hey()

これならクラス定義から遅延してメソッドを追加するのもあまり怖くないかも?

実装を遅延する

例えばこんな関数を作ってみれば便利なのかも?

def define_lazy_if_need_implements(icls, cls, name, value):
    """もし、実装を予定しているものの、まだ実装がされてなかったら実装する。
    この関数はクラスデコレータの中で使われることを想定している。"""
    if icls.implementedBy(cls):
        if hasattr(cls, name):
            sys.stderr.write("\t%s: %s is already defined" % (cls.__name__,  name))
        else:
            setattr(cls, name, value)
    else:
        sys.stderr.write("\t%s: %s is not defined.\n" % (cls.__name__, name))

このdefine_lazy_if_need_implementsを使うと、上のheyxデコレータで、heyが定義されていないときだけ追加することができるようになる。

def heyx(cls):
    _hey = lambda self : "hey, %s" % self.x
    define_lazy_if_need_implements(HasX, cls, "hey", _hey)
    return cls

昨日のコードを書き換えてみる。

昨日のコード(node.py)を書き換えてみる。

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

## 準備
import sys
import zope.interface as zi

class IInspect(zi.Interface):   
    def inspect():
        """ inspect """

class IHasChildren(zi.Interface):
    children = zi.Attribute("children of self")

class ITreeInspect(IInspect, IHasChildren):
    pass
    

def define_lazy_if_need_implements(icls, cls, name, value):
    if icls.implementedBy(cls):
        if hasattr(cls, name):
            sys.stderr.write("\t%s: %s is already defined" % (cls.__name__,  name))
        else:
            setattr(cls, name, value)
    else:
        sys.stderr.write("\t%s: %s is not defined.\n" % (cls.__name__, name))

## ここから, 実際

def inspectable(cls):
    def inspect(self, indent=0, out=sys.stdout):
        sys.stdout.write(" "*indent)
        sys.stdout.write(str(self))
        sys.stdout.write("\n")
            
        indent_ = indent + 2
        for child in self.children:
            child.inspect(indent_)

    define_lazy_if_need_implements(ITreeInspect, cls, "inspect", inspect)
    return cls

@inspectable
class Node(object):
    zi.implements(ITreeInspect)

    def __init__(self, k, v=None):
        self.k = k
        self.v = v
        self._children = {}

    @property
    def children(self):
        return self._children.values()

    def add(self, child):
        self._children[child.k] = child

    def __getattr__(self, k):
        return self._children.get(k)

    def __str__(self):
        return "%s: %s" % (self.k, self.v)

# エラーメッセージが表示されてinspectが定義されない例
# @inspectable
# class Foo(object):
#     pass

if __name__ == "__main__":
    x = Node("x", "x")
    x.inspect()
    xx = Node("xx", "y")
    x.add(xx)
    xy = Node("xy", "z")
    x.add(xy)
    xxx = Node("xxx", "α")
    x.xx.add(xxx)
    x.inspect()