俺は長文matplotlibをやめるぞ！ジョジョーーーッ!!

タイトルオチです｡

## 記事の背景
[あきとしのスクラップノート](https://akitoshiblogsite.com/)さんの以下の記事を読み､感銘を受けました｡

[[python] context manger を使ってmatplotlibの図を大量生産する](https://akitoshiblogsite.com/context-manager-matplotlib/)

**そう､matplotlibってやたら行数多くなるんですよね…！**
まだ読まれてない方はこの記事より先にぜひ読んでください｡

記事の内容をgithubでも公開してくださっています｡

[contextplt -github-](https://github.com/toshiakiasakura/contextplt)

本記事では上記の記事に全面的に依拠しつつ､
データの渡し方を`dict`から`dataclass`にアレンジしてみました｡
ささやかな差分ですが､自分の備忘録代わりにメモします｡

## ※ 2022-04-03 一部内容変更
キーワード引数の渡し方をより簡単な方法に変更しました(`set_xticks`のところ)｡

## dataclassにしてみたコード
```Python:
from typing import NamedTuple, Optional
import dataclasses
from dataclasses import dataclass

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib

@dataclass
class BasicPlotArgs:
    xlim: Optional[tuple[float]] = None
    ylim: Optional[tuple[float]] = None
    xlabel: str = ""
    ylabel: str = ""
    title: str = ""
    save_path: Optional[str] = None
    figsize: tuple[int] = (6,4)
    dpi: int = 150
    tight: bool = False
    show: bool = True


class BasicPlot():
    def __init__(self, pa: Optional[BasicPlotArgs]=None):
        if not pa:
            pa = BasicPlotArgs()
        self.fig = plt.figure(figsize=pa.figsize, dpi=pa.dpi)
        self.ax = self.fig.add_subplot(111)
        self.ax.set_xlabel(pa.xlabel)
        self.ax.set_ylabel(pa.ylabel)
        self.ax.set_xlim(pa.xlim) if pa.xlim else None
        self.ax.set_ylim(pa.ylim) if pa.ylim else None
        self.save_path = pa.save_path
        self.title = pa.title
        self.tight = pa.tight
        self.show = pa.show

    def __enter__(self):
        return(self)

    def __exit__(self, exc_type, exc_value, exc_traceback):
        self.option()
        plt.title(self.title)
        plt.tight_layout() if self.tight else None 
        plt.savefig(self.save_path) if self.save_path else None
        plt.show() if self.show else None

    def option(self):
        '''This method is for additional graphic setting. 
        See DatePlot for example.'''
        pass

xx = np.linspace(-5,5,20)
yy = xx*xx

with BasicPlot() as p:
    p.ax.plot(xx,yy)

```
![output.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602439/6cd7fe05-28ba-6153-13a7-2ef7f8d6aa1d.png)


`dataclass`を定義した以外は元の記事とほぼそのままです｡

元の記事の中ほどに出てきますが､複数の引数を辞書で渡すやり方も紹介されています｡
辞書の代わりに`dataclass`を使うことで､図の設定を少し流用しやすくするという意図です｡


```Python:BasicPlotArgsを
@dataclass
class NewPlotArgs(BasicPlotArgs):
    xlabel:str = 'NEW!'

newplotargs = NewPlotArgs(xlim=(0,10), ylim=(0,10))

with BasicPlot(newplotargs) as p:
    p.ax.plot(xx,yy)
```
![output2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602439/85d75e84-7e16-4f3f-8334-41012cd3339e.png)


いくつかの図の設定をテンプレート的に使い分けたいときや､
`BasicPlot`を外部モジュールとして､複数のファイルから利用するときに便利かもしれません｡

ただし､`dataclass`を使うことによる不便もあります｡
新たな引数を設定するときに､Plot用のクラスと`dataclass`の2箇所変更する必要が出てきます｡

アドホックな分析であれば､with文の中に書く方が手っ取り早いでしょう｡
たとえば新たにx軸の目盛りをいじりたいときは､以下のようにします｡

```Python:
xx = np.linspace(-5,5,20)
yy = xx*xx

with BasicPlot() as p:
    p.ax.set_xticks(range(-5,6))
    p.ax.plot(xx,yy)
```
![output3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602439/3f2c0f83-54a4-0814-9643-ee423a1f5e94.png)

簡単ですね！

もし頻繁にその設定を変更する必要があれば､クラスのオーバーライドをしてもいいかもしれません｡

```Python:
@dataclass
class AnotherPlotArgs(BasicPlotArgs):
    xticks: Any = None


class AnotherPlot(BasicPlot):
    def __init__(self, pa: Optional[AnotherPlotArgs] = None):
        super().__init__(pa)
        self.ax.set_xticks(pa.xticks)
```

先ほどの`set_xticks`はシンプルに1つの位置引数を取りました｡

もし`set_xticks`に`labels`のようなキーワード引数も渡したいときは､
やや強引ですが､位置引数をリスト､キーワード引数を辞書にして､
Plot用のクラスの中でアンパックするやり方が使えるかもしれません｡

```Python:
@dataclass
class AnotherPlotArgs(BasicPlotArgs):
    xticks: Optional[Args] = None


class AnotherPlot(BasicPlot):
    def __init__(self, pa: Optional[AnotherPlotArgs] = None):
        super().__init__(pa)
        self.ax.set_xticks(*pa.xticks.ag, **pa.xticks.kw) if pa.xticks else None


class Args:
    def __init__(self, *args, **kwargs):
        self.ag = args
        self.kw = kwargs


xticks = Args(np.arange(5), labels=['G1', 'G2', 'G3', 'G4', 'G5'])
# 変更前: xticks = Args([np.arange(5)], {'labels':['G1', 'G2', 'G3', 'G4', 'G5']}) 

apa = AnotherPlotArgs(xticks=xticks)
with AnotherPlot(apa) as p:
    p.ax.plot(xx,yy)

```

![output5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602439/f2edeab2-f05d-344e-2c4f-6d291dcf1627.png)



が､この煩雑さに見合うほど楽になるかというと正直微妙です｡
~~特にキーワード引数をいちいち辞書にするのがめんどうです｡~~[^1]
[^1]: **辞書にせずに済むようになりました(2022-04-03変更)｡**

やろうと思えば可能ではある(が､めんどくさい)くらいの認識がちょうどよいかもしれません｡

<details><summary>修正前(クリックで表示)</summary>

```Python:
class Args(NamedTuple):
    ag: list = []
    kw: dict = {}

@dataclass
class AnotherPlotArgs(BasicPlotArgs):
    xticks: Optional[Args] = None

class AnotherPlot(BasicPlot):
    def __init__(self, pa: Optional[AnotherPlotArgs] = None):
        super().__init__(pa)
        self.ax.set_xticks(*pa.xticks.ag,**pa.xticks.kw) if pa.xticks else None
        self.ax.bar(*pa.bar.ag, **pa.bar.kw) if pa.bar else None

xticks = Args([np.arange(5)], {'labels':['G1', 'G2', 'G3', 'G4', 'G5']})

apa = AnotherPlotArgs(xticks=xticks))

with AnotherPlot(apa) as p:
    p.ax.plot(xx,yy)

```
</details>

## まとめ
基本的にはwith文に都度書いていくのが1番手っ取り早そうです｡
繰り返しが多くなってテンプレート化したくなったら､この記事の内容が役に立つかもしれません｡