技術は使ってなんぼ

自分が得たものを誰かの役に立てたい

【Python】オブジェクト指向でスマートなコードを書こう【matplotlib】

こんにちは。

年の瀬が迫ってまいりましたね。

私も仕事やプライベートが一層忙しくなってきてますが、ブログは生業にしたいので頑張って書きます。

さて今回ですが、オブジェクト指向のプログラミングでmatplotlibを実装してみようと思います。

前回に引き続きStreamlitのsearch_parkを使います。

yonesuke0716.hatenablog.com

今回修正を加えるのはこの部分です。

is_graph = st.checkbox('年齢をグラフ化する')
if is_graph:
    fig, ax = plt.subplots()
    ax.bar(df["公園"], df["年齢"])
    ax.set_ylim([0, 3])
    ax.set_yticks([0, 1, 2, 3])
    ax.set_title("公園と年齢")
    ax.set_xlabel("公園")
    ax.set_ylabel("年齢")
    st.pyplot(fig)

matplotlibでグラフを書くのに、これだけコードを毎回記述するのは大変ですよね。

これは一つのグラフですが、複数グラフを書きたい場合どうでしょう。

想像しただけでウンザリしてしまいますね。

コードの可読性という観点からも、非常に読みにくく、今後バグの温床になる可能性もあります。

そんな時にオブジェクト指向でコードを書くと楽になるかもしれませんというお話をしていこうと思います。

それではよろしくお願いします。



オブジェクト指向とは

簡単にオブジェクト指向についておさらいしておきます。

プログラミングを学ぶ人の中でオブジェクト指向を学んだことがある人はいるでしょう。

しかし実際に現場等でオブジェクト指向なコードを書いた人というのは少ないかもしれません。

その理由のひとつとして、オブジェクト指向が抽象的な特徴があるからかもしれません。

具体例をみていきましょう。自動車を例に解説します。

自動車には基本機能として、「走る」「曲がる」「止まる」という機能があり、タイヤが4つあるという特徴があると思います。

また車にはガソリン車もあればEV車もあります。

ガソリン車にはアイドリングストップ機能があるかもしれないし、EV車には自動運転できる車もあるかもしれません。

これをオブジェクト指向でコードを書くと以下のようなイメージなります。

自動車のオブジェクト指向イメージ図

オブジェクト指向ではこのように記述することで、自動車の「概念」を定義、つまり設計するわけです。

設計した自動車を使うためにはどうすればいいでしょうか?このようにします。

自動車クラスのインスタンス

こうすることで、変数carは自動車の共通機能を持った実体として扱うことができます。

これをオブジェクトのインスタンス化といいます。

そして、ガソリン車として動かしたい場合はどうするか。このようになります。

ガソリン車の実行

変数carは自動車クラスなので、class内の関数(メソッド)を呼び出すことができます。

以上がオブジェクト指向のイメージです。

概念を定義するということと、使うときのコードの書き方がイメージできればOKです。


matplotlibでオブジェクト指向プログラミング

オブジェクト指向について理解できたところで、さっそくmatplotlibで実装していきましょう。

新しくgraph.pyというファイルを作成します。

class Graph:
    def __init__(self, x_min=None, x_max=None, y_min=None ,y_max=None,
                 g_title="", x_label="", y_label="", x_ticks=None, y_ticks=None):
        self.fig, self.ax = plt.subplots()
        self.ax.set_title(g_title)
        if x_min is None and x_max is None:
            pass
        elif x_min is None:
            self.ax.set_xlim(right=x_max)
        elif x_max is None:
            self.ax.set_xlim(left=x_min)
        else:
            self.ax.set_xlim([x_min, x_max])
        if y_min is None and y_max is None:
            pass
        elif y_min is None:
            self.ax.set_ylim(top=y_max)
        elif y_max is None:
            self.ax.set_ylim(buttom=y_min)
        else:
            self.ax.set_ylim([y_min, y_max])
        self.ax.set_xlabel(x_label)
        self.ax.set_ylabel(y_label)
        if x_ticks is not None:
            self.ax.set_xticks(x_ticks)
        if y_ticks is not None:
            self.ax.set_yticks(y_ticks)

    # 縦棒グラフ
    def plot_bar(self, x, y, layout):
        self.ax.bar(x, y)
        layout.pyplot(self.fig)

def __init__は名前の通りclassオブジェクトを初期化する関数です。

初期化はclassオブジェクトをインスタンス化した際に一度だけ実行されます。

では早速使ってみましょう。前回のグラフ部分を書き換えるとこのようになります。

import streamlit as st
import matplotlib.pyplot as plt
import japanize_matplotlib
import numpy as np
import pandas as pd
import seaborn as sns

from graph import Graph

sns.set(font="IPAexGothic")

st.markdown(" ## 公園検索")
df = pd.read_excel("test.xlsx", engine="openpyxl")
machi = df["市町村"].unique().tolist()
toshi = df["年齢"].unique().tolist()
m_select = st.sidebar.selectbox("市町村を選択してください", machi)
t_select = st.sidebar.selectbox("年齢を選択してください", toshi)
result_df = df[(df["市町村"] == m_select) & (df["年齢"] == t_select)]
if len(result_df) == 0:
    st.write("一致する結果はありません")
else:
    st.write("一致した場所: ")
    for i in range(len(result_df)):
        st.write(result_df["公園"].values[i])
        url = result_df["URL"].values[i]
        link = f"[{url}]({url})"
        st.markdown(link, unsafe_allow_html=True)

is_graph = st.checkbox('年齢をグラフ化する')
if is_graph:
    max_age = st.slider('年齢の上限', 0, 5, 3)
    age_ticks = range(max_age + 1)
    
    park_list = df["公園"].to_list()
    select_park = st.multiselect('表示する公園名を選択', park_list, park_list[0])
    df = df[df['公園'].isin(select_park)]
    
    # 前回
    # fig, ax = plt.subplots()
    # ax.bar(df["公園"], df["年齢"])
    # ax.set_ylim([0, max_age])
    # ax.set_yticks(age_ticks)
    # ax.set_title("公園と年齢")
    # ax.set_xlabel("公園")
    # ax.set_ylabel("年齢")
    # st.pyplot(fig)

    # 今回
    graph_obj = Graph(y_min=0, y_max=max_age, g_title="公園と年齢", x_label="公園", y_label="年齢", y_ticks=age_ticks)
    graph_obj.plot_bar(df["公園"], df["年齢"], st)

実行してみると、当たり前ですが結果は前回と同じです。

オブジェクト指向のアウトプット

このgraph.pyは縦棒グラフだけですが、defに散布図や円グラフ、ヒストグラム等もどんどん追加できます。

なので今後も汎用性の高いグラフ用のオブジェクトとしてもご利用できますので、良かったら使ってください。

まとめ

いかがでしょうか。大分スマートなコードになったと思います。

冒頭で述べた懸念点が、実際にコードに落とすことでイメージが沸いたのではないでしょうか。

同じような記述を避けるために使われる手法として、関数化が一般的かもしれません。

しかし複雑なシステムになればなるほど、関数で定義するのには限界があります

そんな時にオブジェクト指向で設計しておけば、とてもスマートにコードがかけるのでぜひ使えるようになってください。

いつも通り、今回の変更点もGithubにアップしておきますので、興味があればご覧ください。
github.com