🍨

IceCream を AtCoder でデバッグライトとして用いてみた

2024/12/14に公開

背景

X の TL で Python の IceCream というライブラリが紹介されていて便利そうなので競技プログラミングのコードの実装中にも使ってみました。

*問題ないと思いますがコンテスト中での利用は使い方のミスも含めて自己責任でお願いします。

🍨 IceCream とは?

私は以下の記事で紹介されていたのを目にしました。

https://qiita.com/ryosuke_ohori/items/11b2ad43f1ae50f25cf5

公式のリポジトリはこちら

https://github.com/gruns/icecream

簡単に言うと高性能の print 関数です。軽く触った感じ、ちゃんとしたシステムなどでは logger を使うべきでしょうが、スクラッチレベルやそこまでちゃんとしなくていい時のデバッグライトは ic で出力するのがいいかなと思いました。

例えば以下のように print の代わりに ic を使い実装してターミナルで実行すると

from icecream import ic

def f(a, b, c, d):
    return a + b + c + d


ic()

v = ic(f(1, 2, 3, 4))

ic()

print(v)

以下のように ic() と引数を与えないときには ic() が実装されている行と実行時間が、 ic(f(1, 2, 3, 4)) というように書いた場合は ic() の引数とその値がターミナルに出力されます。
前者は if 文の then 節と else 節の両方に書いてどちらが表示されるかを調べることで期待通り実行されているかを確認できますね。
色もついていてわかりやすいですね。

インストール

pip でいれることができます。

pip install icecream

私の AtCoder での IceCream 以外のデバッグライト

ちゃんとしたシステムなどでは logger を使うべきでしょうが、スクラッチレベルやそこまでちゃんとしなくていい時のデバッグライトは ic で出力するのがいいかなと思いました。

と言いましたが、まさに私が趣味として行っている競技プログラミングは logger を使うほどではないスクラッチに該当し、 ic がちょうどよいのではないかと思いました。

その前に、そもそも私が今どのような方法でデバッグライトを行っているかまとめてみます。

1. debug 関数

以下のような debug 関数をコードの先頭で実装して(実際にはコードスニペットとして VSCode に登録して呼び出しています) 呼び出してデバッグライトに使っています。
こちらは失念して申し訳ないのですが 2021, 2022 年頃の競技プログラミングに関するアドベントカレンダーで有志の方が紹介していたものをほぼそのまま使っています。(この場を借りてお礼申し上げます)

import sys

def debug(*args, end: str = "\n") -> None:
    print(*args, end=end, file=sys.stderr)

この関数は中で print 関数を呼び出しているのがミソで基本的に print 関数とまったく同じ使い方ができます。その一方で file=sys.stderr と出力先を標準エラー出力にしています。

競技プログラミングサイトの一つである AtCoder では submit された提出ファイルを実行したときに「標準エラー出力は無視する」というルールがあるので、これにより print 同様ターミナルに表示はできますが、そのまま AtCoder に submit しても期待されている答え以外のものが出力されてしまって WA となることがありません。
(デバッグライトしてたものを間違ってコメントアウトするのを忘れて WA になった経験はあるあるなのではないでしょうか?)

print と同様に使えるので f-string の書き方もできます。

import sys

def debug(*args, end: str = "\n") -> None:
    print(*args, end=end, file=sys.stderr)

N, M = map(int, input().split())
debug(f"{N=}, {M=}")
$ python tutorial.py                 
12 14
N=12, M=14

💡 なお、「標準エラー出力は無視するというルールがある」とは書いていますが、出力する行為自体は行われるので debug で出力する量が非常に多い場合は TLE になるので注意してください。その可能性がある場合はコメントアウトするのがいいと思います。

import sys

def debug(*args, end: str = "\n") -> None:
    print(*args, end=end, file=sys.stderr)

# サーバに負荷がかかるから実際には実行しないでください
for i in range(100000000):
    debug(i)  # WA にはならないが TLE になります

# ...なんやかんや実装

コメントアウトする場合も debug で検索して一括でコメントアウトすればいいので非常に楽です。
デバッグライトとして print を使っていると、本当に出力しないといけない print もコメントアウトしてしまうことがあるので面倒です。

2. dpprint 関数

こちらは先程の debug 関数の発想を受け、 pprint を自分で標準エラー出力になるように実装したものです。

from functools import partial
from pprint import pprint
import sys

dpprint = partial(pprint, stream=sys.stderr)

pprint はネイティブライブラリの1つで2次元リストや辞書を表示するときに print よりわかりやすい状態で表示することができます。

from functools import partial
from pprint import pprint
import sys

dpprint = partial(pprint, stream=sys.stderr)

a = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]

print(a)

dpprint(a, width=30)  # 横幅を 30 に設定
$ python tutorial.py
[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
[[1, 2, 3, 4],
 [5, 6, 7, 8],
 [9, 10, 11, 12]]

width をちょこちょこいじる必要があるのが面倒な時もありますが、2次元グリッドや2次元配列で DP を実装中のデバッグライトに便利です。
こちらも標準エラー出力にしているのでそのまま submit しても AC できます。
(debug 関数同様出力が多いと TLE になるのでその場合はコメントアウトしてください。)

💡 ちなみに debugpartial を使って実装できますが、有志の方のコードのリスペクト&わかりやすさを重視してそのままにしています。partial を使う場合は以下のように実装できます。

from functools import partial
import sys

debug = partial(print, file=sys.stderr)

3. VSCode の debugger

非常に多くの繰り返しで出力が膨大になってしまいその中から特定の値の時だけの状況を知りたいときなどは VSCode の debugger 機能を使っています。
debugger 機能を使うと特定の条件のときだけ実行停止をすることができます。
例えば以下のコードで i = 5, j = 3, k = 1 の時に実行を停止しその時の各変数の値を確認するということもできます。

def calculate_score(a, b, c):
    # なんやかんや計算
    pass

N = int(input())
A = list(map(int, input().split()))
B = list(map(int, input().split()))
C = list(map(int, input().split()))

for i in range(N):
    for j in range(N):
        for k in range(N):
            a = A[i]
            b = B[j]
            c = C[k]
            calculate_score(a, b, c)

これをデバッグライトでしようとすると、例えば N = 1000 の時、全部で 1000,000,000 の出力がされてしまうので大変なことになってしまいます。

このようなケースのときは debugger を私はいつも使っています。

debugger 機能は当たり前ですが実行が停止し次へ進めるときにはマウスかキーボードをポチポチさせる必要があるのが面倒、実行が少し重いのが個人的にはデメリットかなと思います。
また、全体の値をざっと見たいときには不向きかなと思います。
反面、細かい条件の時にすべての変数を監視しながら動かしたい時などはすごい便利です。

ケース・バイ・ケースで1から3を使い分けているという感じです。

IceCream を使ってみた感想

では本題の IceCream を競技プログラミングに用いてみた感想です。
結論から言うと非常に気に入っています。先程紹介した 1. debug 関数 の代わりに ic 関数を使うという運用をしています。

実際に提出したコード例を以下に示します。

from collections import *
from itertools import *
from functools import cache, partial
from pprint import pprint
import sys
from typing import Any, Final

try:
    from icecream import ic
except ImportError:  # Graceful fallback if IceCream isn't installed.
    ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a)  # noqa


def debug(*args: Any, end: str = "\n") -> None:
    print(*args, end=end, file=sys.stderr)


dpprint = partial(pprint, stream=sys.stderr)
sys.setrecursionlimit(10**6)
MOD = 998244353

H, W, D = map(int, input().split())
S = [list(input()) for _ in range(H)]
# ic(S)


def manhattan(y1, x1, y2, x2) -> int:
    return abs(y1 - y2) + abs(x1 - x2)


def solve(h1, w1, h2, w2) -> int:
    cnt = 0
    for y in range(H):
        for x in range(W):
            if S[y][x] == "#":
                continue
            if manhattan(h1, w1, y, x) <= D or manhattan(h2, w2, y, x) <= D:
                cnt += 1
    return cnt


ans = 0
for h1 in range(H):
    for w1 in range(W):
        for h2 in range(H):
            for w2 in range(W):
                # ic(h1, w1, h2, w2)
                if S[h1][w1] == "#" or S[h2][w2] == "#":
                    continue
                ans = max(ans, solve(h1, w1, h2, w2))
                # ans = max(ans, ic(solve(h1, w1, h2, w2)))

print(ans)

https://atcoder.jp/contests/abc383/submissions/60704107

3箇所で ic を使い、今回は for 文の中で出力が多かったので最後 ic で検索してまとめてコメントアウトしました。(他の記事で紹介されていましたが ic は結構出力に処理がかかっているみたいなので出力が多いときはコメントアウトしていないと TLE になることが多そうです)

また ic 関数はデフォルトで標準エラー出力になっているため debugdpprint 関数と同じくそのまま submit しても問題ないのがありがたかったです。

デバッグライト時の表示は以下のような感じです。
solve(h1, w1, h2, h2) としないといけないのに solve(h1, w1, h1, w1) と書いてしまっていたのをバグを探していた気がします。

コンテスト中のデバッグライトをしているときはたいてい焦っているときなのでそういうときに f-string

debug(f"{h1=}, {w1=}, {h2=}, {w2=}")

と書こうとすると debug(f"{h1}"=) とか debug(f"{h1=, w1=}") みたいに構文がぐちゃぐちゃになってしまうことが多くて余計焦ることが多かったので

ic(h1, w1, h2, w2)

とだけ書けばいいのは助かることが多いです。

ちなみに本番環境つまりジャッジサーバに icecream がインストールされていないからそもそもコメントアウトしていないと ImportError になると思う方もいると思いますが

try:
    from icecream import ic
except ImportError:  # Graceful fallback if IceCream isn't installed.
    ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a)  # noqa

と記載することで icecream がインストールされていないときは擬似的な ic 関数を宣言してくれるので ImportError するのを防いでくれます。

これは公式のページで import-tricks セクションで紹介されていたものをそのまま使っています。めちゃくちゃありがたいですね。

例として同じコードを ic を実行したものを記載します。こちらも AC しています。
(実行時間も B 問題ということもあってか 30 ms ぐらいしか増えてません)

https://atcoder.jp/contests/abc383/submissions/60704129

まとめ

ic 関数やその他のデバッグライトのために使っている関数を紹介させていただきました。

もちろん、今後使い続けていくとやっぱり debug 関数がいいなとか、もっといいデバッグライトがあったということも出てくると思いますが、しばらくは ic 関数を使ってみたいと思います。

主観が含まれている部分は大いにあると思いますので人によっては Not For Me ということもあるかなと思いますが、興味持った方ぜひ一度触ってみてください。

最後に、IceCream in Other Languages セクションで他言語の場合も有志の方がその言語版の icecream を実装していることがあるみたいです。
Rust, C++ 版もありました。他の言語でも一度利用してみてはいかがでしょうか?

皆様の AtCoder の実装がより楽しくなれば幸いです 🤸‍♂️

Discussion