リファクタリングをPythonで実践!基本手法とコード例・注意点まで
1.Pythonとは?
Python(「パイソン」と読みます)はオブジェクト指向のプログラミング言語の一種です。オブジェクト指向は後ほど詳しく解説しますが、プログラミングのパターンの一つです。オブジェクト指向でコードを書くと、拡張性や保守性に優れたコードが書きやすくなります。このオブジェクト指向を実現するための機構が今回の記事で取り上げるclassです。
Pythonはオブジェクト指向である利点を生かし、多数のライブラリが提供されています。ライブラリを使って容易に言語を拡張できるのもオブジェクト指向のメリットの一つです。そのため、AI開発、Web開発、統計処理、数学的研究、RPAと応用の幅が広くなっています。
部品を組み合わせて作るプラモデル、それがオブジェクト指向言語であるPythonのプログラミングのイメージでしょうか。
初心者にはオブジェクト指向はとっつきにくいかもしれませんが、Pythonのclassは簡潔な記述で実装できるため、他のオブジェクト指向言語に比べると直感的に理解しやすくなっています。初心者がオブジェクト指向言語を学ぶなら、Pythonにしておいて損はないでしょう。
2.オブジェクト指向プログラミングとは
①オブジェクトとは
オブジェクトとは、直訳すると「もの」です。従来のプログラミング言語では、処理する対象への処理の仕方を全て順番に記述する必要がありました。そのため、処理する対象を厳密に把握していなければならず、例えば、数値と文字列の違いのときには処理の仕方を全く変える必要がありました。
それに対してオブジェクトは、自分の処理の仕方を自分で知っています。実は、データのまとまりに、そのデータの処理の仕方も一緒にカプセルに入れてしまうのです。
そのおかげで、外側からデータを処理する際、中身が何であっても、単一の記述で違う種類のデータを処理してしまえます。データに処理方法がくっついているおかげで、データの内容ごとに事細かに違う処理を書いてやらなくても済むのです。
これは大変な利点で、コードを拡張するのが容易になります。拡張対象が何であっても同じ記述で済むのですから。また、データの方を見れば処理の記述があるので、データが変わったときにそこだけ修正すれば良くなり、保守性も向上します。オブジェクトの利点は大きくその拡張性、保守性です。
②class(クラス)とは
そのオブジェクトの内容を記述しておく仕組みがclassです。classとは「ひな型」です。ひな型の中に、データを入れる領域、処理の仕方を書く領域を設けます。
classが持つデータへの直接的な操作はclassの中で完結します。しかし、実際のプログラミングではclassが持っているデータを外部から直接操作するのが手っ取り早いときもあるので、Pythonはその手段も提供しています。臨機応変にclassを設計できるのがPythonのclassの特徴です。
③メソッドとは
classの中で、処理の仕方を書いてある部分を「メソッド」と呼びます。実際には、Pythonのclassの「メソッド」は関数です。classの「メソッド」の中では自身のデータを参照できます。
④インスタンス化とは
classとは「ひな型」と書きました。では、現実にデータを格納するときはどうやるのでしょう?それが、「インスタンス化」です。「インスタンス」とは、ひな型から実際のデータを格納したものを作り、データを保存しておくものです。
3.Pythonにおける「class(クラス)」の使い方
①classの定義
この記事では、以下のclassを元に解説していきます。
class SimpleData:
a = 0
b = 0
def sum(self):
return self.a + self.b
def set(self, a, b):
self.a = a
self.b = b
このクラスSampleDataはデータを二つ持っています。aとbです。それに対して、メソッドを二つ持っています。合計値を返すsumと、aとbに値をセットするsetです。
なおselfは「自分自身」という意味です。メソッドの引数にあるselfですが、メソッドは第一引数にselfを書くというルールが決まっています。ここでは深く考えず、こういうものだと認識してください。
それに対して、
self.a = a
self.b = b
の左辺にあるselfとは、自分自身の変数に、という意味になります。引数でもらったaを自分自身のaに、bを自分自身のbに代入する処理になっています。
self.aとa、self.bとbは全く違う変数ですので注意してください。同じ名前が付けられているだけで、全くの別物です。
②classのインスタンス化とインスタンス変数
ではこのSimpleDataを使う際にはどのようにすればいいのでしょうか?それが、「インスタンス化」です。
data1 = SimpleData()
data2 = SimpleData()
のようにして、SimpleDataに実際データが入ったものを作ってやります。
data1 = SimpleData()
data1.set(1, 2)
print(data1.sum())
3
このようにして、「.」の後にメソッド名を続けてメソッドを呼び出します。ここで問題です。以下のコードを実行すると、結果はどうなるでしょうか?
data1 = SimpleData()
data2 = SimpleData()
data1.set(1, 2)
data2.set(3, 4)
print(data1.sum())
print(data2.sum())
答えは
3
7
です。
どうしてこうなるのでしょうか?それは、data1とdata2は別々のインスタンスだからです。お互いに何の関係もありません。データは別々に保持されています。
data1やdata2のことを「インスタンス変数」と呼びます。
メソッドの中では、selfで自身の持つデータにアクセスできます。また引数でもらった変数を参照することもできます。メソッドを定義するとき、classから一つインデントを下げてdefを書き、更にインデントを下げてdefの中の処理を書きます。
Pythonではまとまりごとにインデントで区別するのでしたね。メソッドもまたまとまりです。
③コンストラクタとは
コンストラクタとは、「インスタンス化されたときに最初に呼ばれる特別なメソッド」です。データの初期化の処理を書きます。SimpleDataにコンストラクタを付けましょう。
def __init__(self):
self.a = 0
self.b = 0
メソッドでは第一引数はselfでした。このように書くと、クラスのデータであるaとbが0に初期化されます。
この__init__というコンストラクタを作ると、SimpleDataは次のように書き直せます。
class SimpleData:
def __init__(self):
self.a = 0
self.b = 0
def sum(self):
return self.a + self.b
def set(self, a, b):
self.a = a
self.b = b
どこにもaとbがないと思うかもしれませんが、__init__の中にself.aとself.bという記述があることにより、そこでデータ領域が作られるのです。
つまり、
data1 = SimpleData()
print(data1.sum())
は
0
となります。コンストラクタは複数持つことができ、引数を与えることもできます。
class SimpleData:
def __init__(self):
self.a = 0
self.b = 0
def __init__(self, a, b):
self.a = a
self.b = b
def sum(self):
return self.a + self.b
def set(self, a, b):
self.a = a
self.b = b
def __init__(self, a, b)はaとbを与えてその値で初期化するコンストラクタです。次のように使います。
data1 = SimpleData(1, 2)
このコードだと、aは1に、bは2に初期化されます。
data1 = SimpleData(1, 2)
print(data1.sum())
は
3
となります。どのコンストラクタが呼ばれるかは、引数の数で自動判定されます。
④デストラクタとは
反対に、インスタンスが破棄されたとき(正確に言うと、どこからも参照されなくなったとき)に呼ばれる特別なメソッドを定義することもできます。それをデストラクタと呼びます。
class SimpleData:
def __init__(self):
self.a = 0
self.b = 0
def __init__(self, a, b):
self.a = a
self.b = b
def __del__(self):
print('インスタンスが破棄されました')
def sum(self):
return self.a + self.b
def set(self, a, b):
self.a = a
self.b = b
と書くと、
data1 = SimpleData(1, 2)
print(data1.sum())
data1 = None
は
3
インスタンスが破棄されました
となります。
実際にはデストラクタを使うことはあまりないですが、コンストラクタでファイルを開いていたとか、何かのリソースを予約している場合、そのリソースの破棄の処理をデストラクタに書いたりします。
⑥classの継承とは
classは「継承」できます。耳慣れない言葉が出てきたと思ったかもしれません。「継承」とは、「より一般的なclassを受け継いで、より専門的なclassを作ること」です。
SimpleDataを「継承」してclassを作ってみましょう。
class ComplexData(SimpleData):
def __init__(self):
super().__init__()
self.c = 1
def __init__(self, a, b):
super().__init__(a, b)
self.c = 1
def sum(self):
return self.a + self.b + self.c
一気に難しくなりました。一つひとつ解説します。
def __init__(self):
super().__init__()
self.c = 1
は引数なしでインスタンス化されたとき呼ばれるコンストラクタです。super()でSimpleDataのメソッドが呼べます。ここではSimpleDataの引数なしのコンストラクタを呼んでいます。
そしてその後、cに1をセットします。
def __init__(self, a, b):
super().__init__(a, b)
self.c = 1
は引数ありでインスタンス化されたとき呼ばれるコンストラクタです。
ここではSimpleDataの引数ありのコンストラクタを呼び、cに1をセットしています。そう、このComplexDataでは、cの値を外から初期化することはできないのです。
def sum(self):
return self.a + self.b + self.c
は合計値を返すメソッドです。SimpleDataのaとb、自身のcを合計して返しています。setが見当たりませんね。
data3 = ComplexData()
data3.set(1, 2)
と書いたとき、cの値はどうなるのでしょう?実は、このときはSimpleDataのsetが呼ばれて、aとbがセットされるだけなのです。cの値を変えるメソッドはどこにもありません。
これが、「不変項としてcを付加した特別なSimpleData」であるComplexDataという「特別なクラス」です。継承はこのようにやや難しいですが、実際のPythonのプログラミングではよく登場します。押さえておいていください。
4.Pythonでclassを扱う際の注意点やコツ
classは何段階でも継承ができますが、あまり細かい単位で変更して継承を繰り返すと、継承元がどこまでも遡れるようになり、どのクラスのデータを参照しているのか、どのクラスのメソッドが呼ばれているのか分からなくなります。これを「継承地獄」と言います。本当に処理が変わったときにのみ継承するようにした方がいいでしょう。
また、名前の付け方ですが、Pythonの命名規則では変数名や関数名は
xxxx_xxxx_xxxx
というように小文字のアルファベット・数字を「_」でつなげて記述するのが推奨されていますが、クラスは
XxxxXxxx
というように大文字のアルファベットで単語を始めて残りは小文字にし、それをただ連結する記述が推奨されています。この命名規則に従うことを推奨します。