趣味のPython・深層学習

中級者のための実装集

ダックタイピングとは?Pythonでわかりやすく解説

ダックタイピング

プログラミングの世界には、様々な概念や原則があります。その中でも「ダックタイピング」は、Pythonをはじめとする動的型付け言語で広く活用されている概念です。今回は、ダックタイピングの意味と使い方を、わかりやすい実例を交えて解説していきます。

ダックタイピングとは

ダックタイピングとは、オブジェクトの実際の型ではなく、そのオブジェクトが持つメソッドやプロパティに基づいてオブジェクトを扱う概念です。この考え方の由来となった有名な言葉が「もしある生き物が、アヒルのように歩き、アヒルのように水面を泳ぎ、アヒルのように鳴くならば、その生き物はアヒルである」というものです。 つまり、ダックタイピングでは、オブジェクトの型を気にするのではなく、そのオブジェクトが持つ振る舞い(メソッド)に注目するのです。

ダックタイピングの例

ダックタイピングの概念をよりよく理解するために、具体的な例を見ていきましょう。

class Duck:
    def quack(self):
        print("ガーガー")

    def feathers(self):
        print("ふわふわの羽")

class Person:
    def quack(self):
        print("がーっ!") 

    def feathers(self):
        print("人間には羽がない")

def in_the_forest(obj):
    obj.quack()
    obj.feathers()

# ダックタイピングの例
donald = Duck()
in_the_forest(donald)
# 出力
# ガーガー
# ふわふわの羽

john = Person()
in_the_forest(john)
# 出力
# がーっ!
# 人間には羽がない

上記の例では、DuckクラスとPersonクラスの両方にquackメソッドとfeathersメソッドが実装されています。in_the_forest関数は、受け取ったオブジェクトに対してquackとfeathersメソッドを呼び出します。 ダックタイピングの概念に従えば、in_the_forest関数は、渡されたオブジェクトの実際の型を気にすることなく、そのオブジェクトがquackとfeathersメソッドを持っているかどうかだけを確認します。そのため、DuckオブジェクトとPersonオブジェクトの両方が適切に処理されます。

ダックタイピングのメリット

ダックタイピングには、以下のようなメリットがあります。

コードの拡張性が高い - 新しいクラスを作成する際、既存のコードを変更せずに機能を拡張できます。新しいクラスが必要なメソッドを持っていれば、既存のコードで扱えます。 柔軟性が高い - オブジェクトの実際の型を気にする必要がないため、さまざまな型のオブジェクトを同じコードで扱うことができます。 duck typing is pythonic - ダックタイピングは、Pythonの設計思想と合致しています。Pythonでは、オブジェクト指向プログラミングの原則を厳格に適用するのではなく、ある程度の柔軟性が許容されています。

例: scikit-learnにおけるダックタイピング

scikit-learnは、Python機械学習ライブラリの1つで、ダックタイピングの概念が広く活用されています。具体例を見ていきましょう。

from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 疑似的なデータセットを用意
X = [[1, 2], [3, 4], [5, 6], [7, 8]]
y = [0, 1, 0, 1]

# データセットを訓練用とテスト用に分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# ロジスティック回帰のモデルを作成して訓練
log_reg = LogisticRegression()
log_reg.fit(X_train, y_train)

# 決定木のモデルを作成して訓練 
rf_clf = RandomForestClassifier()
rf_clf.fit(X_train, y_train)

# テストデータで評価
log_reg_pred = log_reg.predict(X_test)
accuracy_log_reg = accuracy_score(y_test, log_reg_pred)
print(f"Logistic Regression Accuracy: {accuracy_log_reg}")

rf_clf_pred = rf_clf.predict(X_test)  
accuracy_rf_clf = accuracy_score(y_test, rf_clf_pred)
print(f"Random Forest Accuracy: {accuracy_rf_clf}")

この例では、2つの異なる機械学習アルゴリズム(ロジスティック回帰と決定木)を使用しています。それぞれのモデルクラス(LogisticRegressionとRandomForestClassifier)は、一見すると全く異なるものに見えます。 しかし、これらのモデルクラスは共通のインターフェース(fitメソッドとpredictメソッド)を持っています。fitメソッドはモデルを訓練データで学習させ、predictメソッドは新しい入力データに対する予測を行います。 scikit-learnのコードでは、これらの共通のインターフェースに基づいて、モデルを扱うことができます。実際にどのようなアルゴリズムなのかを気にする必要はありません。このようにダックタイピングが活用されているのです。

GNNはそもそも何?

グラフニューラルネットワークの核心 - グラフ構造を活かしたノード表現の獲得

こんにちは。今回はグラフニューラルネットワーク(GNN)について、その基礎から核心にわたって詳しく解説していきます。グラフデータの構造情報を効果的に活用する手法であるGNNの仕組みを、具体例を交えながらわかりやすく説明していきますので、ぜひ最後までお付き合いください。

グラフデータとは

まずはグラフデータの構造から見ていきましょう。グラフデータは「ノード(節点)」と「エッジ(辺)」から構成されています。ノードは実体を表し、エッジはノード同士の関係を表します。 例えば、ソーシャルネットワークのグラフデータであれば、ノードが個人を、エッジが個人間の友人関係を表します。また、分子構造のグラフデータであれば、ノードが原子を、エッジが原子間の結合を表すことになります。このように、様々なデータをグラフ構造で表現することができます。 グラフデータの重要な特徴は、「ノード同士が関係性をもつ」ということです。これは従来の機械学習で扱われてきたデータ(画像の画素値、文書の単語ベクトルなど)とは異なり、データ間の依存関係があることを意味します。

従来の機械学習ではグラフ構造を失う

従来の機械学習アルゴリズムでは、このようなグラフ構造のデータを適切に扱うことができませんでした。例えば画像認識タスクでは、画像をまず画素値の集まり(ベクトル)に変換してからニューラルネットワークに入力します。この時、画素値同士の関係性(構造)は失われてしまいます。すべての画素は独立したデータとして処理されることになります。 同様に、文書分類などの自然言語処理タスクにおいても、文書はまず単語のベクトル集合に分解され、単語間の順序関係(構造)が失われてしまいます。 つまり従来の機械学習では、構造化されたデータを単なるフラットなベクトル集合として処理をしていたため、構造の持つ重要な情報を失っていたのです。これがグラフデータを適切に扱えない理由でした。

メッセージパッシングによるグラフ構造の活用

ここでグラフニューラルネットワーク(GNN)が登場します。GNNは「メッセージパッシング」という仕組みにより、グラフ構造の情報を保ったままノードの特徴表現を獲得することができます。 メッセージパッシングのプロセスは以下のようになります。

ノードの初期表現を設定する

最初に、各ノードの初期表現ベクトルを設定します。ノード自身の属性値(特徴量)から生成するのが一般的です。 各ノードが近傍ノードの表現を集約(アグリゲーション)する 次に、各ノードは自分に隣接する近傍ノードの表現を集約します。具体的には、近傍ノードの表現の平均や最大値を取るなどの操作を行います。 例えば、ソーシャルネットワークのグラフであれば、あるユーザーノードは友人関係にあるユーザーノードの表現を集約することになります。

集約した情報と自身の表現を組み合わせ、新しいノード表現を生成する

ステップ2で集約した近傍ノードの情報と、自身のノード表現を組み合わせることで、新しいノード表現を生成します。組み合わせ方は全結合層などのニューラルネットワークモデルを使用します。 ステップ2-3を繰り返し、ノード表現を更新していく ステップ2-3のプロセス(近傍ノードの集約と自身の更新)を繰り返し行うことで、徐々にグラフ構造の情報がノード表現に反映されていきます。

つまり、初期のノード表現から出発し、近傍ノードの情報を次々と取り込んでいくことで、最終的にはグラフの構造を含んだ良質なノード表現が獲得できるというわけです。 例えば、あるユーザーノードの表現は、最初は自身の属性値だけから生成されています。しかしメッセージパッシングを繰り返すことで、友人ノードの情報、さらにはその友人の友人ノードの情報も取り込まれていき、最終的に自分を取り巻くネットワーク全体の構造を反映した表現となります。 従来手法では単に独立したベクトルとして処理されていたノードの情報が、メッセージパッシングによってグラフ構造の情報を反映するようになったのです。

グラフニューラルネットワークアーキテクチャ

具体的なグラフニューラルネットワークアーキテクチャとしては、GCN(Graph Convolutional Network)が有名です。GCNでは、メッセージパッシングの際に、ノード次数(ノードに接続するエッジの数)を用いてメッセージを正規化します。

def propagate(self, edge_index, x):
    # ノード次数を計算
    deg = ...
    
    # メッセージの正規化とアグリゲーション
    msg = x[col] / deg[col].unsqueeze(1) # ノード次数で割る
    x = msg.scatter_add_(0, row, msg)  # 受信ノードでメッセージ集約

これにより、自分に多くのエッジが接続するノード(重要度の高いノード)からのメッセージに重みをつけることができ、構造情報をより適切に取り込めるようになります。

グラフニューラルネットワーク(GNN)の基礎

グラフニューラルネットワーク(GNN)の基礎と実装

はじめに

グラフデータは非構造化データであり、従来の機械学習手法では適切に扱えませんでした。グラフニューラルネットワーク(GNN)はグラフデータを効果的に処理するためのニューラルネットワークアーキテクチャです。 GNNの基本的な考え方は、各ノードの表現(ノードベクトル)を、そのノード自身の特徴と近傍ノードの表現から生成することです。このプロセスをメッセージパッシングと呼び、ノード表現を繰り返し更新することで、グラフ全体の構造的情報が取り込まれていきます。 さっそく実装例を見ていきましょう。ここではCitationFullデータセットを使い、論文の分類タスクを行うGCN(Graph Convolutional Network)を実装します。

import torch
from torch_geometric.datasets import CitationFull
from torch_geometric.loader import NeighborLoader
from torch.nn import Linear
import torch.nn.functional as F

# データセットの読み込み
dataset = CitationFull(root='/tmp/CitationFull')
data = dataset[0]

# データをロード
print(f'Number of graphs: {data.num_graphs}')
print(f'Number of nodes: {data.num_nodes}')  
print(f'Number of edges: {data.num_edges}')
print(f'Number of features: {data.num_node_features}')
print(f'Number of classes: {data.num_node_labels}')

# データローダーの作成
loader = NeighborLoader(
    data,
    num_neighbors=[-1] * 2, 
    batch_size=64,
    shuffle=True
)

# GCNモデル定義
class GCN(torch.nn.Module):
    def __init__(self, in_channels, out_channels):
        super().__init__()
        self.lin1 = Linear(in_channels, 16)
        self.lin2 = Linear(16, out_channels)
    
    def forward(self, x, edge_index):
        x = self.lin1(x)
        x = F.relu(x)
        x = self.propagate(edge_index, x=x)
        x = self.lin2(x)
        return F.log_softmax(x, dim=1)
    
    def propagate(self, edge_index, x):
        n_nodes = x.size(0)
        row, col = edge_index
        deg = torch.zeros(n_nodes, device=x.device)
        deg = deg.scatter_add_(0, row, torch.ones_like(row, device=x.device))
        msg = x[col] / deg[col].unsqueeze(1)
        x = msg.scatter_add_(0, row, msg)
        return x

# モデルのインスタンス化
model = GCN(dataset.num_features, dataset.num_classes)

# 学習ループ
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
criterion = torch.nn.NLLLoss()

def train():
    model.train()
    total_loss = 0
    for batch in loader:
        optimizer.zero_grad()
        out = model(batch.x, batch.edge_index)
        loss = criterion(out[batch.batch], batch.y)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * batch.num_graphs
    return total_loss / len(loader)

@torch.no_grad()
def test():
    model.eval()
    accs = []
    for batch in loader:
        out = model(batch.x, batch.edge_index)
        pred = out[batch.batch].max(1)[1]
        correct = pred.eq(batch.y).sum().item()
        acc = correct / batch.batch_size
        accs.append(acc)
    return sum(accs) / len(accs)

for epoch in range(1, 21):
    loss = train()
    acc = test()
    print(f'Epoch: {epoch:02d}, Loss: {loss:.4f}, Acc: {acc:.4f}')

このコードでは、まずCitationFullデータセットを読み込み、ノード自身と1次、2次のノード近傍を含むデータローダーを作成しています。 GCNモデルは2つの全結合線形層と、メッセージパッシングを行うpropagate関数から構成されています。propagate関数ではノード次数を計算し、受信側のノードから送信されるメッセージをノード次数で正規化した後、受信側ノードでメッセージを集約しています。 学習関数trainでは、データローダーからバッチを取得し、モデルで予測を行った後、予測とラベルからロスを計算し、逆伝播でパラメータを更新しています。test関数では予測値と正解ラベルから正解率を計算しています。 学習ループでは20エポック学習を行い、各エポックでロスと正解率を出力しています。 このように、PyTorchとtorch-geometricライブラリを使えば、比較的簡単にGNNを実装することができます。ぜひ実装を試し、GNNの動作を理解してみてください。

Clean code オブジェクト指向PyTorch実装

はじめに

本記事では、Pythonの深層学習ライブラリであるPyTorchを用いて、深層学習モデルを実装する方法について説明します。ここでは、オブジェクト指向プログラミングの原則に従い、モジュール性と拡張性の高いコードを書くことを目指します。具体的には、以下のようなSOLID原則に基づいたクラス設計を行います。

  • 単一責任の原則 (Single Responsibility Principle)
  • オープン・クローズドの原則 (Open-Closed Principle)
  • インターフェイス分離の原則 (Interface Segregation Principle)

データ準備

まずは、学習に使用するデータセットを準備するクラスから始めましょう。ここでは、単一責任の原則に従い、データの準備とロードに特化したクラスを作成します。

import torch
from torch.utils.data import Dataset

class DummyDataset(Dataset):
    """ダミーデータセットを生成するクラス"""

    def __init__(self, num_samples, input_size, output_size):
        self.num_samples = num_samples
        self.input_size = input_size
        self.output_size = output_size
        self.X = torch.randn(num_samples, input_size)
        self.y = torch.randn(num_samples, output_size)

    def __len__(self):
        return self.num_samples

    def __getitem__(self, idx):
        return self.X[idx], self.y[idx]

DummyDatasetクラスは、PyTorchのDatasetクラスを継承しています。initメソッドでは、サンプル数、入力サイズ、出力サイズを受け取り、ダミーのデータセットを生成します。lenメソッドはデータセットの長さを返し、getitemメソッドは指定されたインデックスのサンプルを返します。

モデルの定義

次に、モデルクラスを定義します。ここでは、開放閉鎖の原則に従い、異なる種類のモデルを簡単に拡張できるようにします。

import torch.nn as nn

class NeuralNetwork(nn.Module):
    """単純な全結合ニューラルネットワーク"""

    def __init__(self, input_size, hidden_size, output_size):
        super(NeuralNetwork, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out)
        out = self.fc2(out)
        return out

class ConvolutionalNeuralNetwork(nn.Module):
    """畳み込みニューラルネットワーク"""

    def __init__(self, input_channels, output_channels, kernel_size):
        super(ConvolutionalNeuralNetwork, self).__init__()
        self.conv = nn.Conv2d(input_channels, output_channels, kernel_size)
        self.relu = nn.ReLU()
        self.pool = nn.MaxPool2d(2, 2)
        self.fc = nn.Linear(output_channels * 12 * 12, 10)

    def forward(self, x):
        out = self.conv(x)
        out = self.relu(out)
        out = self.pool(out)
        out = out.view(-1, 288)
        out = self.fc(out)
        return out

NeuralNetworkクラスは単純な全結合ニューラルネットワークを表し、ConvolutionalNeuralNetworkクラスは畳み込みニューラルネットワークを表しています。両クラスともnn.Moduleを継承しており、forwardメソッドで前向き計算を定義しています。 このように、異なる種類のモデルをそれぞれ別のクラスとして定義することで、開放閉鎖の原則に従っています。新しい種類のモデルを追加する場合は、単に新しいクラスを作成するだけで済みます。

学習とモデル評価

学習とモデル評価の機能は、インターフェイス分離の原則に従って別々のクラスに分離しています。

import torch.nn as nn

class Trainer:
    """モデルの学習を行うクラス"""

    def __init__(self, model, criterion, optimizer, device):
        self.model = model.to(device)
        self.criterion = criterion
        self.optimizer = optimizer
        self.device = device

    def train(self, dataloader, num_epochs):
        self.model.train()
        for epoch in range(num_epochs):
            running_loss = 0.0
            for inputs, labels in dataloader:
                inputs, labels = inputs.to(self.device), labels.to(self.device)

                self.optimizer.zero_grad()
                outputs = self.model(inputs)
                loss = self.criterion(outputs, labels)
                loss.backward()
                self.optimizer.step()

                running_loss += loss.item()

            epoch_loss = running_loss / len(dataloader)
            print(f'Epoch {epoch+1}, Loss: {epoch_loss:.4f}')

class Evaluator:
    """モデルの評価を行うクラス"""

    def __init__(self, model, criterion, device):
        self.model = model.to(device)
        self.criterion = criterion
        self.device = device

    def evaluate(self, dataloader):
        self.model.eval()
        running_loss = 0.0
        with torch.no_grad():
            for inputs, labels in dataloader:
                inputs, labels = inputs.to(self.device), labels.to(self.device)
                outputs = self.model(inputs)
                loss = self.criterion(outputs, labels)
                running_loss += loss.item()

        eval_loss = running_loss / len(dataloader)
        print(f'Evaluation Loss: {eval_loss:.4f}')

Trainerクラスは、モデルの学習を行います。initメソッドでは、モデル、損失関数、オプティマイザー、デバイス(CPU/GPU)を受け取ります。trainメソッドでは、指定された epochの数だけ学習を行い、各epochの損失値を出力します。 Evaluatorクラスは、モデルの評価を行います。initメソッドでは、モデル、損失関数、デバイスを受け取ります。evaluateメソッドでは、渡されたデータローダーを使ってモデルの評価損失を計算し、出力します。

ハイパーパラメータの管理

ハイパーパラメータの管理は、専用のクラスに集約して行います。これにより、グローバル変数を使うことなく、オブジェクト指向の原則に従ったコードを書くことができます。

import torch
import torch.nn as nn
from torch.utils.data import DataLoader

from dataset import DummyDataset
from models import NeuralNetwork, ConvolutionalNeuralNetwork
from trainers import Trainer, Evaluator

class Config:
    """ハイパーパラメータを管理するクラス"""

    def __init__(self, num_samples=1000, input_size=10, output_size=1, hidden_size=20, batch_size=32, num_epochs=100, lr=0.001, device='cpu'):
        self.num_samples = num_samples
        self.input_size = input_size
        self.output_size = output_size
        self.hidden_size = hidden_size
        self.batch_size = batch_size
        self.num_epochs = num_epochs
        self.lr = lr
        self.device = torch.device(device)

def main():
    # ハイパーパラメータの設定
    config = Config()

    # データセットの準備
    dataset = DummyDataset(config.num_samples, config.input_size, config.output_size)
    dataloader = DataLoader(dataset, batch_size=config.batch_size, shuffle=True)

    # モデルの定義
    model = NeuralNetwork(config.input_size, config.hidden_size, config.output_size)

    # 損失関数とオプティマイザーの定義
    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(model.parameters(), lr=config.lr)

    # トレーナーとエバリュエーターの初期化
    trainer = Trainer(model, criterion, optimizer, config.device)
    evaluator = Evaluator(model, criterion, config.device)

    # 学習
    trainer.train(dataloader, config.num_epochs)

    # 評価
    evaluator.evaluate(dataloader)

if __name__ == '__main__':
    main()

Configクラスを新たに作成し、ハイパーパラメータを集約して管理するようにしました。このクラスのインスタンスを作成し、必要な箇所でアクセスすることで、グローバル変数を使う必要がなくなります。 メインスクリプトでは、Configクラスのインスタンスを作成し、その設定を使ってデータセット、モデル、損失関数、オプティマイザー、トレーナー、エバリュエーターを初期化しています。最後に、trainer.trainメソッドでモデルの学習を行い、evaluator.evaluateメソッドでモデルを評価しています。

まとめ

オブジェクト指向とSOLID原則に基づいた、PyTorchを使った深層学習の実装例を紹介しました。単一責任の原則、開放閉鎖の原則、インターフェイス分離の原則に従ってクラスを設計することで、モジュール性と拡張性の高いコードを書くことができます。また、ハイパーパラメータの管理をクラスに集約することで、グローバル変数を使わずにオブジェクト指向の原則に従うことができます。 実際のアプリケーションでは、データの前処理、ハイパーパラメータのチューニング、モデルの評価指標の設定など、さまざまな工夫が必要になります。しかし、ここで紹介した設計原則に従えば、保守性と拡張性の高い深層学習システムを構築することができるでしょう。

スコープ: Pythonのスコープの基礎

はじめに

プログラミングにおいて、「スコープ」とは変数や関数の有効範囲のことを指します。Pythonでは、スコープの概念を適切に理解することが非常に重要です。スコープを理解すれば、変数や関数を適切に使用できるようになり、バグの発生を防ぐことができます。 このブログ記事では、Pythonのスコープについて、基礎から応用まで、視覚的かつ丁寧に解説していきます。

スコープとは

スコープとは、変数や関数が有効な範囲のことを指します。Pythonには、以下の4つのスコープがあります。

  • ローカルスコープ (Local Scope): 関数やメソッド内で定義された変数の有効範囲
  • エンクロージングスコープ (Enclosing Scope): 内側の関数から見た外側の関数のスコープ
  • グローバルスコープ (Global Scope): モジュール内で定義された変数の有効範囲
  • ビルトインスコープ (Built-in Scope): Pythonの組み込み関数やモジュールの有効範囲

それでは、各スコープについて詳しく見ていきましょう。

ローカルスコープ (Local Scope)

ローカルスコープとは、関数やメソッド内で定義された変数の有効範囲のことです。ローカル変数は、その関数やメソッド内でのみ有効です。

def my_function():
    x = 10  # ローカル変数
    print(x)

my_function()  # 出力: 10

print(x)  # エラー: x is not defined

上記の例では、xはローカル変数です。my_function()内ではxを参照できますが、関数の外ではxは存在しないため、エラーが発生します。 ローカルスコープのイメージは以下のようになります。

+--------------------------------+
|            my_function()       |
|   +----------------------------+
|   |         ローカルスコープ    |
|   |   x = 10                   |
|   |   print(x)  # 出力: 10     |
|   +----------------------------+
|                                |
+--------------------------------+

print(x)  # エラー: x is not defined

グローバルスコープ (Global Scope)

グローバルスコープとは、モジュール内で定義された変数の有効範囲のことです。グローバル変数は、モジュール内のどこからでも参照できます。

x = 10  # グローバル変数

def my_function():
    print(x)  # グローバル変数xを参照

my_function()  # 出力: 10

print(x)  # 出力: 10

上記の例では、xはグローバル変数です。my_function()内でも、外でも、xを参照できます。 グローバルスコープのイメージは以下のようになります。

x = 10  # グローバル変数

+--------------------------------+
|            my_function()       |
|   print(x)  # 出力: 10         |
+--------------------------------+

print(x)  # 出力: 10

エンクロージングスコープ (Enclosing Scope)

エンクロージングスコープとは、内側の関数から見た外側の関数のスコープのことです。内側の関数では、外側の関数のローカル変数を参照できます。

def outer_function():
    x = 10  # outer_function()のローカル変数

    def inner_function():
        print(x)  # outer_function()のxを参照

    inner_function()

outer_function()  # 出力: 10

上記の例では、inner_function()はouter_function()のローカル変数xを参照できます。 エンクロージングスコープのイメージは以下のようになります。

+--------------------------------+
|          outer_function()      |
|   x = 10                       |
|   +----------------------------+
|   |        inner_function()    |
|   |   print(x)  # 出力: 10     |
|   +----------------------------+
+--------------------------------+

ビルトインスコープ (Built-in Scope)

ビルトインスコープとは、Pythonの組み込み関数やモジュールの有効範囲のことです。ビルトインスコープは、すべてのスコープから参照できます。

print(len([1, 2, 3]))  # 出力: 3

上記の例では、len()はPythonの組み込み関数です。どのスコープからでもlen()を呼び出すことができます。 ビルトインスコープのイメージは以下のようになります。

+--------------------------------+
|            ビルトインスコープ    |
|   len(), print(), sum(), ...   |
+--------------------------------+
                |
+--------------------------------+
|            グローバルスコープ    |
|   print(len([1, 2, 3]))        |
+--------------------------------+

スコープの優先順位

Pythonでは、変数の検索時に以下の優先順位でスコープを探索します。

ローカルスコープ エンクロージングスコープ グローバルスコープ ビルトインスコープ

つまり、ローカルスコープから順に探索を行い、最初に見つかった変数が使用されます。

pythonCopy codex = 10  # グローバル変数

def outer_function():
    x = 20  # outer_function()のローカル変数

    def inner_function():
        x = 30  # inner_function()のローカル変数
        print(x)  # 出力: 30

    inner_function()
    print(x)  # 出力: 20

outer_function()
print(x)  # 出力: 10

上記の例では、各関数内でxが定義されています。inner_function()では最も内側のローカル変数xが使用され、outer_function()では外側のローカル変数xが使用されます。最後に、グローバル変数xが参照されます。

property, getter, setter 基礎

Pythonのプロパティ、ゲッター、セッターの基礎から応用まで

Pythonにおいて、プロパティ、ゲッター、セッターは、クラスのデータ属性をカプセル化し、適切にアクセスやデータの検証を行うための仕組みです。この仕組みを適切に使用することで、クラスのデータ属性に対するアクセスをコントロールし、コードの信頼性と保守性を高めることができます。 このブログ記事では、Pythonのプロパティ、ゲッター、セッターについて、基礎から応用まで、視覚的かつ丁寧に解説していきます。

プロパティ (Property) とは

プロパティとは、クラスの属性にアクセスするための特別なメソッドです。プロパティを使用することで、属性の値を直接取得または設定するのではなく、ゲッターとセッターを介して値を取得または設定することができます。これにより、属性の値の検証や変更を制御することができます。 プロパティのイメージは以下のようになります。

+---------------------+
|        Person       |
|  +------------------+
|  | _name (データ属性)|
|  +------------------+
|  | @property        |
|  | def name(self):  |  ゲッター
|  |     ...          |
|  +------------------+
|  | @name.setter     |
|  | def name(self,   |
|  |          value): |  セッター
|  |     ...          |
|  +------------------+
+---------------------+

ゲッタープロパティ (Getter Property)

ゲッタープロパティは、属性の値を取得するメソッドです。@propertyデコレータを使用して定義します。

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

上記の例では、name()メソッドがゲッタープロパティとして定義されています。このメソッドは、_nameデータ属性の値を返します。ゲッタープロパティは、属性の値を取得する際に使用されます。

person = Person("Alice")
print(person.name)  # 出力: Alice

セッタープロパティ (Setter Property)

セッタープロパティは、属性の値を設定するメソッドです。@property_name.setterデコレータを使用して定義します。

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("名前は文字列である必要があります")
        self._name = value

上記の例では、name()メソッドがセッタープロパティとして定義されています。このメソッドは、_nameデータ属性の値を設定します。また、値が文字列でない場合はTypeErrorを発生させます。セッタープロパティは、属性の値を設定する際に使用されます。

person = Person("Alice")
person.name = "Bob"  # 値を設定
print(person.name)  # 出力: Bob

person.name = 123  # TypeError: 名前は文字列である必要があります

デリーター (Deleter) デリーターは、属性を削除するメソッドです。@property_name.deleterデコレータを使用して定義します。

class Person:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("名前は文字列である必要があります")
        self._name = value

    @name.deleter
    def name(self):
        del self._name

上記の例では、name()メソッドがデリーターとして定義されています。このメソッドは、_nameデータ属性を削除します。デリーターは、属性を削除する際に使用されます。

person = Person("Alice")
print(person.name)  # 出力: Alice

del person.name
print(person.name)  # AttributeError: 'Person' object has no attribute '_name'

プロパティの利点 プロパティを使用することで、以下のような利点があります。

データの検証: セッタープロパティを使用することで、属性の値を検証し、不正な値の設定を防ぐことができます。 データの計算: ゲッタープロパティを使用することで、属性の値を計算して返すことができます。 カプセル化の強化: データ属性をアンダースコア (_name) で始めることで、外部からのアクセスを制限できます。 コードの可読性の向上: プロパティを使用することで、属性の取得や設定の処理を明確に分離できます。

ゲッターとセッターの従来の方法 プロパティの導入以前は、ゲッターとセッターを手動で実装する必要がありました。以下は、その従来の方法の例です。

class Person:
    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

    def set_name(self, value):
        if not isinstance(value, str):
            raise TypeError("名前は文字列である必要があります")
        self._name = value

上記の例では、get_name()メソッドがゲッター、set_name()メソッドがセッターとして実装されています。この方法では、属性の取得や設定の際に、明示的にメソッドを呼び出す必要があります。

person = Person("Alice")
print(person.get_name())  # 出力: Alice

person.set_name("Bob")
print(person.get_name())  # 出力: Bob

person.set_name(123)  # TypeError: 名前は文字列である必要があります

プロパティを使用すると、属性のように簡単にアクセスできるため、コードがより読みやすく、保守性が高まります。

抽象クラスの基礎:Python abstractmethod

Pythonの抽象クラスについて

抽象クラスとは

抽象クラスとは、共通の機能を持つクラスのベースとなるクラスです。抽象クラスには実装されていないメソッドが含まれており、これらのメソッドは具象クラスで実装されなければなりません。 抽象クラスは、インスタンスを作成することはできません。具象クラスのインスタンスを作成する必要があります。 抽象クラスの役割は、クラスの設計を明確にすることと、共通のインターフェースを強制することです。

抽象クラスには、抽象メソッドと共通のメソッドが定義されています。 具象クラスは、抽象クラスを継承します。 具象クラスは、抽象メソッドを実装しなければなりません。 具象クラスは、共通のメソッドを継承しています。

抽象クラスの定義

Pythonでは、abcモジュールを使って抽象クラスを定義します。

import abc

class AbstractClass(abc.ABC):
    
    @abc.abstractmethod
    def abstract_method(self):
        """実装されていないメソッド"""
        pass
    
    def common_method(self):
        """共通のメソッド"""
        print("共通の処理")

abc.ABCを継承することで、クラスを抽象クラスにします。 @abc.abstractmethodデコレータを使うことで、メソッドを抽象メソッドにします。 抽象メソッドには、passだけが記述されています。

具象クラスの定義

具象クラスは、抽象クラスを継承し、抽象メソッドを実装します。

class ConcreteClass(AbstractClass):
    
    def abstract_method(self):
        """抽象メソッドの実装"""
        print("具象クラスの処理")

抽象クラスを継承します。 抽象メソッドをオーバーライドし、実装します。 共通のメソッドは継承されます。

抽象クラスの利点

抽象クラスには、以下のような利点があります。

コードの一貫性と整合性の向上

関連するクラスに共通のインターフェースを強制できます。 コードの保守性が向上します。

コードの再利用性の向上

共通の機能をベースクラスに集約できます。 重複コードを減らすことができます。

設計の明確化

クラスの設計が明確になります。 具象クラスがどのようなインターフェースを実装する必要があるかが明示的になります。

多態性の実現

抽象クラスのインスタンスではなく、具象クラスのインスタンスを使うことができます。 コードの柔軟性が高まります。

抽象クラスの注意点

抽象クラスを使う際には、以下の点に注意が必要です。

抽象メソッドのオーバーライド

具象クラスでは、抽象クラスの抽象メソッドをすべてオーバーライドする必要があります。 オーバーライドされていない場合は、TypeErrorが発生します。

インスタンス化の制限

抽象クラス自体はインスタンス化できません。 具象クラスのインスタンスを作成する必要があります。

継承の制限

抽象クラスを継承する場合、抽象クラスの抽象メソッドをすべてオーバーライドするか、 または継承したクラスも抽象クラスにする必要があります。

抽象クラスの例

ここでは、形状を表すクラスを例に、抽象クラスの利用例を示します。

import abc

class Shape(abc.ABC):
    """形状を表す抽象クラス"""
    
    @abc.abstractmethod
    def area(self):
        """面積を計算する抽象メソッド"""
        pass
    
    @abc.abstractmethod
    def perimeter(self):
        """周囲を計算する抽象メソッド"""
        pass
    
    def print_info(self):
        """形状の情報を出力するメソッド"""
        print(f"面積: {self.area()}")
        print(f"周囲: {self.perimeter()}")

class Rectangle(Shape):
    """長方形を表す具象クラス"""
    
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        """長方形の面積を計算する"""
        return self.width * self.height
    
    def perimeter(self):
        """長方形の周囲を計算する"""
        return 2 * (self.width + self.height)

# 具象クラスのインスタンス化と使用
rectangle = Rectangle(5, 3)
rectangle.print_info()
出力:
Copy code面積: 15
周囲: 16

Shapeクラスは抽象クラスで、area()とperimeter()が抽象メソッドです。 Rectangleクラスは具象クラスで、Shapeクラスを継承しています。 Rectangleクラスでは、抽象メソッドをオーバーライドし、長方形の面積と周囲を計算する実装を行っています。 最後に、Rectangleクラスのインスタンスを作成し、print_info()メソッドを呼び出すことで、面積と周囲が出力されます。