✏️

Pythonでclassを使う: 設計編(1) 関数とクラス

2022/12/19に公開

はじめに

この記事は、(主に研究等で)pythonのプログラムを書く時に、どのように設計すれば良いかについて論じながら、クラスを導入する時に生じるメリットについて述べていきます。

内容

単一の処理の記述

任意の処理Aの記述の際は、非常に簡単な処理を除けば、処理Aに対応する関数を記述すると良いです。

関数のメリットは以下が考えられます。

  1. 処理に関数名として処理に名前をつけられるため、可読性が高い
  2. 1により余計なコメントアウトを書く必要はない
  3. 汎用化させやすい(同じ処理を何度も書く必要がない)
  4. docstringで記述した関数のコメントをコードで呼べる
  5. 処理の引数に型アシストを付与できる
  6. テストの記述が簡単
  7. 後のパッケージ化が簡単

番号が大きくなるにつれてメリットは薄いかもしれません。

一応4について補足しておきますが、関数名+?で説明を呼ぶことができます。

In [1]: def add_2things(a,b):
   ...:     """
   ...:     引数a,bを足す関数
   ...:     """
   ...:     return a+b
   ...:

In [2]: add_2things?
Signature: add_2things(a, b)
Docstring: 引数a,bを足す関数
File:      ~/tmp/<ipython-input-1-e1908036e578>
Type:      function

In [3]:

またGithubなどの編集アシスト機能などでもdocstringを表示することもできます。

複数の処理の記述

さて次は、あるobjectmodelに対して、処理A,Bを施すことを考えます。

実装をする場合は、引数をmodelとして関数A,B(model_func_A,model_func_B)を記述すると以下のようになります。(記述例1)

記述例1

def model_func_A(model):
    #処理A

def model_func_B(model):
    #処理B

関数名は、modelに対して処理を行うことを明示的に表すため、model*としました。


ここから本記事の本題に移ります。

筆者のオススメは、同一のobjectに対して複数の処理を記述する場合は記述例1と書くところを以下のように記述することです。

class ModelFuncClass:
    def __init__(self, model):
        self.model = model
    def func_A(self):
        #処理A self.modelでmodelを呼び出す
    def func_B(self):
        #処理B

関数で記述した場合(記述例1)との違いは以下です。

  • 複数の関数がクラスにまとまっている
  • 引数をselfで保持することで、メソッドを呼び出す時に引数を減らせている
  • 関数名を短縮できる(modelに対する処理であることはクラス名からわかるので)

複数の処理の記述(共通の引数)

class化のメリットは他にもあります。

例えば処理A,Bを施す際に、処理Xの出力が必要である場合を考えます。

関数で記述した場合は、新たに関数X(model_func_X)を導入して、以下のように書けます。

def model_func_X(model):
    #処理X
    return x

def model_func_A(model,x):
    #処理A

def model_func_B(model,x):
    #処理B

この記述では、関数X(model_func_X)の出力をmain関数側で保持し、関数A,B(model_func_A,model_func_B)に渡す必要があります。

ここでclassを用いる以下のようになります。

class ModelFuncClass:
    def __init__(self, model):
        self.model = model
	self.x = None #初期値があれば書くと良い
    def func_X(self):
        #処理X 
	self.x = x 
    def func_A(self):
        #処理A self.model, self.xでそれぞれmodel,xを呼び出す
    def func_B(self):
        #処理B

関数との大きな違いは、関数Xの出力をmain関数側で保持する必要がないということです。

さらに(当たり前かもしれませんが、)modelの処理に関わる変数をclassで保持できるので、可読性が高いです。

また__init__においてself.x = Noneと記述しているのは、保守性を高めるため(self.xが定義されていないというエラーを出ないようにするため)、可読性を高めるため(クラスが保持する変数が__init__でわかる)です。

また関数Aで以下のようにself.xがない時だけ関数Xを呼ぶのに役立ちます。
(余計なflagを使わないで済む&self.func_Xを何度も呼ばなくて済む)

# classのメソッド
def func_A(self):
    if not self.x:
        self.func_X()
    #処理A 

また関数A(メソッドA)の処理において、途中計算の結果を別のメソッドで使用したいと思った場合、
classで書いておけば、インスタンス変数として定義するだけで済みます。

つまり途中計算の結果をスコープの外で使うことができるということです。(自分一人のシステムなら良いが、多用すると訳がわからなくなるので注意)

# classのメソッド
def func_A(self):
    self.y = (なんらかの途中計算の結果) # 別のメソッドでself.yとして使用可能

まとめ

次回はclassの設計について述べる予定です。

Discussion