1 構造化プログラミング
プログラミングパラダイムの流れ
プログラミング言語の流れ
│
├─ 機械語: CPUが直接理解できる命令の集合
│
├─ アセンブリ: 機械語を人間が読めるように表現した低水準言語
│
└─ 高級言語: 人間に理解しやすい形式で記述できるプログラミング言語
│
├─ 構造化言語: 手続き型原則: GOTO文廃止,モジュール化,抽象化,情報隠蔽
│ └─ 代表例: C, Pascal
│
├─ オブジェクト指向 (OOP): カプセル化,継承,ポリモーフィズム
│ └─ 代表例: Python, Java, C++
│
└─ 関数型言語: 関数を第一級の概念として扱う,状態を持たない設計
└─ 代表例: Haskell, Lisp, Scala
- Pythonでも関数型言語の仕組みを
map, lambdaで実装したりしているので,あくまで代表例は目安
構造化プログラミング
Definition 1.1 構造化プログラミング
- 正しく動作するプログラムを作成するためには,わかりやすい構造にすることが重要というプログラミングスタイル
- Edsger Dijkstra が提唱した “Go To Statement Considered Harmful” に端を発する
構造化プログラミングは「GOTO文」を廃止して,ロジックを
- 順次進行:上から下へ順番に処理を進める
- 条件分岐:
if文などで処理を分岐させる - 繰り返し:
for文やwhile文で処理を繰り返す
の3つの構造だけで表現することを提唱したことで有名です.これらの基本構造は,構造化プログラミングの原則を適用することで生まれる手法であり,その背景には次の3つの基本原則があります:
- Modularity(モジュール化): プログラムを独立した部品(モジュール)に分割することで.理解・管理・保守を容易にする
- Information Hiding(情報隠蔽): モジュール内部の実装を外部から隠すことで,安全に再利用できるようにする
- Abstraction(抽象化): 複雑な内部処理を隠し,ユーザーや他のモジュールが必要な操作や概念だけに集中できるようにする
モジュール化
Definition 1.2 モジュール化
- Modularity は大規模コードを小さく独立した部品に分解し,管理しやすくする設計原則
- 1つのモジュールは50行程度以内 に収め,1つの明確なタスクだけを実行することが理想
とある ipynb 形式ファイルにて
X = load_data()
X = preprocess_data(X)
model = LinearRegression()
model.fit(X, y)
y_pred = model.predict(X)
plot_results(y, y_pred)
evaluate_model(y, y_pred)という実行セルがあるとします.データのロード,前処理,モデルフィット,予測,評価が一つのセルに詰め込まれており, また,グローバル変数 X はline-by-lineの実行状態に応じて変化しており,どの処理でどの値が生成されたのか追跡しにくくなっています(=Observability,可観測性の低下).
モジュール化とObservabilityの改善
モジュール化原則を適応する場合,データのロード,前処理,モデルフィット,予測,評価をそれぞれの関数 or クラスに分けて各処理の状態を明示的に観測できるようにすべきとなります.一例として
def load_and_preprocess(path):
X = load_data(path)
print("Data loaded:", X.shape)
X = preprocess_data(X)
print("Data preprocessed:", X.shape)
return X
def train_model(X, y):
model = LinearRegression()
model.fit(X, y)
print("Model trained:", model.coef_)
return model
def evaluate_model(y_true, y_pred):
metrics = compute_metrics(y_true, y_pred)
print("Evaluation metrics:", metrics)
plot_results(y_true, y_pred)
return metrics
# data load
X = load_and_preprocess("data.csv")
# model fit
model = train_model(X, y)
# predict
y_pred = model.predict(X)
# evaluate prediction
metrics = evaluate_model(y, y_pred)とすることで
- 各ステップで出力・状態が観測可能
- エラー発生時にどこで問題が起きたか特定しやすい
- 分析結果の追跡・再現性が向上
が実現できます.
Information Hiding
Definition 1.3 Information Hiding
- 利用者から内部の実装や詳細を隠す設計思想
- モジュールを利用するユーザーは,内部の実装や複雑な処理の詳細を知らなくてもよい
- 利用者が不用意に内部に手を加えられないことで,開発者が想定していない変更やバグの混入を防ぐ
from sklearn.linear_model import LinearRegression
# create Linear Regression Model instance
model = LinearRegression()
# data fit
model.fit(X_train, y_train)
# predicttion
y_pred = model.predict(X_test)という分析コードが与えられた場合
- このクラスは線形回帰を行う
- fit にデータを渡すと学習ができる
- predict に新しいデータを渡すと予測値が得られる
ということが分析利用者はわかれば良いとなります.一方,
- 内部で
np.linalg.pinvがどのように動作しているか - 行列計算の最適化やメモリ効率の詳細
- ライブラリ側で行われている微小な数値処理の実装
といった詳細はブラックボックスとして隠されており,利用者は気にせずに使用できます.また,fit メソッドの中身はクラス内部に隠蔽されているため,
- 想定外の操作の防止
- 開発者が内部実装を変更しても,ユーザー目線でのインターフェースの影響範囲は限定的
という利点が生まれます.
Information Hidingの技術としてのローカル変数
Information Hiding(情報隠蔽)を実現する基本的な手段のひとつがローカル変数の活用です.
Definition 1.4 ローカル変数
- サブルーチンの中だけで使われる変数
- 外部から直接アクセスできないため,内部の状態や計算手順を隠すことが可能になる
- サブルーチンから抜けるときには消える性質を持つ
例として,
def train_model(X, y):
# ローカル変数 model は関数外から見えない
model = LinearRegression()
model.fit(X, y)
return model # 必要な情報だけを返す- 関数内部の model は外部からアクセス不可 → 情報隠蔽
- 利用者は
train_model(X, y)を呼ぶことで学習済みモデルを取得できるが,内部の計算手順には触れない
というメリットを享受することが出来ます.
Information Hiding をローカル変数や関数の形で実現しても,
- 分析コード全体として構造化プログラミングが意図通り機能する
- モジュール間の安全な連携
を実現するためには,各モジュールや関数が守るべきルールや前提条件を明確にしておく必要があります.ここでいう「ルール」や「前提条件」がプロトコルやデータ契約(Data Contract) に相当します.
| 概念 | 説明 | 具体例 |
|---|---|---|
| プロトコル | モジュールや関数間のやり取りにおける操作の約束事 | train_model(X, y) は必ず DataFrame 型 X と Series 型 y を受け取り,学習済みモデルを返す |
| データ契約 | 入力データ・出力データの形式や意味を明確に定義 | - X は欠損値なしの float 型列を持つ DataFrame- y は 1 次元配列 - 戻り値のモデルは predict() メソッドを持つ |
Abstraction
Definition 1.5 抽象化
- 複雑な処理の本質的な機能だけを提示し,不要な詳細を隠す設計思想
- 利用者は「何をするか」に注目し,内部のアルゴリズムや処理手順の複雑さを意識する必要はない
from sklearn.linear_model import LinearRegression
# create Linear Regression Model instance
model = LinearRegression()
# data fit
model.fit(X_train, y_train)
# predicttion
y_pred = model.predict(X_test)上記の分析コードを例にすると
- 「線形回帰モデルを学習して予測できる」という本質的な操作が抽象化されている
- 利用者は
fitに学習データを渡し,predictで予測値を得るというインターフェースだけ意識すれば良い
これらより,利用者の理解負荷を軽減を実現することが出来ます.
グローバル変数問題: 構造化プラグラミングで残された課題
構造化プログラミングの浸透により,GOTO文の廃止や順次進行・条件分岐・繰り返しの明確化によってコードの可読性や保守性は大幅に向上しました. しかし,グローバル変数の扱いは依然として課題として残ってしまっています.
グローバル変数はなぜ問題なのか?
グローバル変数はどこからでも参照・更新できるため,
- プログラムの実行順序や状態に依存したバグが生じやすい
- 特にNotebook形式や長大なスクリプトでは意図せぬ状態変化を招きやすい
- 関数やモジュール間でのデータの受け渡しが暗黙的になることで,コードの追跡やテストが困難になる
という問題を引き起こします.構造化プログラミングではサブルーチンを活用したモジュール化を取り入れたため,一部はローカル変数の活用により改善されましたが, ローカル変数はサブルーチンの実行が終わると消えるため,関数間で一連の処理(いわゆるパイプライン)を構築する場合は, 依然としてグローバル変数に頼らざる得ないケースが残ります
OOPによる解決
こうした限界を打破したのが,クラス(カプセル化)を導入したオブジェクト指向プログラミング(OOP)となります. クラスがデータと処理をひとつにまとめ,状態の管理や情報隠蔽を容易にすることで,グローバル変数への依存を最小化できるようになりました.
また,継承 によって既存クラスの機能を引き継ぎつつ拡張が可能であり, ポリモーフィズムにより異なるクラスでも共通のインターフェースで扱えるため, 異なるコンテキストで同じパターンを柔軟に再利用できるようになりました. これらにより,構造化プログラミングではサブルーチンの再利用にとどまっていた再利用性が大幅に向上したという流れがあります.