nikkie-ftnextの日記

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

Python で書かれたライブラリ各位に告ぐ。組み込み関数 print の呼び出しは、決してロギングではありません

はじめに

七尾百合子さん、お誕生日 187日目 おめでとうございます! nikkieです。

スター数が多いあるライブラリを使う中で徐々に実装を見ていったところ、ロギングの実装に大きな伸びしろがあったという話です。

目次

『自走プログラマー』より

72:ログにはprintでなくloggerを使う

環境によって切り替えができない点が不便です。

ロギングを使えば、表示をやめたり、ファイルに出力したり、ログを残した日時を残したりできます。

この記事で伝えたいことは、『自走プログラマー』が言っているこのことに尽きます。
ロギングを print で実装すると(その瞬間はやりたいことが楽に実現できたと感じるかもしれませんが)、ログレベルによって出力しないように変えたり、ハンドラで出力先を変えたりといったことができず、かえって不便です。

標準ライブラリ logging のドキュメントより

「基本 logging チュートリアル」の「logging を使うとき」より

print()が最適なタスクは

コマンドラインスクリプトやプログラムで普通に使う、コンソール出力の表示

対してprint()ではなくロガーlogger.info()logger.debug()が最適なのは

プログラムの通常の操作中に発生したイベントの報告 (例えば、状態の監視や障害の分析)

その理由は(『自走プログラマー』でも見たように)ロガーを使えば出力有無を変えたり、日時を残したりできるからですね

私の理解

  • プログラムの機能としてのコンソール出力の表示であればprint()
  • プログラム実行中のイベントの記録には、ロガーを使う

例:crawl4ai

50k超えのOSS crawl4ai1
https://github.com/unclecode/crawl4ai

Open-source LLM Friendly Web Crawler & Scraper.

これを使っていた時期が少しあったのですが、ソフトウェアエンジニアリングの方向性の違いを痛感しました2
私の視点で実装がまずいと感じたのはロギング。
print() で実装してしまっています

crawl4ai は PyPI でのリリースに対応したタグを GitHub リポジトリに残せていない3のでバージョンは近似的な指定になりますが、私が使っていた頃の実装がこちら

AsyncLoggerinfo()error()のような、ログレベルに応じたメソッドを(一見)持っています。
ロギングの実装は_log()に共通化されているのですが、
https://github.com/unclecode/crawl4ai/blob/v0.4.24/crawl4ai/async_logger.py#L147

class AsyncLogger:
    def _log(
        self,
        level: LogLevel,
        message: str,
        # 省略
    ):
        # 省略
        if self.verbose or kwargs.get("force_verbose", False):
            print(log_line)
        # 省略

組み込み関数print()による実装です。
colorama を使っており、Webクロールの過程がカラフルに出力されます。
しかし、これをオフにすることはできません4

crawl4ai のこの print する実装で辛かったのが、APIに組み込んでDockerコンテナで動かしたときに、なんとこの箇所で無限の再帰エラーが発生したことです。
スタックトレースからは、print を重ねる中で(N-1回目までは問題なくともN回目で)突如例外送出という様子でした。
ロギング(この箇所の print)をオフにできたらどれだけよかったでしょう

この事象は colorama の使い方に伸びしろがありました。

crawl4ai はなにか問題が起こったときに泥臭く解決するのではなくガラッと作り変えて対処するというスタンス5で、現在は rich に乗り換えています。
https://github.com/unclecode/crawl4ai/blob/v0.7.4/crawl4ai/async_logger.py#L227

        if self.verbose or kwargs.get("force_verbose", False):
            self.console.print(log_line)

ただ rich はコンソールアプリケーション向けのライブラリであり、この使い方はロギング向け6ではないように思われます。
https://rich.readthedocs.io/en/stable/console.html#printing
表示をやめたり、ログをファイルに出力したり、日時と一緒に残したりすることは、ロギングほど柔軟にはできないでしょう。

終わりに

print() は、決してロギングではありません。
この主張について、全てのPythonライブラリが「当然じゃん」と返す世界であってほしいものです

P.S. 私の体験した範囲から、 crawl4ai は他の開発者にオススメできないですね


  1. スター数とコードの品質は相関しない例の1つかなと思います
  2. あまりに伸びしろが大きすぎますし、作者や他のユーザはこのソフトウェアが抱えた問題と認識していないようです。他にもやりたいことがたくさんある私には、わざわざプルリクエストを送って孤軍奮闘する気はないです
  3. 問題提起 & 私がやっている方法を共有しましたが、作者はスルーです🤷
  4. 標準ライブラリ logging を使った実装であれば、レベルの指定やハンドラの指定といった方法で出力をオフにできます
  5. ここが一番方向性が合わないです。一発逆転を狙うんじゃなくて、目の前の問題を1つ1つ解き続けるのが、私の思うエンジニアリングです
  6. console.logが見つかりました(深く見れてはいないです) https://rich.readthedocs.io/en/stable/console.html#logging