print()でデバッグする必要がなくなるicecream

# 概要

pythonで何かを開発したり、データ分析をしているときのデバッグってみなさんどうされているでしょうか？多くの人がprint()関数を駆使しているのではないかと思います。print()関数自体は単純ですが、データの型や表示の形を都度変えながらデバッグするのは面倒臭いことが多いと思います。

今回はそういったデバッグの面倒臭さを軽減してくれるライブラリ```icecream```を紹介します。

- icecreamのGithubページ：https://github.com/gruns/icecream
- 今回作成したコードの格納先（Github）：https://github.com/KENTAROSZK/icecream_test/tree/main

# icecreamのインストール
単純です。↓のコマンドでインストールできます。
```shell
pip install icecream
```

# 使い方
早速使い方を見ていきましょう。

## 基本機能
### 引数あり

#### 変数 | List
```python
# icrecreamのインポート
from icecream import ic

# List
my_list = ["a", "b", "c", " d"]

ic(my_list) # >> ic| my_list: ['a', 'b', 'c', ' d']
```

"ic|"というプリフィックス（プリフィックスも自由にカスタマイズできます。後で説明します）がついている状態で、変数の名前とその中身が表示されました。

#### 変数 | Dict
```python
# icrecreamのインポート
from icecream import ic

# Dict
my_dict = {"a": 1, " b": 2, "c": 3, "d": 4}

ic(my_list) # >> ic| my_dict: {'a': 1, 'b': 2, 'c': 3, 'd': 4}
```
辞書の場合も、リストと同様に表示されました。

#### ループさせてみる

```python
# icrecreamのインポート
from icecream import ic

# Listでループさせてみる
for i, val in enumerate(my_list):
  ic(i, val)

# 出力
# >> ic| i: 0, val: 'a'
# >> ic| i: 1, val: 'b'
# >> ic| i: 2, val: 'c'
# >> ic| i: 3, val: 'd'
```
リストの要素を```val```という変数に入れて、取得しているので、イテレーション毎にvalの変数の中身が表示されることがわかります。

ちなみに、引数を複数同時に与えることもOKです。

```python
# icrecreamのインポート
from icecream import ic

# Dictでループさせてみる（値を複数以上渡すこともできる）
for key, val in my_dict.items():
  ic(key, val)

# 出力
# >> ic| key: 'a', val: 1
# >> ic| key: 'b', val: 2
# >> ic| key: 'c', val: 3
# >> ic| key: 'd', val: 4
```
こちらもList同様に、キーと値をそれぞれ取得できていることがわかります。


#### pandasを表示させてみる
筆者はデータ分析をやっているため、pandasをよく使います。ので、これをicecreamに与えた場合の実行結果を確認してみます。

```python
# icrecreamのインポート
from icecream import ic

# pandasなどのデータを使った場合の挙動を確認してみる
import pandas as pd

# 辞書型からDataFrameを作成する
data = {
    'name': ['Alice', 'Bob', 'Charlie', 'David'],
    'age': [25, 30, 35, 40],
    'sex': ['女性', '男性', '男性', '男性'],
    'city': ['東京', 'ニューヨーク', 'ロンドン', 'パリ']
}

df = pd.DataFrame(data)

ic(df)


# 実行結果
# >> ic| df:       name  age sex    city
# >>         0    Alice   25  女性      東京
# >>         1      Bob   30  男性  ニューヨーク
# >>         2  Charlie   35  男性    ロンドン
# >>         3    David   40  男性      パリ
```

ちゃんとDataFrameが表示されています。ただ、この場合は、```display(df)```を使ったときの方が視覚的にはわかりやすいと個人的には思いました。


#### 関数
変数ではなく、関数の実行結果を表示させることもできます。
```python
# icrecreamのインポート
from icecream import ic

# 関数の定義
def my_function1(x: int) -> int:
  return x + 100

ic(my_function1(10)) # >> ic| my_function1(10): 110
```

関数名とその引数、関数の実行結果である110が同時に表示されました。


#### おまけ
##### python3.8以降であれば、f文字列で類似のことができる
f文字列を使い、確認したい変数（または関数）に『=』をつけると、icecreamと類似のことができます。

```python
# python3.8以降であれば、上でやったことと同じことができる
# fで文字列を囲み、実行結果を確認したい場合は、『=』をつけてあげる

print(f"{my_function1(10)=}") # >> my_function1(10)=110
print(f"{my_list=}") # >> my_list=['a', 'b', 'c', ' d']
```

セキュリティが厳しくて、ライブラリを自由にインストールできない、といったときは、この方法を使うのが最も簡単かもしれません。これはpandasなどでも使えます。

```python
print(f"{df=}")

# 実行結果
# >> df=      name  age sex    city
# >> 0    Alice   25  女性      東京
# >> 1      Bob   30  男性  ニューヨーク
# >> 2  Charlie   35  男性    ロンドン
# >> 3    David   40  男性      パリ
```



##### 自作のデコレータ
自作でデコレータを作ってデバッグしやすくすることもできます。
ただし、これは変数の表示などには使えないです。
あくまでも関数の出力を確認したい時に使えます。

```python
from functools import wraps

def logger(separator : str = '-'):
  def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
      # 事前処理
      print(20 * separator)

      result = func(*args, **kwargs)

      # 関数の出力結果を表示する
      print(result)

      # 事後処理
      print(20 * separator)

      return result
    return wrapper
  return decorator


# 関数にデコレータをつける
@logger("=")
def simple_func1(x: int, y: int) -> int:
  return x + y

# 実行してみる
func1_result = simple_func1(1,2)


# 出力結果
# >> ====================
# >> 3
# >> ====================
```

### 引数なし
```ic()```に対して引数を与えずに実行した場合は、つぎの情報を示してくれます。

- 呼び出し元のファイル名（実行しているファイル名）
- 行番号
- 親関数
- 実行時刻(UTC)


```python
# icrecreamのインポート
from icecream import ic

ic() # >> ic| 3418770342.py:4 in <module> at 17:11:06.832
```

関数の中で呼び出してみて、実行結果を確認してみる。
```python
# icrecreamのインポート
from icecream import ic

def my_function2():
  ic()
  val = my_function1(10)

  if val >= 10:
    ic()
    ic("val is over 10")
  else:
    ic()
    ic("val is uner 10")

# 実行結果
# >> ic| 955660172.py:5 in my_function2() at 17:12:07.791
# >> ic| 955660172.py:9 in my_function2() at 17:12:07.808
# >> ic| 'val is over 10'
```

5行目と9行目で呼び出されていることが確認できる。かつ、```my_function2()```という関数の中で実行されていることも確認できる。


これは、単に```print()```を使うよりもスマートだと思います。
```python
# icrecreamのインポート
from icecream import ic

def my_function3():
  print(0)
  val = my_function1(10)

  if val >= 10:
    print(1)
    print("val is over 10")
  else:
    print(2)
    print("val is uner 10")

# 実行結果
# >> 0
# >> 1
# >> val is over 10
```



## カスタマイズ

### 出力を文字列化
icecreamの出力（```ic()```）の結果を文字列化させる方法。デバッグのログをtxtファイルに格納しておく、みたいな使い方ができると思います。

```python
# icrecreamのインポート
from icecream import ic

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

print(out) # >> ic| mozi: 'sup'
print(type(out)) # >> <class 'str'>

print(out + " <-- icecreamの出力結果を文字列化したもの") # >> ic| mozi: 'sup' <-- icecreamの出力結果を文字列化したもの
```
文字列なので、最後のように、文字列を```+```で結合することができます。



### カスタム
出力フォーマットをカスタマイズすることができます。icecreamでは以下の5つのカスタマイズパラメータを設定できます。

今回は筆者が便利だと感じた、```preftx```、```argToStringFunction```、```includeContext```、```contextAbsPath```を紹介します。

#### prefix
プリフィックスを『ic|』ではないものにすることができます。
単純な文字列にすることも可能ですが、関数を渡すこともできます。関数を渡せるということは、実行時刻を確認することもできるようになるということですね。

```python
# icrecreamのインポート
from icecream import ic

# prefixの変更
ic.configureOutput(prefix='output :-> ')


ic(my_function1(10)) # >> output :-> my_function1(10): 110
ic(my_list) # >> output :-> my_list: ['a', 'b', 'c', ' d']
ic(my_dict) # >> output :-> my_dict: {'a': 1, 'b': 2, 'c': 3, 'd': 4}
```
自分にとってわかりやすいprefixをつけられるのはデバッグや動作確認で便利ですね。



```python
# icrecreamのインポート
from icecream import ic

def get_current_time():
  from datetime import datetime
  
  # 実行時刻を取得する
  current_time = datetime.now()
  # フォーマットに合わせて整形する
  formatted_time = current_time.strftime("%Y-%m-%d %H/%M/%D %S")
  
  return formatted_time + " :->  "

# prefixの変更
ic.configureOutput(prefix=get_current_time)

import time # 連続して実行してしまうと、時刻が更新されているのかわからないため、time.sleepで強制的に2秒間を停止させてみた。実際のコードではこれは不要です。
ic(my_function1(10)) # >> 2023-11-26 17/17/11/26/23 21 :->  my_function1(10): 110

time.sleep(2)
ic(my_list) # >> 2023-11-26 17/17/11/26/23 23 :->  my_list: ['a', 'b', 'c', ' d']

time.sleep(2)
ic(my_dict) # >> 2023-11-26 17/17/11/26/23 25 :->  my_dict: {' b': 2, 'a': 1, 'c': 3, 'd': 4}
```
関数で定義したように、年月日と実行時の秒が表示されました。

#### argToStringFunction
```ic()```の引数が特定のデータ型だったときに、それだけic()の処理結果を変えるメソッドです。文字列が入ってきたときだけ、何か特殊な出力をさせたい時に使えます。
例えば、文字列だったときは文字列の長さを返す、ListだったときはListの長さを返す、などのことができます。


```python
# icrecreamのインポート
from icecream import ic

# icの引数が文字列だった場合は、その文字列の長さを返す
def toString(obj):
  if isinstance(obj, str):
    return f"string : `{obj}` with length {len(obj)}"
  return repr(obj)


# prefixの変更
ic.configureOutput(prefix="ic|")

# 上で定義した関数をメソッドに引き渡す
ic.configureOutput(argToStringFunction=toString)


ic(10) # >> ic|10
ic('hello') # >> ic|string : `hello` with length 5
ic(10, 'hello') # >> ic|10, string : `hello` with length 5
```

#### includeContext
引数を渡して実行するときに以下の情報を出力するオプション。
- ファイル名
- 行数
- 親関数名


```python
# icrecreamのインポート
from icecream import ic

# 関数の定義
def my_function1(x: int) -> int:
  return x + 100


ic(my_function1(10)) # >> ic|my_function1(10): 110


ic.configureOutput(includeContext=True)
ic(my_function1(10)) # >> ic|1866653307.py:13 in <module>- my_function1(10): 110

ic.configureOutput(includeContext=False)
ic(my_function1(10)) # >> ic|my_function1(10): 110
```

#### contextAbsPath
実行しているファイルの絶対パスを表示するためのオプション。VisualStudioCodeなどのエディタでは、絶対パスを表示した時に、クリックして該当ファイルに飛べるようになっているため、デバッグがしやすくなる。

```python
# icrecreamのインポート
from icecream import ic

# 関数を定義する
def my_function4():
  _list = ["a", "A"]
  ic(_list)


# 絶対パス表示させてみる
ic.configureOutput(includeContext=True, contextAbsPath=True)
my_function4() # >> ic|/private/var/folders/_5/rkk_3xfx78z09g62fhwhsml80000gn/T/ipykernel_21251/3659232331.py:11 in my_function4() _list: ['a', 'A']


# 絶対パス表示させない
ic.configureOutput(includeContext=True, contextAbsPath=False)
my_function4() # >> ic|2094017797.py:10 in my_function4()- _list: ['a', 'A']
```
実行しているファイルの絶対パスが表示と非表示を切り替えることができた。



## icecreamは重い
icecreamはかなり便利ですが、実はかなり重いとのことです [参考記事：https://qiita.com/kinuta_masa/items/50198a2a889ad53afe9b]。筆者は未検証ですが、```print()```するよりも、**80〜150倍程度遅いようです。**

そのため、icecreamを使う場合は、ループの中で使うよりも特定の箇所に限定するような使い方をするのが望ましいと思います。


# まとめ
- デバッグに非常に便利なicecreamを紹介しました。python以外にも、RやC++、Ruby、Javaなどの他の言語でも同様に存在するパッケージのようです。
- 変数名やデータの中身をサクッと確認でき、かつ、実行箇所（何行目か？）なども同時に表示できるので、デバッグの時に非常に便利だと思います。
- ただ、最後に記載したように、重いので、使うべき箇所を見極めた上で使うことが望ましいです。
    - githubで書かれているように、```print()```を使わなくても済む（Never use print() to debug again）とはいかないと思います。