もっとコードに対して言及して記事を書きたいという話

ちょっとノルマ1が迫ってきているのでたまには自分の頭の中にあることをdumpする形でノルマを消化する。

頭の中の風景

色々な人が色々なブログとの付き合い方をしていると想う。自分自身のこのブログへの付き合い方は基本的には備忘録的な意味合いが強い感じかもしれない。つまるところ何かがあっても書き留めて置かないと忘れてしまうのでメモしようというもの。

時間の経過とともに頭の中にあったはずの何かが揮発してしまう。これを防ぎたい。あるいは、頭の中にあるものを外に出力してスッキリしたい(頭のgc)。

今までの言及方法

このブログの記事について考えてみると、何かを調べたいときにはそのサブセットを書いてみてそのサブセットに対して言及するという形を取ることが多い気がする。例えば何らかのエラーで困ったとしたらそのエラーを再現するような小さなサブセットを作ってそれを題材として記事を書く。あるいは望みの挙動を作りたいとしたら、その望みの挙動を説明するような小さな例を書いて記事を書く。

例えばこの記事などがそういう記事。

これはこれで便利なのだけれど。これでは言及できない対象がある。

実際のコードに対して言及したい

その言及できない対象の1つとして実際のコードがある。例えば使っているライブラリの実装自身について言及したいことがある。もっと言うと引用したい。gistなどでコード全体を引用するのではなく特定の部分を引用したい。

これをどうにかできないかという試みをしばらく時間をかけてやっているような気がする。

方法1 githubへのリンク

例えばissueやgistなどで特定のコードに対して言及するときはけっこう手軽にできる。linkを貼ってあげれば良い。同一のrepository内のissueではコードが展開されて貼られるのでそれもまた便利。

例えばstarletteというライブラリのwebsocketへの実装に対して言及したいときには以下の様な形でリンクを示す。

とはいえ、このはてなブログではちょっとやりづらい気がする。あと端的に言えば味気ない。

方法2 概観を掴んでもらいたい場合

(ここからは完全にpythonに限定した話をするのだけれど)

いきなりリンクを貼ってもわからないことがある。もちろん何らかの説明をした後に対応するコードへのリンクを貼るのだけれど。そのコード自体の動きではなく対象と成るオブジェクト(クラス)の雰囲気を説明したいこともある。

このようなときのためにpyinspectというライブラリを作っていて、これ経由である程度は伝えられるのかもしれないと思ったりしている。ざっくり雰囲気をつかめる何かが出力できていれば良い。

具体的に何をやるのかと言うと以下の様な形で利用した時に、自分自身の持つメソッドの呼び出し関係を階層的に表現して表示してくれる。

$ pyinspect inspect starlette.endpoints:WebSocketEndpoint

starlette.endpoints:WebSocketEndpoint <- builtins:object
    [method] __await__(self) -> Generator
        [method] dispatch(self) -> None
            [method] on_connect(self, websocket: starlette.websockets.WebSocket) -> None
            [method] on_disconnect(self, websocket: starlette.websockets.WebSocket, close_code: int) -> None
            [method] decode(self, websocket: starlette.websockets.WebSocket, message: MutableMapping[str, Any]) -> Any
            [method] on_receive(self, websocket: starlette.websockets.WebSocket, data: Any) -> None
    [method, OVERRIDE] __init__(self, scope: MutableMapping[str, Any], receive: Callable[[], Awaitable[MutableMapping[str, Any]]], send: Callable[[MutableMapping[str, Any]], Awaitable[NoneType]]) -> None

どこからも呼ばれないメソッドはトップレベルのメソッドという形、あるメソッドから呼ばれるとしたら1段ネストして表示という形。今回の例で言えば、

  • WebSocketEndpointはObjectを継承
  • __init__()__await__() がトップレベルのメソッド
  • __init__() は overrideされている
  • __await__() から他のメソッドが呼び出されている

というようなことが分かる。なんとなくの呼び出し関係を説明する時に便利な気がしている。

これも含めてエディタから利用する場合には物理的な行番号を指定して利用することができたら便利なんじゃないかとふわっと思っていたりしている(エディタは物理行の指定、CLIでは何らかのパスのような形で指定がやりやすそう)。そんなわけでLSP(Language Service Protocol)のサーバーの実装じゃないけれど、エディタと通信するような個人的なサービスを立ち上げていい感じにやりたいというのはけっこう永年思っていたりする。

蛇足 pyinspectの話

蛇足ではあるのだけれど、ファイル内の関係ではなく継承関係で一覧に観える様になるので複数のライブラリに跨った関係などを把握するのにけっこう便利でたまにコードリーディングのお供に使っていたりする(例えばjupyter-notebookでのtrailtletsのクラスを継承しているコードの理解などに役に立ったりした)。

そして調べてみたらこのブログでもpyinspectに触れる記事を書いていた。

まぁこれは蛇足的な話。

方法3 特定のファイルの特定の箇所を引用したい場合

特定のファイルの特定の箇所を直接引用したいことがある。ここが実際やりたい事ではあるのだけれど。例えば「あるライブラリ(あるモジュール)のある特定のクラスのあるメソッドの特定の行」について言及したいことがある。

例えば、先程のstarletteのjsonをloadしている部分に標準ライブラリのjsonを直接使っているので、代わりにujsonとか使えないね。。と言いたい場合など。

はじめに書いた通りにgithubのリンクを貼っても良いのだけれど。こういう感じになる。味気ない。

そしてわかりやすさを考えると、「どのクラスのどのメソッドの中のどの箇所か?」という形で表現したい(今回の場合は「WebSocketEndpointのdecode()メソッドのjson.loads()部分」という形)。

丁寧に書こうとすると手動での整形が必要になってちょっと面倒だなと思ったりしていた。例えばこういう形で整形する。

class WebSocketEndpoint:

# ...

    async def decode(self, websocket: WebSocket, message: Message) -> typing.Any:

# ...
            try:
                return json.loads(text)
            except json.decoder.JSONDecodeError:
                await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA)
                raise RuntimeError("Malformed JSON data received.")

どうしても誰かに伝えたい場合や説明したい場合、丁寧に言及したい場合には、丁寧に整形するのだけれど、元々が備忘録という関係上なるべくコストを抑えたい。有り体に言えば整形のコストを払いたくない。

この辺りを良い感じにできないかなーとここ一週間ぐらい余暇にちょっとずつ試行錯誤をしていた(次回に続く)。

とりとめもなくなってきたけれどたまにはこういう記事も良いのかもしれない。


  1. ノルマというのは1週間に1つ記事を書くという試みのこと