Pythonでバウンドインナークラスを使う (Python Advent Calendar 2013 最終日)

Python Advent Calendar 2013 25日目のブログ記事です。

Python Advent Calendarでは例年最終日にはゲストをお迎えして記事を書いてもらっています。今年は Python 3.4 のリリースマネージャーである Larry Hastings さんに書いていただきました。

Larry, thank you very much for your posting to this Python Advent Calendar!

ここでは日本語訳を書いていきたいと思います!

Pythonでバウンドインナークラスを使う

Python において、関数をクラス中に定義した際にはある魔法が起こります。 クラスのインスタンスを通して関数にアクセスした場合単なる関数が得られるわけではありません。 代わりに新しいオブジェクト「メソッド (method)」と呼ばれるものが返ります。 概念的にはこの関数はインスタンスに「バウンド/束縛 (bound)」されています。 実際にメソッドを呼んだ際にはそのインスタンスが関数の第一引数に自動的に 渡されます。別の位置引数 (potitional argument)は1つ後ろに追いやられます。

しかしこの魔法は関数にしか有効ではありません。クラスについて考えてみましょう。 Pythonでは、あるクラス C を別のクラス D 内に定義した際に、この C を 「インナークラス (inner class)」と呼びます (「ネステッドクラス (nested class)」かもしれません)。 D のインスタンスを通して C にアクセスした場合、 さきほどのような魔法はおこらず、単に C が返されます。 C を呼び出したとしても C.__init__ への引数は変わりません。

インナークラスが Python で使われることは稀です。なぜでしょうか。 それをする実用的な利点が無いからです。 Python のスコープルールによって面倒が生じます (後述)。

それでも私は Python でインナークラスをよく使います。 概念的に他のクラス内にあるべきクラスの場合です。 大抵の場合インナークラスはアウタークラスに “バウンド” されて欲しいものです。 しかしこのように、手動でする必要があります:

class D:
    class C:
        def __init__(self, outer):
            self.outer = outer
d = D()
c = d.C(outer)

関数がするように、インナークラスも “バウンド” されると良さそうです。 アウタークラスが自動的にインナークラスの __init__ に渡されると良さそうです。

でもこれって Python でできることなんでしょうか? 数年前、 Stack Overflow で聞いてみました:

返答には驚きました。それが不可能というだけでなく、意味がないと言うからです。 その後に Alex Martelli が、それは可能だと教えてくれました。 その解法には心底驚かされましたよ!彼の解法はクラスデコレーターを使って、 クラスを「ディスクリプター (descriptor)」にするというものでした。 これは関数がメソッドになるのと全く同じメカニズムです。実に素晴らしい!

Alexの許可を得て、私はこれを「レシピ」として、 Python Cookbookに投稿しました:

このアプローチには何度も手直しをしました。今ではちゃんと動作しますよ! そして...そうなんです、このレシピは Python 2 と 3 両方で変更なしに動作します。

このバウンドインナークラス (bound inner class) のことは本当に気に入っていますし、 可能であればどこでも使います。バウンドインナークラスを人に教えたときの 一番よくあるフィードバックは 「それのユースケースは?」ですが、 私の回答はこうです「メソッド呼び出しのユースケースは?」 私が思うに、この2つは全く同じ質問です。そしてバウンドインナークラスは メソッドと同じくらい便利なものであるとオススメしておきます。 もしかしたらそれ以上かもしれません!

もちろん、クラスは関数よりも複雑なものです。つまりはバウンドインナークラスは メソッドよりも複雑です。例えば継承と2レベル以上のネストとなるとちょっと ややこしいことになります。 こんな場合でもバウンドインナークラスが実用的で、理解しやくなるよう 考えてみました。どう動作するかを少し理解して、いくつか単純なルールに従う 必要があります。これについては、上記の「レシピ」にすべて記載されています。

ぜひ皆さんのコードでもバウンドインナークラスを使ってみてください!

なぜインナークラスが不便になり得るかを疑問に思ってるかもしれませんね。 以下について考えてみてください:

class D:
    value = 5
    class C:
        value2 = value * 5

このコードは動作しません、 value について NameError を送出します。 C のコード中では value が見えないのです。 D のどのメンバーも見えません。 C が見えるすべてのメンバーはグローバルとビルドインです。 (残念ながら nonlocal キーワードもここでは助けになりません。 これはネストした関数でのみ有効で、クラス本体はネストした関数のようには 振る舞いません)

ここで、コードをこんな感じに変えてみましょう:

class D:
    value = 5
    class C:
        value2 = D.value * 5

悲しいことに、これもまた動作しません。 D はまだ存在しないからです。技術的には、 D は “まだバウンドされていません”。 このコードは D についての NameError を送出します。

唯一動作するのは、ルックアップをランタイムで行うことです:

class D:
    value = 5
    class C:
        def __init__(self):
            self.value2 = D.value * 5

言い換えると、コンパイルタイムにおいてネストしたクラスは アウタークラスのどのメンバーにもアクセスできないということです。

追伸、クリスマスを楽しむ日本の読者の皆様へ。 私が皆さんに教えてあげたいのは、実際にはアメリカ人は KFC をクリスマスに食べないということです!これは日本の KFC が創りだしたマーケティング上のギミックなのです。アメリカ人は家族でのディナーを盛大に行うものですが、伝統的なメインコースというものはありません。

以上です。

Larryさん、ありがとうございました!

(なおフッターにあるCC-BYの表記はこの記事においては有効ではありません。 The above expression about CC-BY is not available at this entry.)