🦾

Python Notebookを速く安全に書く

2024/02/28に公開

この記事で紹介する方法について

Pythonを以下の要件で動かしたいときに便利です.

  • すぐに動かさないといけない
  • Notebookを共有しないといけない
  • 後で参照したい (スパゲッティを避ける)
  • VMは使う時だけ動けばいい

逆に以下のようなときはやらない方がいいです

  • パッケージ管理を行う
  • 長期間保守する

紹介する方法は長期間に渡って動作が保証される方法ではありません. 長期間保守するなら素直にDocker+pyenv+Poetryで構築することを推奨します.

環境編

Google Colabを直接使います. ローカルで開発してNotebookをuploadといったことはしないです. 色々理由はありますが, 要因は以下です.

  • vimが動く
  • IDEに頼らずともそこそこ開発サポートがある

実はGoogle Colabではvimが使えます. SettingsからEditorを選択してvimを選択すればNotebookをvimで編集することができます. これだけでだいぶ作業効率が高まりますね.

AIはあった方が良いのは勿論ですが, なくても大丈夫です. なぜならば定義確認とジャンプがそこそこ効くからです.

この機能は書いている時にも作動します. つまり, 軽量なJetBrainsやVSCodeの感覚でNotebookをかけます. 開発環境側のサポートが開いた瞬間からあるのはありがたいですね. ちなみにPydanticは最初から入っているのでpip install不要です.

  • 普段の環境と同じように補完が効く

  • 関数への入力値の設定もしやすい

typeguardは私がよく使うライブラリの一つです. 残念ながらtypeguardは入っていないのでpip installする必要があります.

https://pypi.org/project/typeguard/

MyPyほどの柔軟性は無いですが, 静的解析もそこそこ走ります.

Coding編

私は(他のユーザー定義関数に依存しないコードは)以下のようなコードの人まとまりを1ブロックに書くように心がけています.

from pydantic import BaseModel, StrictStr
from typing import List
from typeguard import check_type


class SayHelloDto(BaseModel):
    name: StrictStr


class SayHelloResult(BaseModel):
    message: StrictStr


def say_hello(dto: SayHelloDto) -> SayHelloResult:
    check_type(dto, SayHelloDto)

    return SayHelloResult(
        message=f"Hello {dto.name}"
    )


def test_say_hello() -> None:
    expected = SayHelloResult(message="Hello @shunsock")
    actual = say_hello(
        dto=SayHelloDto(
            name="@shunsock"
        )
    )
    assert expected == actual


test_say_hello()

特徴としては以下があります

  • 関数: globalに公開するものは関数だけにする
  • 型の確認: 型を(なるべく)安全に扱う
  • Test in Block: 関数宣言時にテストを行う

関数

Notebookを使う時に最も起こりがちなバグとして以下があります.

a = "This is global variable" # どこから呼び出されるかわからない

このように変数を公開してしまうと, 関数のなかのlocal変数だと思っていたものが実際にはglobal変数でバグが発生するということがありがちです. こうしたエラーは解決に時間がかかり開発速度が低下するのでなるべく避けた方が良いです. (一時的に使ったのなら再起動してglobalに公開した変数を消す)

型の確認

Pythonには実行時の型チェックがないので関数やクラスの型を確認しないと後々にバグの発生源となり, 作業の遅延につながります. 動的な型をなるべく安全に使用するためにPydantic Classとtypeguardを使います.

  • Pydantic: Class初期化時にValidationを実行
  • typeguard: 関数の引数の型をValidationを実行

定数は諦めてください. 一応, 以下のように書く手があります.

def shunsock_twitter_name() -> str:
    return "@shunsock"


def greet() -> None:
    print("Please follow me:", shunsock_twitter_name())


greet()

# Please follow me

Test in Block

Notebookで起こりがちな事故として関数をglobalに宣言して呼び出し元でバグが発覚するケースです. このようなケースを防ぐためにテストを書きたいです.

ただし, 通常のPython Projectではtestsディレクトリ下にテストコードを分離することができますが, Notebookの場合は1ファイルに全てを書くのでその手を使うことはできないです. そこで, 関数をglobalに宣言する(=blockの実行)のタイミングでテストが走るようにします.

そのようにしておけば, 呼び出し可能な関数はテストの要件を満たしているので比較的安全に使うことができます.

長期間の保守はしない前提なので, assertで十分かなと考えていますが, 使いたい人はライブラリを刺すのも良いかなと思います.

その他

  • Classについては, 基本的にPydanticでのデータ保存用Classだけを使います
    • 全文検索したくない
    • pytorchのようなパターンを除いて, methodが必要なClassが必要なコードをNotebookに書く必要があるか若干疑問
    • 同じ理由でabstract classも使っていないです
  • ユーザー定義関数に依存する関数はどう扱っている?
    • Notebookのtext blockを使って, 機能->Unitな関数に依存するもの->Unitな関数 といったように分離しています

終わりに

上記のやり方であればそこそこIDEのような機能の恩恵を受けつつ, バグを出しにくいコードを実験的に書き進められるので個人的におすすめです.

Pythonの開発環境は人によって違うと思うのでお気に入りのツールがある方は是非コメントで紹介していただけると嬉しいです.

Discussion