タイトルオチです。
記事の背景
あきとしのスクラップノートさんの以下の記事を読み、感銘を受けました。
[python] context manger を使ってmatplotlibの図を大量生産する
そう、matplotlibってやたら行数多くなるんですよね…!
まだ読まれてない方はこの記事より先にぜひ読んでください。
記事の内容をgithubでも公開してくださっています。
本記事では上記の記事に全面的に依拠しつつ、
データの渡し方をdict
からdataclass
にアレンジしてみました。
ささやかな差分ですが、自分の備忘録代わりにメモします。
※ 2022-04-03 一部内容変更
キーワード引数の渡し方をより簡単な方法に変更しました(set_xticks
のところ)。
dataclassにしてみたコード
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)
dataclass
を定義した以外は元の記事とほぼそのままです。
元の記事の中ほどに出てきますが、複数の引数を辞書で渡すやり方も紹介されています。
辞書の代わりにdataclass
を使うことで、図の設定を少し流用しやすくするという意図です。
@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)
いくつかの図の設定をテンプレート的に使い分けたいときや、
BasicPlot
を外部モジュールとして、複数のファイルから利用するときに便利かもしれません。
ただし、dataclass
を使うことによる不便もあります。
新たな引数を設定するときに、Plot用のクラスとdataclass
の2箇所変更する必要が出てきます。
アドホックな分析であれば、with文の中に書く方が手っ取り早いでしょう。
たとえば新たにx軸の目盛りをいじりたいときは、以下のようにします。
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)
簡単ですね!
もし頻繁にその設定を変更する必要があれば、クラスのオーバーライドをしてもいいかもしれません。
@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用のクラスの中でアンパックするやり方が使えるかもしれません。
@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)
が、この煩雑さに見合うほど楽になるかというと正直微妙です。
特にキーワード引数をいちいち辞書にするのがめんどうです。1
やろうと思えば可能ではある(が、めんどくさい)くらいの認識がちょうどよいかもしれません。
修正前(クリックで表示)
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)
まとめ
基本的にはwith文に都度書いていくのが1番手っ取り早そうです。
繰り返しが多くなってテンプレート化したくなったら、この記事の内容が役に立つかもしれません。
-
辞書にせずに済むようになりました(2022-04-03変更)。 ↩