【競プロ】Pythonのデバッグログをいい感じにするテンプレート

## はじめに

みなさん、このような経験はありませんか？

- print("cnt", cnt) みたいにログ書くのつらい。変数名勝手に出て
- コード提出時に各所に散らばったログ出力を消して回るのつらい。勝手に消えて
- ログ消し忘れてWA出た(´;ω;｀)

これらの問題を解決するテンプレートを紹介します。みなさんの競プロハッピーライフの一助になればうれしいです🐾

## このテンプレートでできること

- **関数名**, **行番号**, **変数名**, **値**を手間なくログとして出力
    - 見通しが良くなりデバッグが快適！
- コード提出時にログ出力を自動的に**OFF**
  - ログを消す手間を削減！
  - ログの消し忘れによるWAを回避！
  - ログ出力による処理速度低下を回避！

結果のみ利用したい方は以下のコードを参考にどうぞ。

<details><summary>テンプレート</summary>

```python
import sys

def get_logger(argv: list):
    """コマンドライン引数で d が与えられた場合、icを返す"""

    if len(argv) < 2 or argv[1] != "d":
        return lambda *args: None

    from icecream import ic

    def f(output: str):
        p = output.split(" ")
        lino, module, body = p[0].split(":")[-1], p[2][:-1], " ".join(p[3:])
        print(f"{module}:{lino}| {body}")

    ic.configureOutput(prefix="", outputFunction=f, includeContext=True)
    return ic

ic = get_logger(sys.argv)

def solve():
    a = 10
    ic(a * a)  # 出力 solve():24| a * a: 100

solve()
```

</details>

## 関数名、行番号、変数名付きのログを出力する

### 変数名付きのログを出力する

[IceCream](https://github.com/gruns/icecream)というロギングライブラリを使用します。IceCreamによって変数名付きのログを出力できるようになります。

```python
from icecream import ic

def solve():
    i = 10
    ic(i * i)   # ←ここでログを出力

solve()
```

出力

```txt
ic| i * i: 100
```

変数名（式）付きでログが出力できました。便利！！

人力で同じことをする場合、 `print(f"i * i: {i * i}")` のように書く必要があり、かなりストレスフルでした。

:::note warn
追記
[コメント](https://qiita.com/atilol/items/54fdded480396b18e0f4#comment-ec70eaad080b5c09d294)で教えていただいたのですが、Python3.8からはf-stringを使用して `print(f"{i * i = }")` のように書くことで変数名や式を出力できたようです。こちらも便利ですね。
:::

競プロサイトの実行環境にIceCreamはインストールされてないじゃん！RuntimeErrorになるが？と思う方もいると思います。
後ほど説明しますが、IceCreamはローカルでの実行時のみ動作するよう実装するので、RuntimeErrorの心配はいりません。
ローカルには `pip install icecream` 等でインストールが必要です。

### 関数名、行番号付きのログを出力する

`ic.configureOutput` を使用して、ログの出力フォーマットをカスタマイズします。  
私の場合、**関数名:行番号| 変数** の形式で出力したかったので以下のようにしました。

```python
from icecream import ic

def f(output: str): # たぶんもっときれいに書ける。。
    p = output.split(" ")
    lino, module, body = p[0].split(":")[-1], p[2][:-1], " ".join(p[3:])
    print(f"{module}:{lino}| {body}")

ic.configureOutput(prefix="", outputFunction=f, includeContext=True)

def solve():
    i = 10
    ic(i * i)   # ←ここでログを出力
solve()
```

出力

```txt
solve():11| i * i: 100
```

関数名と行番号が表示できました！！
`ic(i * i)` の記述だけでこの出力をしてくれるのうれしすぎないですか！！？？

また、複数の引数を指定することなどもできます。詳細は公式ドキュメントをご覧ください

```python
略
def solve():
    i = 10
    ic(i * i, i**2)   # 出力 solve():11| i * i: 100, i**2: 100
```

## 提出時にログ出力を自動的にOFFにする

提出時にログが残っていると `WA` になるため競プロerにとっては死活問題です！

そこで、特定のコマンドライン引数を指定した場合のみ、デバッグログを出力するようにします。これにより、ローカル実行時のみログ出力が有効になるようにします。

具体的には、コマンドライン引数に `d` を指定した場合、以下のようにログが出力され、

```txt
$ python a.py d
solve():11| i * i: 100
```

`d` を指定しない場合、以下のようにログが出力されないように実装します。

```bash
$ python a.py
```

### コマンドライン引数によるログ出力ON/OFFの実装


```python
import sys

def get_logger(argv: list):
    """コマンドライン引数で d が与えられた場合、icを返す"""

    if len(argv) < 2 or argv[1] != "d":
        return lambda *args: None

    from icecream import ic

    def f(output: str):
        p = output.split(" ")
        lino, module, body = p[0].split(":")[-1], p[2][:-1], " ".join(p[3:])
        print(f"{module}:{lino}| {body}")

    ic.configureOutput(prefix="", outputFunction=f, includeContext=True)
    return ic

ic = get_logger(sys.argv)

# ---- ↑ テンプレート ↑ ----

def solve():
    i = 10
    ic(i * i)   # ←ここでログを出力
solve()
```

#### ログ出力のOFFの動作

`get_logger` 関数は、コマンドライン引数 `d` が指定されない場合、icの代わりに引数を受け取るだけで何も処理しない関数を返却します。  

これにより、ログ出力の記述を残したままでもログが出力されなくなり、処理速度の低下も最低限に抑えられます。

処理速度については、関数呼び出しのオーバーヘッドがあるため影響を完全にゼロにはできませんが、私の環境では$10^6$回の呼び出しで$50ms$弱の処理時間増加でしたので、ログ出力処理が原因でTLEになることは基本的にはなさそうです。


#### 競プロサイトへの提出時

AtCoder等のサイトへコード提出した際には、ログ出力用のコマンドライン引数 `d` は与えられないため、**自動的にログ出力をOFFにできます**。

#### バッドマナーゆるして

IceCreamのimportを関数内で行っている部分は一般的には行儀が悪い書き方ですが、AtCoder等の実行環境にはIceCreamはインストールされておらず、importするとRuntime Errorが発生するため、コマンドライン引数が指定されるローカルでの実行時のみimportが行われるようにしています。

## おわりに

IceCreamを使用してログ出力フォーマットをカスタマイズし、コマンドライン引数によりログのON/OFFを切り替える方法を紹介しました。改善点や他のいいテンプレートがあればぜひ教えてください🐾

## わたしの競プロ用Pythonテンプレート

現在わたしが使用しているテンプレートを載せておきます。ご自由に利用いただき、バグや改善点など教えていただけるとうれしいです。みんなで最強のテンプレートをつくろう！

### ビジュアライザ

2次元テーブル、木、グラフを可視化できるようにしています。デバッグに便利！

### ロガー

ヒューリスティックコンテストではicの代わりに標準モジュールのloggingを使用しています。ヒュには最近参加し始めたのですが、実装が複雑になりログレベル(DEBUG, INFO, ERROR)を指定して出力したい場合があるため使い分けています。

```python
import sys

sys.setrecursionlimit(10**7)
rint = lambda: int(sys.stdin.readline())
rints = lambda: list(map(int, sys.stdin.readline().split()))
rdecints = lambda: [e - 1 for e in rints()]
rfloat = lambda: float(sys.stdin.readline())
rfloats = lambda: list(map(float, sys.stdin.readline().split()))


class Vis:
    @staticmethod
    def table(arr: list):
        """2次元配列をテーブル表示"""
        rpad = 3
        width = max([max(map(lambda x: len(str(x)), row)) for row in arr])
        header = f"{'':<{rpad}}| " + "  ".join(
            f"{i:<{width}}" for i in range(len(arr[0]))
        )
        print(f"{header}\n{'-'*len(header)}")
        for row_idx, row in enumerate(arr):
            formatted_row = f"{row_idx:<{rpad}}| " + "  ".join(
                f"{str(item):<{width}}" for item in row
            )
            print(formatted_row)

    @staticmethod
    def tree(E: list, root=0):
        """隣接リストをツリー表示"""

        def dfs(u: int, marks: list):
            visit.add(u)
            s = ""
            for i, m in enumerate(marks):
                if i - len(marks) + 1 == 0:
                    s += f"{m}── "
                else:
                    s += "    " if m == "└" else "│   "
            print(s + str(u))
            vs = [v for v in E[u] if v not in visit]
            for i, v in enumerate(vs):
                marks.append("└" if i == len(vs) - 1 else "├")
                dfs(v, marks)
                marks.pop()

        visit = set()
        dfs(root, [])

    @staticmethod
    def graph(E: list, directed=False, weighted=False):
        """隣接リストをグラフ表示"""

        import networkx as nx
        from matplotlib import pyplot as plt

        n = len(E)
        G = nx.DiGraph() if directed else nx.Graph()
        G.add_nodes_from(list(range(n)))
        for v in range(n):
            if weighted:
                G.add_weighted_edges_from([(v, u, w) for u, w in E[v]])
            else:
                G.add_edges_from([(v, u) for u in E[v]])

        node_color = [(1.0, 1.0, 1.0, 0)] * n if directed else [(1.0, 1.0, 1.0)] * n

        pos = nx.spring_layout(G, weight=None)
        nx.draw_networkx_nodes(G, pos, node_color=node_color, node_size=1000)
        nx.draw_networkx_edges(G, pos, edge_color=["#000000"] * G.number_of_edges())
        nx.draw_networkx_labels(G, pos, font_size=18)
        if weighted:
            nx.draw_networkx_edge_labels(
                G,
                pos,
                nx.get_edge_attributes(G, "weight"),
                node_size=10,
                font_color="darkgreen",
            )

        plt.show()


class Logger:

    @staticmethod
    def ic(argv: list):
        """コマンドライン引数で d が与えられた場合、icを返す"""

        if len(argv) < 2 or argv[1] != "d":
            return lambda *args: None

        from icecream import ic

        def f(output: str):
            p = output.split(" ")
            lino, module, body = p[0].split(":")[-1], p[2][:-1], " ".join(p[3:])
            print(f"{module}:{lino}| {body}")

        ic.configureOutput(prefix="", outputFunction=f, includeContext=True)
        return ic

    @staticmethod
    def logging(argv: list):
        """コマンドライン引数でログレベルが指定された場合、ロガーを返す
        Args:
            argv (list): argv[1]でログレベルを指定する。(d/info/warn/err/cri)
        """

        LOG_LEVEL = {"d": 10, "info": 20, "warn": 30, "err": 40, "cri": 50}
        if len(argv) < 2 or argv[1] not in LOG_LEVEL:
            f = ["debug", "info", "warning", "error", "critical"]
            return type("A", (object,), {k: lambda *args: None for k in f})()

        import logging

        class CustomFormatter(logging.Formatter):
            def format(self, record):
                if record.args:  # 複数引数の場合は文字列として結合
                    record.msg = ", ".join(map(str, [record.msg, *record.args]))
                    record.args = ()
                return super().format(record)

        logger = logging.getLogger("")
        logger.setLevel(LOG_LEVEL[argv[1]])
        handler = logging.StreamHandler(sys.stderr)
        handler.setFormatter(
            CustomFormatter("[%(levelname)s:%(funcName)s:%(lineno)s] %(message)s")
        )
        logger.addHandler(handler)
        return logger


INF = (1 << 64) - 1
D = [(-1, 0), (1, 0), (0, -1), (0, 1)]
UDLR = {"U": (-1, 0), "D": (1, 0), "L": (0, -1), "R": (0, 1)}

log = Logger.ic(sys.argv)
# -------------------------------


```