nikkie-ftnextの日記

イベントレポートや読書メモを発信

Pythonのlen()関数はデザインパターンのAdapterパターン、なのでしょうか?(もやもやを自分の中で決着させるための記事)

はじめに

PyCon JP 2017に素晴らしい発表があります。
プレゼンテーション:len()関数がオブジェクトの長さを手にいれる仕組み | PyCon JP 2017 in TOKYO

この発表をアーカイブで見て感銘を受けましたし、Pythonの特殊メソッドに開眼するきっかけになりました。

さて、私は変更しやすいソフトウェアを書けるようになりたくて設計のインプット・アウトプットもしています。
最近だと「ちょうぜつ本 読書ログ」シリーズですね。
ちょうぜつ本でAdapterパターンを学んだことで、上の発表に出てきた「len()関数はAdapter Pattern」という点だけどうにも飲み込めなくなってしまいました。
3連休のあたりはずっと考えていて結構苦しかったのですが、現時点の理解をアウトプットして自分の中で区切りをつけます

目次

PyCon JP 2017 「len()関数がオブジェクトの長さを手にいれる仕組み」

素晴らしいのでみんな見て!

SlideShareは下スクロールで全スライドが見える(広告も挟まってる)ように変わりましたね)

slide=12が「len()関数はAdapter Pattern」のスライドです。
https://www.slideshare.net/shimizukawa/how-does-python-get-the-length-with-the-len-function/12
アーカイブは5:00過ぎです。

最初に聞いたときは「そうか、len()関数はAdapterなのかー」と疑問に思うことなく飲み込みました。
むしろ、「こんなところでデザインパターン使われているんだ。Pythonすごい!」と鮮明に記憶に焼き付きました。
ところが、Adapterについて知ってしまったばかりに、「len()関数がAdapter」というのが納得できなくなってしまったのです。

現時点の考え:2つのインターフェースが合わないときのアダプタではないと思う

  • デザインパターンのAdapterパターンは、2つのインターフェースが合わないときに間に入って変換するもの
    • 2つのインターフェースはどちらも変えなくてよい
    • Adapterが、ACアダプタのように間に挟まるため
  • len()関数は引数のオブジェクトの__len__()を呼び出す
    • len()関数が関わるインターフェースは、特殊メソッド__len__()1つだけと私は捉えている
  • インターフェースは1つだけなので、2つのインターフェースの間を埋めるAdapterパターンは持ち出せないように思われる
    • なので、いまの私は「len()関数はAdapter Pattern」という主張には同意しません
    • この点だけ解釈不一致ですが、PyCon JP 2017のこの発表は素晴らしいものです(私が重箱の隅をつついています)

Adapterパターン:2つのインターフェースが合わないときに変換して解決

結城先生のインタビューより

インタフェース(API)があわないとき、それをうまく変換してやって適合するようにしてあげる存在がいればいいんです。 それを一般に「アダプター」と呼びます。ACアダプターと同じですね。

ちょうぜつ本の説明と合わせて、私の中でのAdapterパターンはACアダプタです1
直流と交流で2つのインターフェースが合わないので、アダプタを間に入れて変換します。
ACアダプタは交流を直流に変換するものです2

『実践 Python 3』という本でも、デザインパターンは扱われています。

あるクラスが、互換性のないインタフェースを持つ別のクラスを利用したい場合、そのどちらのクラスも変更することなしに利用する。(2.1より)

まさにACアダプタですね。
2つのインターフェースを変えるのではなく、ACアダプタが間に入ります。

len()関数は特殊メソッド__len__()を呼び出す

(今回言及しているPyCon JP 2017の発表の説明がおすすめなのですが、)Pythonの組み込み関数len()は、引数で渡されたオブジェクトの__len__()を呼び出します。
https://docs.python.org/ja/3/reference/datamodel.html#object.__len__

Called to implement the built-in function len().

これは特殊メソッドという仕組みです。
先日の記事でも取り上げました。

  • 長さを尋ねる操作は、+演算子やfor文と同列の上位の操作
  • 上位の操作は特殊メソッドで提供
  • 特殊メソッドの命名規則はアンダースコア2つで挟む

__len__ に対するインタフェースは len() という組み込み関数になっている。(先日の記事中で参照した稲田さんのブログより)

飲み込めていないところ:「2つ」のインターフェースの変換

「len()関数はAdapter Pattern」という説明で飲み込めずにいるのは、Adapterが間に入る2つのインターフェースがあるのだろうかという点です。

  • len()関数は引数のオブジェクトの__len__()を呼び出す
    • 呼び出した後、負値であれば例外送出などもするが、長さを尋ねる処理は引数の__len__()に委譲
  • インターフェースは特殊メソッド__len__()1つだけではないか

直流と交流のようにインターフェースが2つあるのではなく、インターフェースは1つだけのように思うのです。
なので、2つのインターフェースを変えずに変換のために挟まるAdapterの出番がないように思われます。

Adapterパターンをlen()関数と一緒に使う例

こういうケースだったらAdapterパターンだよねと言えそうな例です。
ただしlen()関数がAdapterパターンというわけではありません。

まず、サードパーティの素晴らしいPythonライブラリawesomeがあるとします。
非常に革新的だったので、使っていきたいです。

awesomeの中心となるAwesomeCoreContainerクラスには、長さを返すメソッドlength()が実装されていました。
awesomeの開発者の方はPythonを深くまでは知らなかったようで(その分他の言語の造詣が深かったのでしょう)、特殊メソッド__len__()は実装されていません。

このAwesomeCoreContainerを手元のコードでlen()関数と一緒に使いたいです。
len()関数は__len__()を要求しますが、AwesomeCoreContainerにはlength()しかありません。
交流と直流のように、2つのインターフェースが合わない状況ですね。

ここにAdapterパターンを適用したクラスを導入します。

class SizedAwesomeCoreContainerAdapter:
    def __init__(self, target: AwesomeCoreContainer) -> None:
        self.target = target

    def __len__(self) -> int:
        return self.target.length()

このAdapterを使えば、len()関数もAwesomeCoreContainerクラスも変えずに、インターフェースを一致させてlen()関数で長さを返せます。

len(SizedAwesomeCoreContainerAdapter(AwesomeCoreContainer(...)))

繰り返しますが、これは2つのインターフェースが合わない状態をAdapterのクラスを作って解決したもので、len()関数がAdapterパターンと示したものではありません。

終わりに

かつて聞いた「len()関数はAdapter Pattern」、デザインパターンを学んだことで飲み込めなくなってしまい、いったんアウトプットして自分の中では決着させました。

私はまだ見つけられていないのですが、Guido氏や多数のPython core開発者が「len()関数をはじめとする特殊メソッドの機構はAdapterパターン」という見解を表明していたら、この記事は反証されます。


  1. AC=alternating current(交流)、DC=direct current(直流)
  2. ref: ACアダプターとは? | 【ユニファイブ】ACアダプター&スイッチング電源メーカー