今回はプログラミング初心者向けに
オブジェクト指向プログラミング(OPP)について解説します。
OOPはソフトウェア開発で広く使われている設計手法で、
プログラムの整理や保守性を高めるのに役立ちます。
オブジェクトって何?というかたも大丈夫です。
1歩づつ理解を深めていきましょう!
OOPを極めて、開発のレベルを上げていこう!
オブジェクト指向プログラミングとは?
オブジェクト指向プログラミングとは、
ある特定の目的に沿ったデータとそのデータに関する処理を「オブジェクト」
という形にまとめるプログラミング方法です。
例えば、家に関するデータと処理をまとめるとします。
家に関するデータとしては築年数・住所などを定義して、
処理には「築年数から建て替えが必要かを判定する処理」や「住所からどこの国かを調べる処理」
を書くこともできるでしょう。
このように、データとそのデータに関する処理をまとめて書くことで、
読みやすいプログラムにもつながってきます。
オブジェクトの作り方
オブジェクトは「ある目的に沿ったデータと処理をまとめたもの」であると説明しました。
オブジェクトを作るためには、何のデータを持つか?どんな処理を持つのか?
を定義する”クラス”と呼ばれる設計図が必要になります。
ざっくりと以下のようなイメージです。
- クラス
オブジェクトを作るための設計図。
まとめて管理するデータと処理を定義したもの。
- オブジェクト
クラスから作った実際にデータを持った実体。
築年数のデータをクラスで定義したなら、50年など具体的な値を持ったもの。
以下に実際のプログラムを記載します。
家に関するデータとして「築年数」と「築年数から建て替えが必要かを判定する処理」
を定義したクラスとそのクラスから築年数が100年の家のオブジェクトを作った例です。
# 家クラス.
class House:
def __init__(self, age): # 初期化メソッド.
self.age = age # 家の築年数というデータ.
def is_rebuild_needed(self): # 家の建て替えが必要か判定する処理.
if self.age > 50: # 築50年以上だったら建て替えが必要.
print(f"建て替えよう!")
else:
print(f"まだ必要ないよ!")
# オブジェクトの作成.築年数の100をデータとして渡す.
houseA = House(100)
クラスについて、より細かい説明が欲しいかたは以下を参考にしてみてください!
オブジェクト指向の4要素とは?
オブジェクト指向には、4つの大きな要素があります。
それぞれについて、チェックしていきましょう!
- カプセル化
- 抽象化
- 継承
- ポリモーフィズム
聞きなれない言葉ばかりですよね。難しい言葉使いよって~
1.カプセル化
カプセル化は、クラスで管理するデータに外部から直接アクセスできないようにすることです。
これにより、データを意図せずに変更してしまう事態を防ぐことが可能です。
また、直接ではなくメソッドを通じての変更は可能です。
これにより、必ずチェックを入れてから、データを変更するという流れを作ることができます。
例えば、家クラスで築年数の設定にチェックを追加した場合は以下の通りです。
# 家クラス.
class House:
def __init__(self, age): # 初期化メソッド.
self.__age = age # 家の築年数という性質(属性). __をつけることでプライベート属性にする.
@property
def age(self):
return self.__age
# ageに値をセットするときに呼ばれるメソッド.
@age.setter
def age(self, age):
if 0 <= age:
self.__age = age
else:
raise ValueError("値がおかしいよ!")
# オブジェクトの作成.
houseA = House(20) # houseAという実体を作る.
houseA.age = -30 # 値のチェックが入り、"値がおかしいよ!"とエラーが出力.
具体的な実装方法としては、保護したいデータに対して
①データにプライベート属性をつけて、外部から直接アクセスをさせないこと
②セッター・ゲッターという間接的にデータの変更・取得ができるメソッドを作ること
の2つの要素を追加します。
設定したいデータが不正な値でないかチェックしたい場合は、
セッターの中にチェックを入れましょう。
以上により、意図しないデータの変更を防ぎ、より正確なデータ管理を行うことができます。
2.継承
継承とは、特定のクラスのデータや処理を引き継ぐ仕組みです。
引き継ぎ元を親クラス、引き継ぎ先を子クラスと呼びます。
- 親クラス(基底クラス、スーパークラス)
全てのデータと処理が子クラスに引き継がれる。
子クラスで共通して使う項目を書く。
- 子クラス
親クラスからデータと処理を引き継ぐ。
加えて、オリジナルのデータや処理を追加することが可能。
以上のように、子クラスは親クラス+オリジナルの設定を加えることができるため、
子クラスは親クラスよりも具体的な情報を持つクラスになります。
以下は親クラスとして動物クラス、子クラスとして猫クラスと犬のクラスを書いた例です。
【親クラス】
#### 親クラス ####
# 動物クラス.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement this method")
【子クラス】
#### 子クラス ####
# 犬クラス (動物クラスを継承).
class Dog(Animal):
def __init__(self, name):
super().__init__(name) # 親クラスのコンストラクタを呼び出す.
def speak(self):
print(f"ワン!")
# 猫クラス (動物クラスを継承).
class Cat(Animal):
def __init__(self, name):
super().__init__(name) # 親クラスのコンストラクタを呼び出す.
def speak(self):
print(f"にゃーお!")
# 動作確認.
dog = Dog("いぬぅ")
dog.speak() # "ワン!"が表示される.
cat = Cat("ぬぅこぉ")
cat.speak() # にゃーお!"が表示される
親クラスは通常のクラスの定義と同様の書き方をし、
子クラスは継承元のクラス名を書くことで親クラスを設定することができます。
また、speakメソッドのように親クラスと同名のメソッドを子クラスに作ると、
処理を上書きすることができます(オーバーライド)。
このように共通するものを親クラス、独自のものを子クラスに集める事で、
より整理されたプログラムを書くことにもつながります。
お店の商品を種類分けしてクラスにするなら、どうなるかな?
と考えるのも設計の練習になって面白いですよ!
3.抽象化
抽象化とは、具体的な実装を書かないでとりあえず定義だけ作っておく方法です。
継承と共に使った場合では、
親クラスでメソッドの定義を作って中身の処理を書かず、
子クラスで同じメソッド名を定義して具体的な処理を書くといったイメージです。
型だけ用意しておくから、中身は継承先で書いてね!という手法のため、
子クラスで定義を書かないとエラーになるので、絶対に定義を書かせるということができます。
これにより、共通の処理を定義することができ、
よりメンテナンスや拡張がしやすいプログラムになります。
【抽象クラス】
from abc import ABC, abstractmethod
#### 抽象クラス ####
# 動物クラス.
class Animal(ABC):
# 鳴き声を表示するメソッド.中身は何も書かない.
@abstractmethod
def speak(self):
pass
【継承先のクラス】
#### 子クラス ####
# 犬クラス.
class Dog(Animal):
# 鳴き声を表示するメソッド(中身も定義する).
def speak(self):
print(f"ワン!")
# 猫クラス.
class Cat(Animal):
# 鳴き声を表示するメソッド(中身も定義する).
def speak(self):
print(f"にゃーお!")
動物クラスを抽象クラスにして、鳴き声を表示するメソッドであるspeakを定義した例です。
子クラスとして、犬クラスと猫クラスを用意しています。
speakを継承先のクラスで中身を指定することで、
犬クラスと猫クラスの固有の動きを指定することができています。
また、継承とは違い、抽象化したメソッドは子クラスで定義を書かないとエラーになります。
今回であれば犬クラスか猫クラスのspeakの定義を削除すると確認できます。
継承のオーバーライドは一部の子クラスの処理だけが違う場合、
抽象化は全ての子クラスで固有の処理を定義したいときに使っていきましょう。
複数人で実装すると、メソッド名がバラバラになりがちです。
先に抽象化した定義を入れておくと名前を統一できるので見やすいよ!
4.ポリモーフィズム
ポリモーフィズム(多態性)とは、同じ名前の処理であっても中身が異なることです。
OOPでは、クラス間で同じ名前のメソッドがあっても中身が異なることを指します。
抽象化も子クラス間で同じ名前のメソッドを作り、
中身の挙動はそれぞれ違うものを定義できることから、ポリモーフィズムのベースになります。
抽象化と違う点では、継承を使わなかったり、別の継承元であっても、
同じ名前のメソッド名であればポリモーフィズムとなります。
例えば、自動車・動物・食器のクラスでそれぞれの名称を取得するメソッドを
get_name() という名前で同じにすれば条件を満たします。
これにより、異なるクラス間であっても同じような扱いをすることができ、
柔軟な実装設計を行うことにもつながります。
【クラス定義】
親クラスに動物クラスと子クラスに犬クラスと猫クラス、
それに加えて動物とは関連のない車のクラスを作成しています。
from abc import ABC, abstractmethod
#### 親クラス ####
# 動物クラス.
class Animal(ABC):
@abstractmethod
def get_name(self):
pass
# 車クラス
class Car():
def get_name(self):
return 'くるま'
#### 子クラス ####
# 犬クラス.
class Dog(Animal):
def get_name(self):
return "いぬぅ"
# 猫クラス.
class Cat(Animal):
def get_name(self):
return "ぬぅこ"
【ポリモーフィズムを利用した関数】
インスタンスを引数にし、get_nameのメソッドを呼び出す関数です。
# 名前を表示する関数.
def print_name(class_instance):
print(class_instance.get_name())
【使用例】
継承を使った犬・猫クラスと関係のない車のクラスからインスタンスを作り、
print_nameの関数を使用しています。
# 犬と猫のインスタンスを作成.
dog = Dog()
cat = Cat()
# 車のインスタンスを作成.
car = Car()
# それぞれの鳴き声を出力
print_name(dog) # いぬぅ"
print_name(cat) # "ぬぅこ"
print_name(car) # "くるま"
犬・猫・車などオブジェクトの具体的な型を意識することなく、同じメソッドを使って処理する事が可能です。
新しい動物の種類を増やしたときにも、get_nameのメソッドを定義すると、
print_nameの関数で同じように実行する事ができます。
異なるクラスのオブジェクトを統一的に扱うことができることで、
コードの柔軟性と再利用性の向上につながります。
同じ名前のメソッドを使うメリットですね!
メリット・デメリット
オブジェクト指向のメリット・デメリットを紹介します。
どのように使うべきかを判断するための土台にしていきましょう。
メリット
メリットを4つ紹介します。
- 再利用性の向上
- 保守性の向上
- 直感的な実装設計
1.再利用性の向上
OPPはコードを再利用できる特徴をいくつか持っています。
例えば、クラスという設計図から何個でも同じデータ構造のオブジェクトを生成でき、
継承により共通の処理を親クラスにまとめることで
同じプログラムを何回も書く必要がなくなります。
また、別のプログラムで使用したい機能があれば、
クラス単位での移植は簡単にできるでしょう。
2.保守性の向上
保守性とは修正・変更・拡張が簡単にできるのか?という性質です。
この要素は長期的に運営するサービスの場合では重要な項目になります。
継承や抽象化でメソッド名を共通にすることで読みやすくなったり、
クラス単位でデータと処理をまとめることにより、個別に機能を拡張することも簡単です。
3.直感的な実装設計
オブジェクト指向では、現実にあるモノや概念をそのままプログラムに落とし込むことができます。
例えば、家のクラスを作るときも築年数などの現実にあるパラメータを持たせることができます。
継承も親を動物クラスにして子に犬クラスを作るときには、
現実にある動物という枠に犬という種類がいる情報をそのまま使うことができました。
現実にあるものをそのまま落とし込める設計は、考えやすく実装しやすいということですね。
簡単にまとめるとプログラムを整理でき、
慣れると実装しやすい手法ということですね!
デメリット
デメリットについても3つ紹介します。
- 学習難易度が高い
- 実装設計の複雑化
- パフォーマンスの低下
1.学習難易度が高い
OPPは学習することが多く、初心者には難しく感じる点も多くあります。
直感的な実装設計ができる点を活かして、
まずは現実のモノや概念を例にとって、実装のイメージを作ると理解が進みやすくなるでしょう。
2.実装設計の複雑化
便利な点の多いOPPですが、複雑なプログラムを生み出してしまうこともあります。
例えば、継承を50回も繰り返したプログラムを読みたいと思いますか?
きっと読むのを諦めて見なかったことにするでしょう。
複雑化して直すのも面倒になる前に、見直す機会をつくってみましょう。
3.パフォーマンスの低下
OPPを使うと処理速度が遅くなったり、メモリを使いすぎたりなどの
パフォーマンスを低下させるデメリットがあります。
例えば、オブジェクトの生成時にはコンストラクタによる初期化処理が呼び出されますし、
メソッドはクラスと結び付けて管理する分のメモリのコストも発生します。
小規模なプログラムで問題になることは少ないですが、
便利な書き方ができる分のコストも発生する点は把握しておきましょう。
メリットは大きいですが、デメリットもちゃんと存在することは
知っておきましょう!
まとめ
今回はオブジェクト指向プログラミングについて解説しました。
理解するまで時間のかかる手法ですが、習得したときのメリットも大きなものがあります。
特に企業などのチームで大きなプログラムを作る際には、
OPPの何かしらの要素は絶対と言って良いほど使用しています。
プログラミング技術のレベルアップのためにも、じっくりと学んでいきましょう!
OPPを習得する前と後で作ったプログラムを見比べると、
書き方がガラリと変わるので面白いですよ~