[Python]カプセル化: 直接アクセス vs プロパティ

## Pythonにおける属性アクセスの比較: 直接アクセス vs プロパティ

Pythonでクラスの属性にアクセスする方法には、大きく分けて2つの方法があります。一つは直接属性にアクセスする方法、もう一つはプロパティを利用する方法です。一見すると両者の動作結果は似ていますが、内部的には重要な違いがあります。この記事では、それぞれの方法の特徴と使い分けについて解説します。
（私が新卒入社時に受けた研修でこれが説明された時は一ミリも理解できませんでした。笑）



## 直接アクセスの場合 (例: `self.name = name`)

以下は、属性に直接アクセスするシンプルな例です。

```python
class Person:
    def __init__(self, name, age):
        self.name = name  # 属性を公開
        self.age = age

    def introduce_yourself(self):
        print(f"My name is {self.name}")
        print(f"I'm {self.age} years old")

# 動作確認
person = Person("Kite", 15)
person.introduce_yourself()
person.name = "Michel"  # 属性を直接変更
person.introduce_yourself()
```

### **メリット**
- **シンプルで直感的**: コードが短く、初学者にも理解しやすい。
- **即時アクセス可能**: 外部から直接属性を取得・変更できるため、手間がかからない。

### **デメリット**
- **制御が難しい**:
  - 外部から属性に自由にアクセスできるため、不正な値が設定される可能性がある。
  - 例: `person.name = 1234` のような無効な値が許容されてしまう。
- **追加処理が難しい**:
  - 値の変更や取得時に、特定のロジックを追加することができない。



## プロパティを使った場合 (例: `@property`と`@name.setter`)

次に、プロパティを使って同じ例を実装してみます。

```python
class Person:
    def __init__(self, name, age):
        self.__name = name  # 非公開属性
        self.__age = age

    @property
    def name(self):  # getter
        return self.__name

    @name.setter
    def name(self, new_name):  # setter
        if not isinstance(new_name, str):  # 値の型チェック
            raise ValueError("Name must be a string!")
        self.__name = new_name

    def introduce_yourself(self):
        print(f"My name is {self.__name}")
        print(f"I'm {self.__age} years old")

# 動作確認
person = Person("Kite", 15)
person.introduce_yourself()
person.name = "Michel"  # プロパティ経由で属性を変更
person.introduce_yourself()
```
### **コードの解説**
- `self.__name` と `self.__age` は非公開属性として定義されています。
- `@property` デコレータを使用して、`name` を取得するためのメソッドを定義しています（getter）。
- `@name.setter` デコレータを使用して、`name` に値を設定する際のバリデーションを実施しています（setter）。
  - 文字列以外の値が設定されようとすると、`ValueError` を発生させます。
- `introduce_yourself` メソッドで、名前と年齢を出力します。

### **動作結果**

1. **最初のインスタンス生成と自己紹介**
   ```python
   person = Person("Kite", 15)
   person.introduce_yourself()
   ```
   - `__name` に `"Kite"` が、`__age` に `15` が設定されます。
   - `introduce_yourself` を呼び出すと、`"My name is Kite"` と `"I'm 15 years old"` が出力されます。

2. **`name` 属性の変更**
   ```python
   person.name = "Michel"
   ```
   - setter を通じて `__name` の値が `"Michel"` に変更されます。
   - 値の型チェックが行われますが、`"Michel"` は文字列なので問題なく設定されます。

3. **変更後の自己紹介**
   ```python
   person.introduce_yourself()
   ```
   - `__name` が `"Michel"` に変更されているため、`introduce_yourself` を呼び出すと、`"My name is Michel"` と `"I'm 15 years old"` が出力されます。



### **出力結果**
```plaintext
My name is Kite
I'm 15 years old
My name is Michel
I'm 15 years old
```



### **注意点**
- このコードでは、`name` に対する安全なアクセス制御が実装されており、文字列以外の値（例えば、`1234`）を設定しようとすると以下のエラーが発生します。

   ```python
   person.name = 1234
   ```
   **エラー例:**
   ```plaintext
   ValueError: Name must be a string!
   ``` 

このように、プロパティを使うことで安全に値を操作でき、クラス設計の信頼性が向上します。

### **メリット**
- **制御が可能**:
  - 値の取得や変更時に条件を設定できる。
  - 例: `name`属性に対して、文字列以外の値を防ぐ制御を追加。
- **隠蔽性の確保**:
  - 属性を非公開にし、外部から直接アクセスできないようにできる。
- **柔軟性が高い**:
  - 属性の取得や変更時に自動処理を組み込むことが可能。
  - 例: 文字列を常に大文字に変換する処理など。
- **外部コードへの影響が少ない**:
  - クラス設計が変更されても、プロパティを通じて操作することで外部コードの修正を最小限に抑えられる。

### **デメリット**
- **実装が複雑**:
  - 初学者には少し難しく感じる場合がある。
- **コードが冗長**:
  - シンプルな用途では、やや大げさに感じることもある。

このコードは、`Person` クラスのプロパティを利用して属性を安全に操作する仕組みを示しています。コードの動作結果を具体的に見ていきましょう。


## どちらを選ぶべきか？

以下の基準で選択すると良いでしょう。

### **1. シンプルなクラスの場合**
- 値の制約や追加処理が不要な場合は、直接アクセス (`self.name = name`) が適しています。

### **2. 制御や拡張が必要な場合**
- 値に対するバリデーションや処理を追加したい場合は、プロパティを利用する方が適しています。
  - 例: 名前を常に大文字に変換する、年齢が正の整数であることを保証するなど。

### **3. 長期的な管理が必要な場合**
- クラスが拡張される可能性がある場合は、プロパティを利用すると柔軟性が向上します。



## 実際の選択例

- **短期的な小規模プロジェクトや個人開発**: シンプルに直接アクセスで十分。
- **長期的なプロジェクトや複雑な設計**: プロパティを利用することで安全性と柔軟性を確保。


## まとめ

プロパティを使用することで、クラス設計の柔軟性や安全性が向上します。ただし、用途によって適切な方法を選択することが大切です。長期的に管理するコードでは、プロパティを使う設計を採用することを強くおすすめします！



## **おまけ：　getterのみの場合のエラーケース**

getterのみを実装した場合、属性を読み取り専用にすることができます。setterが定義されていないため、外部から値を変更しようとするとエラーが発生します。

### **コード例**

```python
class Person:
    def __init__(self, name):
        self.__name = name  # 非公開属性

    @property
    def name(self):  # 読み取り専用プロパティ
        return self.__name

# 動作確認
person = Person("Kite")
print(person.name)

# 値を変更しようとする
try:
    person.name = "Michel"  # setterがないためエラー
except AttributeError as e:
    print(f"Error: {e}")
```

### **出力結果**
```
Kite
Error: can't set attribute
```

**改善点:**
- 属性を読み取り専用にすることで、外部からの変更を防止できます。
- 設計上、変更不要な属性を安全に保護できます。