はじめに
こんにちは!最近Pythonを体系的に学び直しているのですが、その中で「プログラミングパラダイム」について理解が浅かったと感じ、改めて整理してみました。
「オブジェクト指向」はよく耳にしますが、それ以外はよく知らない、、、という方も多いのではないでしょうか?実はPythonはそれだけではなく「手続型」や「関数型」の書き方もできる、とても柔軟なマルチパラダイム言語です。
本記事では、Pythonにおける主要な3つのパラダイム、手続型・関数型・オブジェクト指向について紹介し、それぞれで同じ機能を実装して比べてみたいと思います!
プログラミングパラダイムとは?
プログラミングパラダイムとは、簡単にいうとプログラミングのスタイル、手法のことです。
代表的なパラダイムは以下のようなものがあります:
- 手続型(Procedural):処理を順番に書いていくスタイル
- 関数型(Functional):関数を組み合わせて処理するスタイル
- オブジェクト指向(OOP):データと処理を"オブジェクト"としてまとめるスタイル
言語によって得意、不得意ありますが、Pythonはこの3つ全てに対応しています!すごい!
各パラダイムの特徴とPythonでの書き方
以下に、代表的な3つのパラダイム(手続型、関数型、オブジェクト指向)の特徴と、同じ処理(リストから偶数を取り出して2倍にする)を実装した時の書き方に違いがあるか見ていきましょう。
1.手続型プログラミング
特徴
手続型は、プログラムを「手順(手続的)に沿って書く」最も基本的な書き方です。
numbers = [1,2,3,4,5,6]
result = []
for num in numbers:
if num % 2 == 0:
result.append(num * 2)
print(result) # [4, 8, 12]
このように、状態(この例ではresultリスト)を変化させながら、少しずつ目的の目的の答えに近づけていくのが特徴です。
手続型の落とし穴
- 状態を管理する変数が、プログラムの様々な場所から変更できてしまうため、「副作用」が起きやすい
- プログラムが大きくなると、どこで状態が変わっているか把握しづらくなることがある
2.関数型プログラミング
特徴
関数型プログラミングの起源は「ラムダ計算(λ計算)」という数学的な理論です。
この考えに基づいてできているため、以下のような特徴を持ちます。
- 状態を持たない: データを変数に格納して変更していくのではなく、新しいデータを生成する
- 参照透過性: 関数の動作は「引数」のみに依存する(同じ入力からは、必ず同じ出力が得られる)
- 関数の戻り値を他の関数に渡すことで処理を繋いでいく
numbers = [1,2,3,4,5,6]
# filterで偶数を取り出し、mapで2倍に変換する
result = list(map(lambda x: x * 2, filter(lambda x: x % 2 == 0, numbers)))
print(result) # [4, 8, 12]
lambdaを使わず、通常の関数定義を使って書くと以下のようになります。
def is_even(num):
return num % 2 == 0
def double(num):
return num * 2
numbers = [1, 2, 3, 4, 5, 6]
result = list(map(double, filter(is_even, numbers)))
print(result) # [4, 8, 12]
状態を保持する変数への再代入やループは登場せず、純粋な関数の組み合わせだけで処理が完結しています。
Pythonらしい書き方:リスト内包表記
実は、Pythonではリスト内包表記を使うことで、mapやfilterのような処理をより直感的に書くことができます。
numbers = [1, 2, 3, 4, 5, 6]
result = [num * 2 for num in numbers if num % 2 == 0]
print(result) # [4, 8, 12]
手続型のforループに似ていますが、新しいリストを一度に生成しており、途中の状態を変化させないという点で、関数型プログラミングに近い考え方と言えます。
3.オブジェクト指向プログラミング
特徴
オブジェクト指向も、関数型と同様に手続型が抱える状態管理の問題を解決しようとしますが、アプローチが異なります。
- 状態はオブジェクト自身に持たせる
- データ(属性)と処理(メソッド)をひとまとめにする(カプセル化)
- クラスという設計図から**オブジェクト(インスタンス)**を生成して利用する
class NumberProcessor:
# __init__は、オブジェクト作成時に最初に呼ばれる特別なメソッド(初期化処理)
def __init__(self, numbers):
# selfはオブジェクト自身を指す。self.numbersのようにして、オブジェクトにデータを保存する
self.numbers = numbers
def process(self):
return [x * 2 for x in self.numbers if x % 2 == 0]
# NumberProcessorクラスから、具体的な処理を行うオブジェクトを生成
processor = NumberProcessor([1, 2, 3, 4, 5, 6])
# オブジェクトに処理を依頼する
print(processor.process()) # [4, 8, 12]
関連するデータと処理をNumberProcessorという一つの「モノ」に集約することで、プログラムの見通しが良くなり、機能の追加や修正が容易になります。例えば、「奇数を3倍する」機能を追加したくなった場合も、このクラスに新しいメソッドを追加するだけで対応でき、他の部分への影響を最小限に抑えられます。
複数のクラスを定義し、オブジェクト同士が連携することで、システム全体を構成していくのがオブジェクト指向プログラミングのスタイルです。
3つのスタイルのまとめと比較
| パラダイム | 状態の扱い方 | 長所 | 短所 |
|---|---|---|---|
| 手続型 | 手続きの中で変数の値を直接変更していく | シンプルで直感的。小規模な処理に向く | プログラムが複雑になると、状態管理が煩雑になりやすい |
| 関数型 | 状態を持たず、関数の入力(引数)と出力(戻り値)でデータを扱う | 副作用がなくテストが容易。処理の組み合わせで再利用性が高い | 慣れるまでは読みにくい。すべてを関数で表現しようとすると、かえって処理の流れが追いにくくなることがある |
| オブジェクト指向 | データとそれに関連する処理を「オブジェクト」としてカプセル化し、状態を管理する | 現実世界のモデル化と相性が良く、大規模開発での保守性・拡張性が高い | 設計が複雑になりがちで、小規模なプログラムには過剰な場合がある |
終わりに
今回Pythonを用いて3つのプログラミングパラダイムをまとめてみました!Pythonはこれらすべてのパラダイムを自然な文法で書ける柔軟な言語です。
「こうしなきゃいけない」という縛りがない分、どのパラダイムが優れているかではなく、解決したい問題に応じて最適なスタイルは何かを考え、意図的に使い分けることができれば、エンジニアとして1ランクアップできると思います。
まずは自分が書きやすい手法から始めて、少しずつ他のパラダイムにも触れていくのがおすすめです!
おまけ
この記事で書いたコードをまとめておきます。
# --- 1. 手続型 ---
numbers = [1, 2, 3, 4, 5, 6]
result_procedural = []
for num in numbers:
if num % 2 == 0:
result_procedural.append(num * 2)
print(f"手続型: {result_procedural}")
# --- 2. 関数型 ---
# その1: lambda式
numbers = [1, 2, 3, 4, 5, 6]
result_functional_1 = list(map(lambda x: x * 2, filter(lambda x: x % 2 == 0, numbers)))
print(f"関数型 (lambda): {result_functional_1}")
# その2: 通常の関数定義
def is_even(num):
return num % 2 == 0
def double(num):
return num * 2
numbers = [1, 2, 3, 4, 5, 6]
result_functional_2 = list(map(double, filter(is_even, numbers)))
print(f"関数型 (def): {result_functional_2}")
# その3: リスト内包表記
numbers = [1, 2, 3, 4, 5, 6]
result_list_comprehension = [num * 2 for num in numbers if num % 2 == 0]
print(f"関数型 (リスト内包表記): {result_list_comprehension}")
# --- 3. オブジェクト指向 ---
class NumberProcessor:
def __init__(self, numbers):
self.numbers = numbers
def process(self):
return [x * 2 for x in self.numbers if x % 2 == 0]
processor = NumberProcessor([1, 2, 3, 4, 5, 6])
result_oop = processor.process()
print(f"オブジェクト指向: {result_oop}")