Closed4

デバッグ出力を使いやすくする「icecream」を試す

kun432kun432

恥ずかしながら知らなかった。

GitHubレポジトリ
https://github.com/gruns/icecream

IceCream — もうprint()を使ってデバッグする必要はありません

あなたはコードをデバッグするためにprint()log()を使ったことがありますか?もちろん使ったことがあるでしょう。アイスクリーム(略してic)は、printデバッグを少しだけ楽にしてくれます。

ic()print()のようなものですが、もっと便利です:

  1. 変数や式とその値を一緒に出力します。
  2. タイピングが60%速くなります。
  3. データ構造がフォーマットされ、美しく表示されます。
  4. 出力がシンタックスハイライトされます。
  5. プログラムのコンテキスト(ファイル名、行番号、親関数など)をオプションで含むことができます。

アイスクリームは十分にテストされており、寛容なライセンスのもと提供され、Python 3およびPyPy3をサポートしています。

kun432kun432

Colaboratoryで。

パッケージインストール

!pip install icecream
from icecream import ic

def foo(i):
    return i + 333

ic(foo(123))
出力
ic| foo(123): 456
456

notebookなので2重に出力されてしまっているが、なるほど。

辞書やクラスなども。

d = {'key': {1: 'one'}}
ic(d['key'][1])

class klass():
    attr = 'yep'
ic(klass.attr)
出力
ic| d['key'][1]: 'one'
ic| klass.attr: 'yep'
'yep'

よくある処理フローのステップごとにprint分を入れるようなやつ。icを使わない場合だとこう。

def first():
    return "1番目の関数を実行しました"


def second():
    return "2番目の関数を実行しました"


def third():
    return "3番目の関数を実行しました"


def foo(expression):
    print("1番目の関数を実行します")
    first()

    if expression:
        print("2番目の関数を実行します")
        second()
    else:
        print("3番目の関数を実行します")
        third()
foo(True)
出力
1番目の関数を実行します
2番目の関数を実行します
foo(False)
1番目の関数を実行します
3番目の関数を実行します

icを使うとこうかける。

def first():
    return "1番目の関数を実行しました"


def second():
    return "2番目の関数を実行しました"


def third():
    return "3番目の関数を実行しました"


def foo(expression):
    ic()
    first()

    if expression:
        ic()
        second()
    else:
        ic()
        third()
foo(True)
出力
ic| <ipython-input-19-12dd67137a08>:14 in foo() at 19:32:25.935
ic| <ipython-input-19-12dd67137a08>:18 in foo() at 19:32:25.998
foo(False)
ic| <ipython-input-19-12dd67137a08>:14 in foo() at 19:33:07.978
ic| <ipython-input-19-12dd67137a08>:21 in foo() at 19:33:08.037

icは引数を返すので、既存のコードにも組み込みやすい。例えばこういうコード。

a = 6

def half(i):
    return i / 2

b = half(a)
b
出力
3.0

ここにicを組み込む。

a = 6

def half(i):
    return i / 2

b = half(ic(a))  # ここ
b
出力
ic| a: 6
3.0

これでも良さそう

a = 6

def half(i):
    return i / 2

b = ic(half(a))  # ここ
b
出力
ic| half(a): 3.0
3.0

あー、これは便利だねえ。

ic()は標準エラー出力になるが、ic.format(*args)だと文字列として出力される。

s = 'sup'
out = ic.format(s)

print(type(out))
print(out)
出力
<class 'str'>
ic| s: 'sup'

ic.disable()ic.enable()ic()の出力の無効・有効を制御できる。

ic(1)

ic.disable()
ic(2)

ic.enable()
ic(3)
出力
ic| 1
ic| 3
kun432kun432

複数ファイルで扱う場合、呼び出し側となるルートのファイル内でinstall()しておけば、ic()がビルトインモジュールに追加sれるので、呼び出される側でもic()を直接呼び出すことができる。逆にuninstall()でこれを無効にすることもできる。

A.py
%%writefile A.py
from icecream import install

# `ic()`をすべてのファイルで利用可能にする
install()

# 別のファイルの関数を呼び出す
from B import foo

print("A.pyからB.pyのfoo()を呼びます:")
foo()
B.py
%%writefile B.py

def foo():
    x = 42
    ic(x)  # `ic()`を直接使用可能
!python A.py
出力
A.pyからB.pyのfoo()を呼びます:
ic| x: 42

icecreamがインストールされていない場合は以下のようにチェックすると良い

try:
    from icecream import ic
except ImportError:  # `icecream`がインストールされていない場合でもエラーにならない
    ic = lambda *args: None  # `ic()`が何も出力しないダミー関数になる
kun432kun432

icのデバッグ出力をカスタマイズできる

パラメータ デフォルト値 説明
prefix 'ic| ' 出力の先頭に付ける文字列または動的関数
outputFunction 標準エラー 出力をカスタム関数にリダイレクト
argToStringFunction PrettyPrint 引数の文字列化のカスタマイズ
includeContext False ファイル名、行番号、関数名を出力に含める
contextAbsPath False 絶対パスでファイル名を表示(includeContext=Trueが必要)

スタティックな文字列でプレフィックスを指定

from icecream import ic

ic.configureOutput(prefix='デバッグ -> ')
ic('テスト')
出力
デバッグ -> 'テスト'

関数に渡して動的にプレフィックスを付ける

import time
from icecream import ic

def timestamp():
    return f"{int(time.time())} |> "

ic.configureOutput(prefix=timestamp)
ic('テスト')
出力
1732133988 |> 'テスト'
テスト

outputFunctionを使うと、出力を標準エラー出力ではなく、関数に文字列として渡すことができる。loggingと組み合わせるとこんな感じ。

import logging
from icecream import ic

logging.basicConfig(level=logging.WARNING)

def log_warning(message):
    logging.warning(message)

ic.configureOutput(outputFunction=log_warning)
ic('警告メッセージ')
出力
WARNING:root:1732134141 |> '警告メッセージ'

argToStringFunctionを使うと、ic()に渡された引数をカスタムなフォーマットで文字列にできる。まず、argToStringFunctionのデフォルトはicecream.argumentToString、でこの内部でpprint.pformat()が呼ばれている。

from icecream import ic, argumentToString
from pprint import pformat

data = {"key": "value", "nested": {"inner_key": [1, 2, 3]}}

# デフォルト
ic(data)

# argumentToString
print(argumentToString(data))

# pformat
print(pformat(data))
出力
ic| data: {'key': 'value', 'nested': {'inner_key': [1, 2, 3]}}
{'key': 'value', 'nested': {'inner_key': [1, 2, 3]}}
{'key': 'value', 'nested': {'inner_key': [1, 2, 3]}}

カスタムな関数を指定してこれを書き換える。

from icecream import ic

def custom_string_format(value):
    if isinstance(value, str):
        return f"[文字列: {value} (長さ={len(value)})]"
    return str(value)

ic.configureOutput(argToStringFunction=custom_string_format)
ic(42, "テスト")
出力
WARNING:root:1732134435 |> 42, [文字列: テスト (長さ=3)]

さらにargumentToStringにはregister / unregisterメソッドがあり、特定のデータ型ごとにカスタム関数を登録・解除することができる。

numpy.ndarrayに対するフォーマット関数を登録

import numpy as np
from icecream import ic, argumentToString

# NumPy 配列に対するカスタム関数を登録
@argumentToString.register(np.ndarray)
def ndarray_to_string(array):
    return f"NumPy Array (shape={array.shape}, dtype={array.dtype})"

# テストデータ
x = np.zeros((2, 3))

# デバッグ出力
ic(x)
出力
ic| x: NumPy Array (shape=(2, 3), dtype=float64)

登録された型と関数はargumentToString.registryで確認できる。

print(argumentToString.registry)
出力
{<class 'object'>: <function argumentToString at 0x7fb546116200>, <class 'numpy.ndarray'>: <function ndarray_to_string at 0x7fb546ec5240>}

解除するとデフォルトのpprint.pformatを使用した表示に戻る。

argumentToString.unregister(np.ndarray)

ic(x)
出力
ic| x: array([[0., 0., 0.],
              [0., 0., 0.]])

argumentToStringfunctools.singledispatchを使用しているので、特定の型やクラスだけでなく、そのサブクラスにも適用される。

from icecream import ic, argumentToString

# 親クラス
class Animal:
    def __init__(self, name):
        self.name = name

# サブクラス
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

# Animal 型に対するカスタム関数を登録
@argumentToString.register(Animal)
def animal_to_string(obj):
    return f"Animal(name={obj.name})"

# データ
generic_animal = Animal("Generic")
specific_dog = Dog("Buddy", "Golden Retriever")

# デバッグ出力
ic(generic_animal)
ic(specific_dog)
出力
ic| generic_animal: Animal(name=Generic)
ic| specific_dog: Animal(name=Buddy)

サブクラス専用の関数を登録することもできる。

# Dog 型に対するカスタム関数を登録
@argumentToString.register(Dog)
def dog_to_string(obj):
    return f"Dog(name={obj.name}, breed={obj.breed})"

# 再度デバッグ出力
ic(generic_animal)  # Animal 用の関数が適用される
ic(specific_dog)    # Dog 用の関数が適用される
出力
ic| generic_animal: Animal(name=Generic)
ic| specific_dog: Dog(name=Buddy, breed=Golden Retriever)

includeContextを有効にすると、ic()の出力にファイル名、行番号、関数名が含まれるようになる。デフォルトはfalse

%%writefile example.py
from icecream import ic

# includeContext を有効化
ic.configureOutput(includeContext=True)

def foo():
    i = 42
    ic(i)

foo()
!python example.py
出力
ic| example.py:8 in foo()- i: 42

さらにcontextAbsPathを組み合わせると、ファイル名が絶対パスになる。

%%writefile example.py
from icecream import ic

# includeContext と contextAbsPath を有効化
ic.configureOutput(includeContext=True, contextAbsPath=True)

def foo():
    i = 42
    ic(i)

foo()
!python example.py
出力
ic| /content/example.py:8 in foo()- i: 42

VSCodeなどでは絶対パスの場合にリンクが付与されるので、デバッグ等に便利。

このスクラップは7時間前にクローズされました