はじめに
このシリーズは計算化学を題材にした Python プログラミングを紹介した「計算化学者のためのPython入門」の1つです。
特に「計算化学関連の Python プログラム開発に携わる予定」の読者を想定してます。
Python3 は Google Colab を使えばブラウザだけで実行 できます。
本記事の要点
- 計算化学においてもクラスの使い道はたくさんある。
- プログラミング原則、DRY (Don't repeat yourself)、を意識しましょう。
- 執筆者はオブジェクト指向初心者です。お手柔らかにお願いします。
オブジェクト指向で頻出単語
クラス、メソッド、インスタンス、コンストラクタなどの頻出単語をまとめます。
最初は何を意味しているのかわかりにくいと思いますが、がんばって慣れましょう(カタカナ覚えられない…)。
-
クラス(class)
- 学級というよりも、「類」という意味に近いと思います。
- ポケモンなどの概念を一括りにした類、というイメージです。
-
コンストラクタ(constructor)
- construct で「構築する」という意味です。
- クラスを構築するときに最初に呼ばれるメソッドのことを指します。
-
メソッド(method)
- これは
def hoge():
で定義するメソッドと同じ、「関数」を意味する。 - クラス内に定義された関数もメソッドと呼ぶ。
- これは
-
インスタンス(instance)
- For instance で「例えば」という意味ですね。instance は実例という意味があります。
- オブジェクト(object、実体、物) とも呼ばれる。
クラスやコンストラクタ、メソッド、または実際に利用する際のインスタンスは以下のように使います。
class Pokemon(): # クラスの定義方法
def __init__(self, name, level, type1, type2=None): # コンストラクタ:初期設定
self.name = name # self.xxxx とすることでクラス内で利用可能になる
self.level = level # 詳細は上記のリンクへgo
self.type1 = type1
self.type2 = type2
return
def show_type(self): # メソッドを定義
print("type1 = ", self.type1)
if self.type2 is not None:
print("type2 = ", self.type2)
return
def greeting(self): # self が必要な理由:上記のリンクへgo
print("I'm " + self.name + ". Lv. " + str(self.level) + "!")
return
# ---- Main program
pikachu = Pokemon("Pikachu", 81, "Electric") # インスタンス pikachu を定義
pikachu.show_type() # インスタンス(主語)、メソッド(動詞+目的語)のように使うと見通しが良い
pikachu.greeting()
print("---")
kabigon = Pokemon("Snorlax", 75, "Normal")
kabigon.show_type()
kabigon.greeting()
type1 = Electric
I'm Pikachu. Lv. 81!
---
type1 = Normal
I'm Snorlax. Lv. 75!
(ざっくりとした)オブジェクト指向
オブジェクト指向プログラミングとは、
- いろいろな機能(メソッド)が詰まったクラスを、
- まるで変数(オブジェクト、インスタンス)のようにみなす/扱うことで、
- プログラムの見通しを良くしたり、保守性を高めることができる
- プログラミングの流儀
のことです。
より詳細には「カプセル化」や「ポリモーフィズム (polymorphism: 多相性)」、「継承と抽象化」という概念があります。
以下の記事がわかりやすかったので共有します。
クラスの使いみち(ポケモン)
上のプログラムの簡易版をオブジェクト指向とそうじゃない流儀で書いてみましょう。
まずはオブジェクト指向です。
class Pokemon():
def __init__(self, name, level):
self.name = name
self.level = level
return
def greeting(self):
print("I'm " + self.name + ". Lv. " + str(self.level) + "!")
return
# ---- Main program
pikachu = Pokemon("Pikachu", 81)
pikachu.greeting()
print("---")
kabigon = Pokemon("Snorlax", 75)
kabigon.greeting()
ここで「クラスをオブジェクトのように扱う」というのは、pikachu = Pokemon("Pikachu", 81, "Electric")
の部分を指します。greeting()
という挨拶機能を持ったPokemon
クラスをあたかも変数pikachu
として扱っています。
このように、ポケモンとして共通の機能(greeting
)を持った個体(実体:オブジェクト)(Ex. pikachuやkabigon)をつくるという考え方がオブジェクト指向です。このようにすると、他のポケモンを扱うためにはどうすればいいのか、という見通しがつきやすくなります。
実はこのプログラムはオブジェクト指向を使わなくても書けます。
def pikachu(name, level):
print("I'm " + self.name + ". Lv. " + str(self.level) + "!")
return
def kabigon(name, level):
print("I'm " + self.name + ". Lv. " + str(self.level) + "!")
return
# ---- Main program
pikachu("Pikachu", 81)
print("---")
kabigon("Snorlax", 75)
ただし、このように書いてしまうと、プログラミング原則「DRY (Don't repeat yourself)」に反してしまいます。
つまり、print("I'm " + self.name + ". Lv. " + str(self.level) + "!")
をポケモンの数だけ書く必要があります。
こうなってしまうと、挨拶メッセージを変えたくなった場合、全部の行を間違いなく正確に修正する必要があります。
この程度のコードであれば、完璧にアップデートできると思いますが、もしこれが数百匹のポケモン全部に対して定義されていた場合、置換するとしても非常に多くの労力を費やすことになります。これでは楽するためのプログラムでかえって疲れてしまいます。
オブジェクト指向の場合、Pokemon
クラス内のgreeting()
メソッドだけを修正すれば、挨拶メッセージの改良に対応できます。このような点で、オブジェクト指向は保守性に優れていると言えます。
クラスの使いみち(計算化学)
では、計算化学の世界ではどのようにオブジェクト指向プログラミングを取り入れればいいでしょうか。
Moleculeクラス
ポケモンと同じ感覚で使われるのがMolecule
クラスです。
分子(Molecule)が持つ共通の性質(原子名、座標、分子量、沸点、融点 など)を返すメソッドを定義してあげれば、アンモニアやベンゼンといったそれぞれの実体をオブジェクトとして扱えるようになります。
例えば、PySCFプログラムではMole
クラスが定義されています。
- https://github.com/pyscf/pyscf/blob/7b91b3ce5a00947654c9057bdf1a22499278b2bf/pyscf/gto/mole.py#L2084
抽象化で子クラスの機能を縛る
クラスの機能として、抽象化と継承があります。詳細は省きますが、サンプルコードを見ればなんとなく意味がわかるはずです。
計算化学に限らず、プログラマーがよく遭遇する状況に「◯◯と✕✕の機能はほぼ同じなんだけど、△△だけ違う」というものがあります。
例えば、
- ◯◯と✕✕は、SCF手続きの流れは共通だけど、△△の部分だけ異なる。
- 第一原理MD計算の初期条件サンプリング法の◯◯と✕✕は、ほぼ共通だけど、エネルギー分配の方法だけ違う。
- いろいろな電子状態計算 (ESC) プログラムの結果をxxxxプログラム形式に変換したい。
のような状況です。
ここでも意識すべき原則は「DRY (Don't repeat yourself)」です。例えば、最後の ESCparser のサンプルプログラムを書いてみましょう。
このプログラムはESCの出力からenergy、gradientを取得することが求められているとします。このように異なるクラスだけれども、共通する機能が必要な場合、抽象化が役に立ちます。
from abc import ABCMeta, abstractmethod
class ESCparser(metaclass=ABCMeta): # 抽象クラスを定義
@abstractmethod # この下に定義したメソッド(energy)は
def energy(self): # 継承先の子クラスで必ず実装しなければならない
raise NotImplementedError
@abstractmethod
def gradient(self):
raise NotImplementedError
class GaussianParser(ESCparser): # ESCparserを継承した子クラスを定義
def __init__(self):
xxxx
return
def energy(self): # これを定義しないとエラーになる
xxxx
return
def gradient(self): # これも定義しないとエラーになる
xxxx
return
class GAMESSParser(ESCparser): # 抽象クラスの継承で機能を"縛る"
def __init__(self):
xxxx
return
def energy(self):
xxxx
return
def gradient(self):
xxxx
return
def hessian(self): # 新しいメソッドの追加は可
xxxx
return
このようにすることで、GaussianParser
とGAMESSParser
の機能(メソッド)を縛ることができました。
この抽象クラスと継承は共同開発において非常に役に立ちます。
つまり、抽象クラスを定義した開発者は今後MolproParser
を作る開発者に必要な機能を知らせることができます。一方、新規開発者は迷うことなくenergy
とgradient
メソッドの実装に取り掛かることができます。マニュアル化するのは面倒だけどプログラムの仕様として重要な部分は抽象クラスを使って縛るのが良いかもしれません。
情報を格納する箱
クラスの他の使い道として情報を受け取る箱として使うというものがあります(かなりPythonicなのでわかりにくいかもしれませんが)。もしかしたらPySCFもそのように使っているかもしれません。
Pythonではリスト型や辞書型、json形式で書き出す、などいろいろな方法で情報を格納することができます。ただ、これらの場合、格納した情報にアクセスするためには、事前知識が必要になることが多いです。
例えば、以下のように。
list_data = save_data2list() # データをlistに格納 print(list_data[0][0][0]) # xxxx はどの階層にあるのかな? dict_data = save_data2dict() # データをdict(辞書)に格納 print(dict_data["ESC"]["xxxx"]) # key(鍵)を知っていればデータにアクセス可能 save_data2json("save.json") # データをjson形式で出力 dict_data = load_json("save.json") # いちいち読み書きが必要
このようなことを避けるために、クラスを使うことができます。
ただ、この記事が対象としているPython初心者が扱える範疇を越えているような気がするので、参考書を紹介するにとどめたいと思います(車輪の再開発を避けた)。
おわりに
クラスやオブジェクト指向は初心者には難しい(かもしれない)概念です。
一度でわからなくてもいいと思います。何度も遭遇して、自分で少しずつ理解するのが良いと思います。