カプセル化とアクセス修飾子 – データ保護の技術

2025-10-28

はじめに

オブジェクト指向プログラミングのカプセル化は、データとそれを操作するメソッドを1つの単位にまとめ、外部からの不正なアクセスから保護する重要な概念です。これにより、プログラムの信頼性、保守性、セキュリティが大幅に向上します。

現実世界で例えるなら、自動車のエンジンはカプセル化の良い例です。ドライバーはアクセルやブレーキといったインターフェースを通じて車を操作しますが、エンジンの内部構造や動作原理を知る必要はありません。この「内部の詳細を隠蔽する」という考え方がカプセル化の本質です。

カプセル化の基本概念

カプセル化には主に2つの目的があります。

  1. データの保護: オブジェクトの内部状態を外部から保護する
  2. 実装の隠蔽: 内部の実装詳細を隠し、インターフェースのみを公開する

カプセル化の利点として、まず内部実装を変更しても外部インターフェースが変わらなければ影響を最小限に抑えられるため保守性が向上し、データの整合性を保証できることで信頼性も高まります。また、重要なデータへの直接アクセスを防ぐことでセキュリティが強化され、複雑な内部実装を知らなくても利用可能な使いやすいインターフェースを提供できる点も大きなメリットです。

Pythonのアクセス修飾子

Pythonでは、他の言語のような厳格なアクセス修飾子(public, private, protected)はありませんが、命名規則を通じてアクセスレベルを示す慣習があります。

公開(Public)メンバー

アンダースコアなしで始まるメンバーは公開され、どこからでもアクセス可能です。

次のコードは、銀行口座を表す BankAccount クラスの定義です。

  • コンストラクタ __init__ で口座名義(account_holder)と初期残高(balance)を受け取り、公開属性として保持します。
  • 公開メソッド deposit は、引数で渡された金額が正の値であれば残高に加算し、成功した場合は True を返します。金額が不正(0以下)の場合は処理せず False を返します。
class BankAccount:
    def __init__(self, account_holder, initial_balance):
        self.account_holder = account_holder  # 公開属性
        self.balance = initial_balance        # 公開属性

    def deposit(self, amount):  # 公開メソッド
        if amount > 0:
            self.balance += amount
            return True
        return False

つまり、このクラスは口座残高の管理と入金処理を簡単に扱えるように設計されています。

保護(Protected)メンバー

シングルアンダースコア(_)は非公開にしたいことを示す慣習的な目印であり、技術的なアクセス制限はなく、外部からも利用可能ですが、「触らないでほしい」という開発者間の暗黙の約束を意味します。メンバーは保護を意図し、クラス内部とサブクラスからのアクセスを想定しています。

  • コンストラクタ __init__ では、口座名義(account_holder)を公開属性として保持し、残高(_balance)や取引回数(_transaction_count)を保護属性として内部的に管理します。
  • 保護メソッド _validate_amount は、入金や出金時に金額が正の値かどうかを検証する内部処理用メソッドです。
  • 保護メソッド _increment_transaction_count は、取引が行われた際に取引回数をインクリメントする内部処理用メソッドです。
class BankAccount:
    def __init__(self, account_holder, initial_balance):
        self.account_holder = account_holder
        self._balance = initial_balance  # 保護属性
        self._transaction_count = 0      # 保護属性

    def _validate_amount(self, amount):  # 保護メソッド
        """金額の検証(内部使用)"""
        return amount > 0

    def _increment_transaction_count(self):  # 保護メソッド
        """取引回数をインクリメント(内部使用)"""
        self._transaction_count += 1

つまり、これらの保護属性・保護メソッドを用いることで、外部からの直接操作を避けつつ、内部処理を安全に管理できる設計になっています。

非公開(Private)メンバー

アンダースコア2つで始まるメンバーは非公開を意図し、名前のマングリング(name mangling)が行われます。外部から直接アクセスできず、クラスの内部実装を隠すために使われます。

  • コンストラクタ __init__ では、口座名義(account_holder)を公開属性として保持し、残高(__balance)や口座番号(__account_number)を非公開属性として内部で管理します。
  • 非公開メソッド __generate_account_number は、口座番号をランダムに生成する内部処理用メソッドです。
  • 非公開メソッド __validate_transaction は、取引が有効か(正の金額で残高が十分か)を検証する内部処理用メソッドです。
  • 公開メソッド get_balance は、残高を取得するための公開インターフェースとして外部からアクセス可能です。
class BankAccount:
    def __init__(self, account_holder, initial_balance):
        self.account_holder = account_holder
        self.__balance = initial_balance        # 非公開属性
        self.__account_number = self.__generate_account_number()  # 非公開属性

    def __generate_account_number(self):  # 非公開メソッド
        """口座番号を生成(内部実装)"""
        import random
        return f"ACC{random.randint(10000, 99999)}"

    def __validate_transaction(self, amount):  # 非公開メソッド
        """取引の検証(内部実装)"""
        return amount > 0 and self.__balance >= amount

    def get_balance(self):  # 公開メソッド
        """残高を取得(公開インターフェース)"""
        return self.__balance

上記のコードでは、__balance__account_numberなどの変数、および__generate_account_number()__validate_transaction()といったメソッドが非公開属性として定義されています。これらは名前の前に__を付けることで、Pythonが内部的に_BankAccount__balanceのように**name mangling(名前改変)**を行い、外部から直接アクセスしにくくしています。これにより、内部実装を保護し、サブクラスなどで誤って上書きされるのを防ぐことで、クラスの安全性とカプセル化を高めています。

プロパティの使用

基本的なプロパティの使い方

Pythonの@propertyデコレータを使用すると、属性へのアクセスをメソッドで制御できます。次のコードは@propertyデコレータによるプロパティ機能を使って、温度データを安全かつ直感的に扱う方法を示しています。

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        """摂氏温度を取得"""
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        """摂氏温度を設定(検証付き)"""
        if value < -273.15:
            raise ValueError("絶対零度以下は設定できません")
        self._celsius = value

    @property
    def fahrenheit(self):
        """華氏温度を計算(読み取り専用)"""
        return (self._celsius * 9/5) + 32

# 使用例
temp = Temperature(25)
print(temp.celsius)     # 25
print(temp.fahrenheit)  # 77.0

temp.celsius = 30      # セッターを呼び出し
# temp.fahrenheit = 100  # エラー: セッターが定義されていない

celsiusは取得と検証付きの設定が可能で、-273.15℃(絶対零度)未満を防ぎます。一方、fahrenheitは読み取り専用プロパティとして定義され、celsiusの値から自動的に華氏を計算します。これにより、内部変数を直接操作せずに、安全で分かりやすいインターフェースで温度を管理できます。

ビジネスロジックを含むプロパティ

次のコードは、@propertyデコレータを用いたカプセル化と自動計算の仕組みを示しています。

class Employee:
    def __init__(self, name, base_salary):
        self.name = name
        self._base_salary = base_salary
        self._bonus = 0
        self._performance_rating = 1.0

    @property
    def base_salary(self):
        return self._base_salary

    @base_salary.setter
    def base_salary(self, value):
        if value >= 0:
            self._base_salary = value
        else:
            raise ValueError("基本給は0以上である必要があります")

    @property
    def performance_rating(self):
        return self._performance_rating

    @performance_rating.setter
    def performance_rating(self, value):
        if 0.5 <= value <= 2.0:
            self._performance_rating = value
            self._calculate_bonus()
        else:
            raise ValueError("評価は0.5から2.0の間である必要があります")

    def _calculate_bonus(self):
        """ボーナスを計算(内部メソッド)"""
        self._bonus = self._base_salary * (self._performance_rating - 1.0)

    @property
    def total_salary(self):
        """総支給額(読み取り専用)"""
        return self._base_salary + self._bonus

    @property
    def bonus(self):
        """ボーナス額(読み取り専用)"""
        return self._bonus

Employeeクラスでは、base_salaryperformance_ratingを安全に設定できるよう検証付きのセッターを定義しています。評価値performance_ratingが変更されると、自動的に内部メソッド_calculate_bonus()が呼ばれ、ボーナスが再計算されます。また、total_salarybonusは読み取り専用プロパティとして定義され、外部から直接変更できません。これにより、整合性の取れた給与管理が可能になります。

ゲッターとセッターメソッド

プロパティを使用しない場合、明示的なゲッター・セッターメソッドを定義することもできます。次のコードはゲッターとセッターによるカプセル化の基本を示しています。

class Student:
    def __init__(self, name, age):
        self._name = name
        self._age = age
        self._grades = []
        self._is_graduated = False

    # ゲッターメソッド
    def get_name(self):
        return self._name

    def get_age(self):
        return self._age

    def get_grades(self):
        return self._grades.copy()  # コピーを返して内部リストを保護

    def is_graduated(self):
        return self._is_graduated

    # セッターメソッド
    def set_name(self, name):
        if isinstance(name, str) and len(name) >= 2:
            self._name = name
        else:
            raise ValueError("名前は2文字以上の文字列である必要があります")

    def set_age(self, age):
        if isinstance(age, int) and 0 <= age <= 120:
            self._age = age
        else:
            raise ValueError("年齢は0から120の整数である必要があります")

    def add_grade(self, grade):
        if 0 <= grade <= 100:
            self._grades.append(grade)
        else:
            raise ValueError("成績は0から100の間である必要があります")

    def graduate(self):
        if len(self._grades) >= 5:
            self._is_graduated = True
        else:
            raise ValueError("卒業には5つ以上の成績が必要です")

Studentクラスは名前・年齢・成績・卒業状態などの情報を持ち、それぞれの属性を安全に操作するためのメソッドを定義しています。直接属性を変更させず、set_name()set_age()などのセッターで値の検証を行うことで、不正なデータの代入を防ぎます。また、get_grades()はリストのコピーを返すことで外部からの改変を防ぎ、データの整合性を保ちます。これはオブジェクト指向における情報隠蔽の好例です。

カプセル化の実践例:銀行口座システム

実際の銀行口座システムを模した、しっかりとしたカプセル化の例を見てみましょう。

BankAccountクラスでは、__balance__account_numberなどの重要なデータを二重アンダースコアによるname manglingで保護し、外部からの直接アクセスを防いでいます。口座番号生成(__generate_account_number)や取引検証(__validate_transaction)、履歴記録(__record_transaction)といった内部処理も非公開メソッドとして定義されています。

また、deposit()withdraw()transfer()などのメソッドを通してのみ残高操作が可能で、@propertyを利用して残高や口座状態を安全に参照できます。さらに、get_transaction_history()では履歴を一部マスクして返すなど、セキュリティとデータ保護を重視した設計となっています。

class BankAccount:
    def __init__(self, account_holder, initial_balance=0):
        self._account_holder = account_holder
        self.__account_number = self.__generate_account_number()
        self.__balance = initial_balance
        self.__transaction_history = []
        self.__is_active = True
        self.__overdraft_limit = 0

    def __generate_account_number(self):
        """口座番号を生成(非公開メソッド)"""
        import random
        import hashlib
        base_number = f"{self._account_holder}{random.randint(1000, 9999)}"
        return hashlib.md5(base_number.encode()).hexdigest()[:10].upper()

    def __validate_transaction(self, amount):
        """取引の検証(非公開メソッド)"""
        if not self.__is_active:
            raise ValueError("口座が無効です")
        if amount <= 0:
            raise ValueError("取引額は正の値である必要があります")
        return True

    def __record_transaction(self, transaction_type, amount):
        """取引履歴を記録(非公開メソッド)"""
        import datetime
        transaction = {
            'type': transaction_type,
            'amount': amount,
            'balance': self.__balance,
            'timestamp': datetime.datetime.now()
        }
        self.__transaction_history.append(transaction)

    @property
    def account_holder(self):
        return self._account_holder

    @property
    def account_number(self):
        return f"****{self.__account_number[-4:]}"

    @property
    def balance(self):
        return self.__balance

    @property
    def is_active(self):
        return self.__is_active

    def deposit(self, amount):
        """預入処理"""
        if self.__validate_transaction(amount):
            self.__balance += amount
            self.__record_transaction('DEPOSIT', amount)
            return True
        return False

    def withdraw(self, amount):
        """引出処理"""
        if self.__validate_transaction(amount):
            if self.__balance + self.__overdraft_limit >= amount:
                self.__balance -= amount
                self.__record_transaction('WITHDRAWAL', -amount)
                return True
            else:
                raise ValueError("残高不足です")
        return False

    def transfer(self, amount, target_account):
        """振込処理"""
        if self.withdraw(amount):
            target_account.deposit(amount)
            self.__record_transaction('TRANSFER_OUT', -amount)
            target_account.__record_transaction('TRANSFER_IN', amount)
            return True
        return False

    def get_transaction_history(self, recent_count=10):
        """取引履歴を取得(限定公開)"""
        recent_transactions = self.__transaction_history[-recent_count:]
        # 機密情報をマスクして返す
        masked_history = []
        for transaction in recent_transactions:
            masked_transaction = transaction.copy()
            masked_transaction['balance'] = '***'
            masked_history.append(masked_transaction)
        return masked_history

    def close_account(self):
        """口座を閉鎖"""
        if self.__balance == 0:
            self.__is_active = False
            return True
        else:
            raise ValueError("残高が0でないと口座を閉鎖できません")

    def set_overdraft_limit(self, limit):
        """当座貸越限度額を設定(制限付き)"""
        # 実際のシステムではより複雑な検証が必要
        if 0 <= limit <= 100000:  # 10万円まで
            self.__overdraft_limit = limit
            return True
        return False

コンポジションによるカプセル化

継承ではなくコンポジション(包含)を使用することで、より柔軟なカプセル化を実現できます。次のコードは、コンポジション(合成)とカプセル化を組み合わせた設計の例です。

CarクラスはEngineクラスのインスタンスを内部に持ち、エンジン関連の操作(始動・停止・走行距離の加算など)をすべてEngineオブジェクトに委譲しています。これにより、エンジンの詳細(馬力・燃料・走行距離など)はCarから直接操作できず、start_engine()drive()などの限定的なインターフェースを通じてのみ利用可能です。

class Engine:
    def __init__(self, horsepower, fuel_type):
        self.__horsepower = horsepower
        self.__fuel_type = fuel_type
        self.__is_running = False
        self.__mileage = 0

    def start(self):
        if not self.__is_running:
            self.__is_running = True
            return True
        return False

    def stop(self):
        if self.__is_running:
            self.__is_running = False
            return True
        return False

    def add_mileage(self, distance):
        if distance > 0 and self.__is_running:
            self.__mileage += distance
            return True
        return False

    def get_maintenance_info(self):
        return {
            'horsepower': self.__horsepower,
            'fuel_type': self.__fuel_type,
            'mileage': self.__mileage,
            'needs_oil_change': self.__mileage % 5000 == 0
        }

class Car:
    def __init__(self, brand, model, engine_horsepower, fuel_type):
        self._brand = brand
        self._model = model
        self.__engine = Engine(engine_horsepower, fuel_type)  # コンポジション
        self.__speed = 0

    def start_engine(self):
        return self.__engine.start()

    def stop_engine(self):
        return self.__engine.stop()

    def drive(self, distance):
        if self.__engine.add_mileage(distance):
            self.__speed = min(120, distance / 10)  # 単純化した速度計算
            return True
        return False

    def get_car_info(self):
        engine_info = self.__engine.get_maintenance_info()
        return {
            'brand': self._brand,
            'model': self._model,
            'current_speed': self.__speed,
            'engine_info': engine_info
        }

    # エンジン内部への直接アクセスは許可しない
    # 必要な機能はCarクラスを通じて提供する

このような構造により、Engineの内部実装を隠蔽しつつ、Carクラスは必要な機能だけを外部に公開できます。結果として、部品同士が独立し保守性の高いオブジェクト設計が実現されています。

カプセル化の設計原則

最小権限の原則

オブジェクトは、そのタスクを実行するために必要な最小限の権限のみを持つべきです。

情報隠蔽

実装の詳細を隠し、安定した公開インターフェースのみを提供すべきです。

不変性の確保

可能な限り、オブジェクトの状態を不変(イミュータブル)に保つことで、予期しない変更を防ぎます。

デザインパターンとの関連

カプセル化は多くのデザインパターンの基礎となります。

ファサードパターン

ファサードパターンとは、複雑なシステムの内部構造を隠蔽し、外部には単純で統一されたインターフェースを提供するデザインパターンです。これにより、利用者は内部の詳細を意識せずにシステムを簡単に扱えるようになります。

プロキシパターン

プロキシパターンとは、実際のオブジェクトへのアクセスを隠蔽し、代わりに代理オブジェクト(プロキシ)が処理を仲介するデザインパターンです。これにより、アクセス制御や遅延処理、キャッシュなどを実オブジェクトの外側で柔軟に管理できます。

オブザーバーパターン

オブザーバーパターンとは、あるオブジェクトの状態変化を隠蔽しつつ、依存する複数のオブジェクト(オブザーバー)へ自動的に通知する仕組みを提供するデザインパターンです。これにより、状態変化と通知処理を分離し、柔軟で拡張性の高い設計が可能になります。

まとめ

カプセル化は、オブジェクト指向プログラミングにおいて、コードの品質と信頼性を高める重要な概念です。Pythonでは命名規則を通じてアクセスレベルを表現し、プロパティを使用して属性アクセスを制御します。

適切なカプセル化を行うことで、データの整合性を保ちつつ、内部実装の変更による影響を局所化し、利用者にとって使いやすいインターフェースを提供するとともに、セキュリティの向上も実現できます。


演習問題

初級問題

問題1
次のクラスをカプセル化してください。age属性が0以上120以下であることを保証する検証を追加してください。

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

問題2
BankAccountクラスを作成し、残高(balance)を非公開属性として保護してください。残高はdepositwithdrawメソッドでのみ変更できるようにしてください。

問題3
Temperatureクラスを作成し、摂氏温度を保護属性として管理してください。華氏温度とケルビン温度をプロパティで計算できるようにしてください。

中級問題

問題4
Studentクラスを作成し、成績リストを保護してください。成績は0から100の間のみ追加できるようにし、平均点を計算するメソッドを追加してください。

問題5
Employeeクラスを作成し、給与情報をカプセル化してください。基本給とボーナスを別々に管理し、総支給額をプロパティで計算できるようにしてください。

問題6
PasswordManagerクラスを作成し、パスワードを非公開属性として安全に管理してください。パスワードの強度検証と変更機能を実装してください。

問題7
InventoryItemクラスを作成し、在庫数量を保護してください。在庫切れの警告と在庫補充の機能を追加してください。

問題8
CarクラスとEngineクラスを作成し、コンポジションを使用してエンジンの詳細をカプセル化してください。エンジンの状態はCarクラスを通じてのみ操作できるようにしてください。

問題9
Userクラスを作成し、メールアドレスを保護属性として管理してください。メールアドレスの形式検証と変更機能を実装してください。

上級問題

問題10
LibraryクラスとBookクラスを作成し、書籍の貸出状態をカプセル化してください。貸出履歴を非公開属性で管理し、統計情報を提供するメソッドを追加してください。

問題11
StockPortfolioクラスを作成し、株の保有情報をカプセル化してください。各銘柄の購入価格と数量を保護し、現在の評価額と損益を計算する機能を実装してください。

問題12
HealthMonitoringSystemクラスを作成し、ユーザーの健康データを厳重に保護してください。バイタルサインの記録、トレンド分析、異常値の検出機能を実装し、データへのアクセスを制御してください。

初級問題

問題1 解答

class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = None  # 初期化してからセッターを使用
        self.age = age  # セッターを呼び出して検証

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

    @name.setter
    def name(self, value):
        if isinstance(value, str) and len(value) >= 2:
            self._name = value
        else:
            raise ValueError("名前は2文字以上の文字列である必要があります")

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        if isinstance(value, int) and 0 <= value <= 120:
            self._age = value
        else:
            raise ValueError("年齢は0から120の整数である必要があります")

    def display_info(self):
        print(f"名前: {self.name}, 年齢: {self.age}歳")

# 使用例
try:
    person = Person("山田太郎", 25)
    person.display_info()  # 名前: 山田太郎, 年齢: 25歳

    person.age = 30  # 正常に更新
    person.age = 150  # ValueError: 年齢は0から120の整数である必要があります
except ValueError as e:
    print(f"エラー: {e}")

解説: プロパティを使用して年齢の検証を行い、無効な値が設定されないように保護しています。

問題2 解答

class BankAccount:
    def __init__(self, account_holder, initial_balance=0):
        self._account_holder = account_holder
        self.__balance = initial_balance  # 非公開属性
        self.__transaction_count = 0

    def deposit(self, amount):
        """預入処理"""
        if amount > 0:
            self.__balance += amount
            self.__transaction_count += 1
            print(f"{amount}円預け入れました。残高: {self.__balance}円")
            return True
        else:
            print("預入額は正の値である必要があります")
            return False

    def withdraw(self, amount):
        """引出処理"""
        if amount > 0:
            if amount <= self.__balance:
                self.__balance -= amount
                self.__transaction_count += 1
                print(f"{amount}円引き出しました。残高: {self.__balance}円")
                return True
            else:
                print("残高不足です")
                return False
        else:
            print("引出額は正の値である必要があります")
            return False

    def get_balance(self):
        """残高を取得(公開メソッド)"""
        return self.__balance

    def get_transaction_count(self):
        """取引回数を取得"""
        return self.__transaction_count

    @property
    def account_holder(self):
        return self._account_holder

# 使用例
account = BankAccount("山田太郎", 10000)
print(f"口座名義: {account.account_holder}")
print(f"残高: {account.get_balance()}円")  # ゲッターを使用

account.deposit(5000)
account.withdraw(2000)
account.withdraw(15000)  # 残高不足

# 直接アクセスしようとするとエラーにはならないが、慣習的に非公開
# print(account.__balance)  # これはマングリングされるためアクセスできない

問題3 解答

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius  # 保護属性

    @property
    def celsius(self):
        """摂氏温度を取得"""
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        """摂氏温度を設定(検証付き)"""
        if value < -273.15:
            raise ValueError("絶対零度以下は設定できません")
        self._celsius = value

    @property
    def fahrenheit(self):
        """華氏温度を計算(読み取り専用)"""
        return (self._celsius * 9/5) + 32

    @property
    def kelvin(self):
        """ケルビン温度を計算(読み取り専用)"""
        return self._celsius + 273.15

    def display_all(self):
        """すべての温度単位で表示"""
        print(f"摂氏: {self.celsius}°C")
        print(f"華氏: {self.fahrenheit:.2f}°F")
        print(f"ケルビン: {self.kelvin:.2f}K")

# 使用例
temp = Temperature(25)
temp.display_all()
# 摂氏: 25°C
# 華氏: 77.00°F
# ケルビン: 298.15K

temp.celsius = 30
print(f"変更後の華氏: {temp.fahrenheit:.2f}°F")  # 変更後の華氏: 86.00°F

try:
    temp.celsius = -300  # ValueError: 絶対零度以下は設定できません
except ValueError as e:
    print(f"エラー: {e}")

中級問題

問題4 解答

class Student:
    def __init__(self, name, student_id):
        self._name = name
        self._student_id = student_id
        self.__grades = []  # 非公開属性
        self.__is_graduated = False

    def add_grade(self, grade):
        """成績を追加(検証付き)"""
        if isinstance(grade, (int, float)) and 0 <= grade <= 100:
            self.__grades.append(grade)
            print(f"成績 {grade}点 を追加しました")
            return True
        else:
            print("成績は0から100の数値である必要があります")
            return False

    def remove_grade(self, index):
        """指定したインデックスの成績を削除"""
        if 0 <= index < len(self.__grades):
            removed_grade = self.__grades.pop(index)
            print(f"成績 {removed_grade}点 を削除しました")
            return True
        else:
            print("無効なインデックスです")
            return False

    def calculate_average(self):
        """平均点を計算"""
        if not self.__grades:
            return 0
        return sum(self.__grades) / len(self.__grades)

    def get_grade_report(self):
        """成績レポートを取得(限定公開)"""
        if not self.__grades:
            return "成績がありません"

        average = self.calculate_average()
        max_grade = max(self.__grades)
        min_grade = min(self.__grades)

        return {
            'grades_count': len(self.__grades),
            'average': average,
            'max_grade': max_grade,
            'min_grade': min_grade,
            'grade_letters': [self._convert_to_letter(grade) for grade in self.__grades]
        }

    def _convert_to_letter(self, grade):
        """成績を評価記号に変換(保護メソッド)"""
        if grade >= 90: return 'A'
        elif grade >= 80: return 'B'
        elif grade >= 70: return 'C'
        elif grade >= 60: return 'D'
        else: return 'F'

    def check_graduation_eligibility(self):
        """卒業資格を確認"""
        if len(self.__grades) >= 8 and self.calculate_average() >= 60:
            self.__is_graduated = True
            return True
        return False

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

    @property
    def student_id(self):
        return self._student_id

    @property
    def is_graduated(self):
        return self.__is_graduated

# 使用例
student = Student("佐藤花子", "S12345")

# 成績追加
student.add_grade(85)
student.add_grade(92)
student.add_grade(78)
student.add_grade(65)
student.add_grade(88)

# 無効な成績
student.add_grade(150)  # エラー

# 成績レポート
report = student.get_grade_report()
print(f"平均点: {report['average']:.1f}")
print(f"最高点: {report['max_grade']}")
print(f"評価記号: {report['grade_letters']}")

# 卒業資格確認
if student.check_graduation_eligibility():
    print("卒業資格があります")
else:
    print("卒業資格がありません")

問題5 解答

class Employee:
    def __init__(self, name, employee_id, base_salary):
        self._name = name
        self._employee_id = employee_id
        self._base_salary = base_salary  # 保護属性
        self.__bonus = 0  # 非公開属性
        self.__performance_rating = 1.0

    @property
    def base_salary(self):
        return self._base_salary

    @base_salary.setter
    def base_salary(self, value):
        if value >= 0:
            self._base_salary = value
            self.__calculate_bonus()  # ボーナス再計算
        else:
            raise ValueError("基本給は0以上である必要があります")

    @property
    def performance_rating(self):
        return self.__performance_rating

    @performance_rating.setter
    def performance_rating(self, value):
        if 0.5 <= value <= 2.0:
            self.__performance_rating = value
            self.__calculate_bonus()  # ボーナス再計算
        else:
            raise ValueError("評価は0.5から2.0の間である必要があります")

    def __calculate_bonus(self):
        """ボーナスを計算(非公開メソッド)"""
        self.__bonus = self._base_salary * (self.__performance_rating - 1.0)

    @property
    def total_salary(self):
        """総支給額(読み取り専用プロパティ)"""
        return self._base_salary + self.__bonus

    @property
    def bonus(self):
        """ボーナス額(読み取り専用プロパティ)"""
        return self.__bonus

    def get_salary_breakdown(self):
        """給与内訳を取得"""
        return {
            'base_salary': self._base_salary,
            'performance_rating': self.__performance_rating,
            'bonus': self.__bonus,
            'total_salary': self.total_salary
        }

    def apply_raise(self, percentage):
        """昇給を適用"""
        if percentage > 0:
            self._base_salary *= (1 + percentage / 100)
            self.__calculate_bonus()  # ボーナス再計算
            print(f"{percentage}%の昇給を適用しました")
            return True
        return False

# 使用例
emp = Employee("山田太郎", "E001", 500000)

print("初期状態:")
breakdown = emp.get_salary_breakdown()
print(f"基本給: {breakdown['base_salary']}円")
print(f"ボーナス: {breakdown['bonus']}円")
print(f"総支給額: {breakdown['total_salary']}円")

print("\n評価を上げた後:")
emp.performance_rating = 1.5
breakdown = emp.get_salary_breakdown()
print(f"ボーナス: {breakdown['bonus']}円")
print(f"総支給額: {breakdown['total_salary']}円")

print("\n昇給後:")
emp.apply_raise(10)
breakdown = emp.get_salary_breakdown()
print(f"基本給: {breakdown['base_salary']}円")
print(f"総支給額: {breakdown['total_salary']}円")

問題6 解答

import hashlib
import re

class PasswordManager:
    def __init__(self):
        self.__stored_passwords = {}  # 非公開属性 {service: hashed_password}

    def __validate_password_strength(self, password):
        """パスワードの強度を検証(非公開メソッド)"""
        if len(password) < 8:
            return False, "パスワードは8文字以上である必要があります"

        if not re.search(r'[a-z]', password):
            return False, "パスワードには小文字を含めてください"

        if not re.search(r'[A-Z]', password):
            return False, "パスワードには大文字を含めてください"

        if not re.search(r'\d', password):
            return False, "パスワードには数字を含めてください"

        if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
            return False, "パスワードには特殊文字を含めてください"

        return True, "パスワードの強度は十分です"

    def __hash_password(self, password):
        """パスワードをハッシュ化(非公開メソッド)"""
        return hashlib.sha256(password.encode()).hexdigest()

    def add_password(self, service, password):
        """パスワードを追加"""
        is_valid, message = self.__validate_password_strength(password)
        if is_valid:
            hashed_password = self.__hash_password(password)
            self.__stored_passwords[service] = hashed_password
            print(f"サービス '{service}' のパスワードを追加しました")
            return True
        else:
            print(f"パスワード強度不足: {message}")
            return False

    def verify_password(self, service, password):
        """パスワードを検証"""
        if service not in self.__stored_passwords:
            print(f"サービス '{service}' は登録されていません")
            return False

        hashed_input = self.__hash_password(password)
        if self.__stored_passwords[service] == hashed_input:
            print("パスワードが正しいです")
            return True
        else:
            print("パスワードが間違っています")
            return False

    def change_password(self, service, old_password, new_password):
        """パスワードを変更"""
        if self.verify_password(service, old_password):
            return self.add_password(service, new_password)
        return False

    def remove_service(self, service, password):
        """サービスを削除"""
        if self.verify_password(service, password):
            del self.__stored_passwords[service]
            print(f"サービス '{service}' を削除しました")
            return True
        return False

    def get_services_list(self):
        """登録済みサービスのリストを取得"""
        return list(self.__stored_passwords.keys())

    def get_security_report(self):
        """セキュリティレポートを生成"""
        total_services = len(self.__stored_passwords)
        return {
            'total_services': total_services,
            'services_list': self.get_services_list(),
            'security_level': '高' if total_services > 0 else '低'
        }

# 使用例
pm = PasswordManager()

# パスワード追加(強度不足)
pm.add_password("email", "weak")  # 強度不足

# パスワード追加(強度十分)
pm.add_password("email", "StrongPass123!")

# 別のサービス
pm.add_password("bank", "SecureBank456@")

# パスワード検証
pm.verify_password("email", "wrongpass")  # 間違い
pm.verify_password("email", "StrongPass123!")  # 正しい

# パスワード変更
pm.change_password("email", "StrongPass123!", "NewSecure789#")

# セキュリティレポート
report = pm.get_security_report()
print(f"登録サービス数: {report['total_services']}")
print(f"セキュリティレベル: {report['security_level']}")

問題7 解答

class InventoryItem:
    def __init__(self, item_id, name, price, initial_stock=0):
        self._item_id = item_id
        self._name = name
        self._price = price
        self.__stock_quantity = initial_stock  # 非公開属性
        self.__low_stock_threshold = 10
        self.__reorder_quantity = 50

    def sell(self, quantity):
        """商品を販売"""
        if quantity <= 0:
            print("販売数量は正の値である必要があります")
            return False

        if self.__stock_quantity >= quantity:
            self.__stock_quantity -= quantity
            print(f"'{self._name}'を{quantity}個販売しました")

            # 在庫チェック
            self.__check_stock_level()
            return True
        else:
            print(f"在庫不足です。現在の在庫: {self.__stock_quantity}個")
            return False

    def restock(self, quantity=None):
        """在庫を補充"""
        if quantity is None:
            quantity = self.__reorder_quantity

        if quantity > 0:
            self.__stock_quantity += quantity
            print(f"'{self._name}'を{quantity}個補充しました。現在の在庫: {self.__stock_quantity}個")
            return True
        else:
            print("補充数量は正の値である必要があります")
            return False

    def __check_stock_level(self):
        """在庫レベルをチェック(非公開メソッド)"""
        if self.__stock_quantity <= self.__low_stock_threshold:
            print(f"警告: '{self._name}'の在庫が少なくなっています(残り: {self.__stock_quantity}個)")
            print("在庫の補充を検討してください")

    def set_low_stock_threshold(self, threshold):
        """低在庫しきい値を設定"""
        if threshold >= 0:
            self.__low_stock_threshold = threshold
            print(f"低在庫しきい値を{threshold}に設定しました")
            return True
        return False

    def set_reorder_quantity(self, quantity):
        """発注数量を設定"""
        if quantity > 0:
            self.__reorder_quantity = quantity
            print(f"発注数量を{quantity}に設定しました")
            return True
        return False

    def get_inventory_status(self):
        """在庫状況を取得"""
        status = "十分"
        if self.__stock_quantity == 0:
            status = "在庫切れ"
        elif self.__stock_quantity <= self.__low_stock_threshold:
            status = "少ない"

        return {
            'item_id': self._item_id,
            'name': self._name,
            'price': self._price,
            'stock_quantity': self.__stock_quantity,
            'status': status,
            'total_value': self._price * self.__stock_quantity
        }

    @property
    def item_id(self):
        return self._item_id

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

    @property
    def price(self):
        return self._price

# 使用例
item = InventoryItem("P001", "ノートパソコン", 150000, 25)

# 在庫状況確認
status = item.get_inventory_status()
print(f"在庫状況: {status['status']} ({status['stock_quantity']}個)")

# 販売
item.sell(5)
item.sell(15)  # 在庫が少なくなる

# 在庫切れテスト
item.sell(10)  # 在庫不足

# 補充
item.restock()

# 設定変更
item.set_low_stock_threshold(5)
item.set_reorder_quantity(30)

# 最終状況
status = item.get_inventory_status()
print(f"商品価値総額: {status['total_value']}円")

問題8 解答

class Engine:
    def __init__(self, horsepower, fuel_type, cylinders):
        self.__horsepower = horsepower  # 非公開属性
        self.__fuel_type = fuel_type
        self.__cylinders = cylinders
        self.__is_running = False
        self.__mileage = 0
        self.__oil_level = 100  # %

    def start(self):
        if not self.__is_running and self.__oil_level > 10:
            self.__is_running = True
            print("エンジンを始動しました")
            return True
        elif self.__oil_level <= 10:
            print("オイルが不足しています。エンジンを始動できません")
            return False
        else:
            print("エンジンは既に稼働中です")
            return False

    def stop(self):
        if self.__is_running:
            self.__is_running = False
            print("エンジンを停止しました")
            return True
        else:
            print("エンジンは既に停止中です")
            return False

    def add_mileage(self, distance):
        if self.__is_running and distance > 0:
            self.__mileage += distance
            # オイル消費(簡易的な計算)
            self.__oil_level = max(0, self.__oil_level - distance * 0.001)
            return True
        return False

    def change_oil(self):
        self.__oil_level = 100
        print("オイルを交換しました")

    def get_maintenance_info(self):
        return {
            'horsepower': self.__horsepower,
            'fuel_type': self.__fuel_type,
            'cylinders': self.__cylinders,
            'mileage': self.__mileage,
            'oil_level': f"{self.__oil_level:.1f}%",
            'needs_maintenance': self.__mileage % 10000 == 0
        }

    def _get_internal_status(self):
        """内部状態を取得(保護メソッド)"""
        return {
            'is_running': self.__is_running,
            'oil_level': self.__oil_level
        }

class Car:
    def __init__(self, brand, model, engine_horsepower, fuel_type, cylinders):
        self._brand = brand
        self._model = model
        self.__engine = Engine(engine_horsepower, fuel_type, cylinders)  # コンポジション
        self.__speed = 0
        self.__fuel_level = 100  # %

    def start_engine(self):
        return self.__engine.start()

    def stop_engine(self):
        return self.__engine.stop()

    def drive(self, distance):
        if self.__engine.add_mileage(distance):
            self.__speed = min(180, distance / 0.1)  # 簡易的な速度計算
            self.__fuel_level = max(0, self.__fuel_level - distance * 0.05)
            print(f"{distance}km 走行しました。速度: {self.__speed:.1f}km/h")

            if self.__fuel_level <= 10:
                print("警告: 燃料が少なくなっています")

            return True
        else:
            print("エンジンが稼働していないか、無効な距離です")
            return False

    def refuel(self):
        self.__fuel_level = 100
        print("燃料を満タンにしました")

    def change_engine_oil(self):
        self.__engine.change_oil()

    def get_car_info(self):
        engine_info = self.__engine.get_maintenance_info()
        engine_status = self.__engine._get_internal_status()

        return {
            'brand': self._brand,
            'model': self._model,
            'current_speed': self.__speed,
            'fuel_level': f"{self.__fuel_level:.1f}%",
            'engine_info': engine_info,
            'engine_status': engine_status
        }

    def get_maintenance_alert(self):
        engine_info = self.__engine.get_maintenance_info()
        alerts = []

        if engine_info['needs_maintenance']:
            alerts.append("エンジンのメンテナンスが必要です")

        if self.__fuel_level <= 10:
            alerts.append("燃料を補充してください")

        engine_status = self.__engine._get_internal_status()
        if float(engine_status['oil_level'].rstrip('%')) <= 20:
            alerts.append("オイル交換が必要です")

        return alerts

# 使用例
car = Car("Toyota", "Camry", 203, "gasoline", 4)

# 車の操作
car.start_engine()
car.drive(50)
car.drive(100)

# 車情報の取得
info = car.get_car_info()
print(f"ブランド: {info['brand']}")
print(f"モデル: {info['model']}")
print(f"走行距離: {info['engine_info']['mileage']}km")
print(f"燃料残量: {info['fuel_level']}")

# メンテナンスアラート
alerts = car.get_maintenance_alert()
if alerts:
    print("\nメンテナンスアラート:")
    for alert in alerts:
        print(f"- {alert}")

# エンジン内部への直接アクセスはできない
# car.__engine.__horsepower  # アクセス不可

問題9 解答

import re

class User:
    def __init__(self, username, email):
        self._username = username
        self._email = None  # 初期化してからセッターを使用
        self.email = email  # セッターを呼び出して検証
        self.__is_email_verified = False  # 非公開属性

    def __validate_email(self, email):
        """メールアドレスの形式を検証(非公開メソッド)"""
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        return re.match(pattern, email) is not None

    @property
    def username(self):
        return self._username

    @username.setter
    def username(self, value):
        if isinstance(value, str) and 3 <= len(value) <= 20:
            self._username = value
        else:
            raise ValueError("ユーザー名は3〜20文字の文字列である必要があります")

    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if self.__validate_email(value):
            if self._email != value:  # 変更がある場合のみ
                self._email = value
                self.__is_email_verified = False  # 確認が必要
                print("メールアドレスを変更しました。確認が必要です。")
        else:
            raise ValueError("無効なメールアドレス形式です")

    @property
    def is_email_verified(self):
        return self.__is_email_verified

    def verify_email(self, verification_code):
        """メールアドレスを確認"""
        # 実際のシステムではより複雑なロジック
        expected_code = "123456"  # 仮の確認コード
        if verification_code == expected_code:
            self.__is_email_verified = True
            print("メールアドレスが確認されました")
            return True
        else:
            print("確認コードが間違っています")
            return False

    def send_verification_email(self):
        """確認メールを送信(模擬)"""
        if self._email and not self.__is_email_verified:
            print(f"確認メールを {self._email} に送信しました")
            print("確認コード: 123456")  # 実際はランダムなコードを生成
            return True
        elif self.__is_email_verified:
            print("メールアドレスは既に確認済みです")
            return False
        else:
            print("有効なメールアドレスが設定されていません")
            return False

    def get_user_info(self):
        """ユーザー情報を取得"""
        email_status = "確認済み" if self.__is_email_verified else "未確認"

        return {
            'username': self._username,
            'email': self._email,
            'email_status': email_status,
            'can_login': self.__is_email_verified
        }

    def reset_password(self):
        """パスワードリセット処理"""
        if self._email:
            print(f"パスワードリセットリンクを {self._email} に送信しました")
            return True
        else:
            print("メールアドレスが設定されていません")
            return False

# 使用例
try:
    user = User("yamada_taro", "yamada@example.com")

    # ユーザー情報表示
    info = user.get_user_info()
    print(f"ユーザー名: {info['username']}")
    print(f"メールアドレス: {info['email']}")
    print(f"メールステータス: {info['email_status']}")

    # メール確認プロセス
    user.send_verification_email()
    user.verify_email("123456")

    # メールアドレス変更
    user.email = "taro.yamada@newdomain.com"
    user.send_verification_email()

    # 無効なメールアドレス
    user.email = "invalid-email"  # ValueError

except ValueError as e:
    print(f"エラー: {e}")

上級問題

問題10 解答

from datetime import datetime, timedelta

class Book:
    def __init__(self, book_id, title, author, isbn, total_copies=1):
        self._book_id = book_id
        self._title = title
        self._author = author
        self._isbn = isbn
        self.__total_copies = total_copies  # 非公開属性
        self.__available_copies = total_copies
        self.__borrow_records = []  # 非公開属性

    def borrow_book(self, user_id, borrow_days=14):
        """本を貸し出す"""
        if self.__available_copies > 0:
            borrow_date = datetime.now()
            due_date = borrow_date + timedelta(days=borrow_days)

            record = {
                'user_id': user_id,
                'borrow_date': borrow_date,
                'due_date': due_date,
                'return_date': None,
                'is_overdue': False
            }

            self.__borrow_records.append(record)
            self.__available_copies -= 1

            print(f"'{self._title}'を{user_id}さんに貸し出しました")
            print(f"返却期限: {due_date.strftime('%Y-%m-%d')}")
            return True
        else:
            print("すべての貸出可能なコピーが貸し出されています")
            return False

    def return_book(self, user_id):
        """本を返却する"""
        for record in self.__borrow_records:
            if record['user_id'] == user_id and record['return_date'] is None:
                record['return_date'] = datetime.now()

                # 延滞チェック
                if record['return_date'] > record['due_date']:
                    record['is_overdue'] = True
                    overdue_days = (record['return_date'] - record['due_date']).days
                    print(f"延滞しています。延滞日数: {overdue_days}日")

                self.__available_copies += 1
                print(f"'{self._title}'を{user_id}さんから返却を受けました")
                return True

        print(f"{user_id}さんはこの本を借りていません")
        return False

    def __calculate_overdue_rate(self):
        """延滞率を計算(非公開メソッド)"""
        total_borrows = len(self.__borrow_records)
        if total_borrows == 0:
            return 0

        overdue_count = sum(1 for record in self.__borrow_records if record['is_overdue'])
        return (overdue_count / total_borrows) * 100

    def get_book_status(self):
        """本の状態を取得"""
        return {
            'book_id': self._book_id,
            'title': self._title,
            'author': self._author,
            'total_copies': self.__total_copies,
            'available_copies': self.__available_copies,
            'borrowed_copies': self.__total_copies - self.__available_copies,
            'availability': '利用可能' if self.__available_copies > 0 else '貸出中'
        }

    def get_borrowing_statistics(self):
        """貸出統計を取得"""
        total_borrows = len(self.__borrow_records)
        current_borrows = sum(1 for record in self.__borrow_records if record['return_date'] is None)

        return {
            'total_borrows': total_borrows,
            'current_borrows': current_borrows,
            'overdue_rate': f"{self.__calculate_overdue_rate():.1f}%",
            'popularity': '高' if total_borrows > 10 else '低'
        }

    def get_borrow_history(self, limit=5):
        """貸出履歴を取得(限定公開)"""
        recent_records = self.__borrow_records[-limit:]

        formatted_records = []
        for record in recent_records:
            status = "返却済み" if record['return_date'] else "貸出中"
            if record['is_overdue']:
                status += " (延滞)"

            formatted_records.append({
                'user_id': record['user_id'],
                'borrow_date': record['borrow_date'].strftime('%Y-%m-%d'),
                'due_date': record['due_date'].strftime('%Y-%m-%d'),
                'status': status
            })

        return formatted_records

    @property
    def book_id(self):
        return self._book_id

    @property
    def title(self):
        return self._title

    @property
    def author(self):
        return self._author

class Library:
    def __init__(self, name):
        self._name = name
        self.__books = {}  # 非公開属性 {book_id: Book}
        self.__users = set()  # 非公開属性

    def add_book(self, book):
        """本を図書館に追加"""
        self.__books[book.book_id] = book
        print(f"'{book.title}'を図書館に追加しました")

    def register_user(self, user_id):
        """ユーザーを登録"""
        if user_id not in self.__users:
            self.__users.add(user_id)
            print(f"ユーザー {user_id} を登録しました")
            return True
        else:
            print(f"ユーザー {user_id} は既に登録されています")
            return False

    def borrow_book(self, book_id, user_id):
        """本を貸し出す"""
        if user_id not in self.__users:
            print(f"ユーザー {user_id} は登録されていません")
            return False

        if book_id in self.__books:
            return self.__books[book_id].borrow_book(user_id)
        else:
            print(f"本ID {book_id} は見つかりません")
            return False

    def return_book(self, book_id, user_id):
        """本を返却する"""
        if book_id in self.__books:
            return self.__books[book_id].return_book(user_id)
        else:
            print(f"本ID {book_id} は見つかりません")
            return False

    def get_library_report(self):
        """図書館レポートを生成"""
        total_books = len(self.__books)
        total_borrows = sum(len(book._Book__borrow_records) for book in self.__books.values())
        available_books = sum(1 for book in self.__books.values() 
                            if book.get_book_status()['available_copies'] > 0)

        return {
            'library_name': self._name,
            'total_books': total_books,
            'total_users': len(self.__users),
            'available_books': available_books,
            'total_borrows': total_borrows,
            'utilization_rate': f"{(total_borrows / (total_books * 10)) * 100:.1f}%"  # 簡易的な利用率
        }

    def find_available_books(self):
        """利用可能な本のリストを取得"""
        available_books = []
        for book in self.__books.values():
            status = book.get_book_status()
            if status['available_copies'] > 0:
                available_books.append({
                    'title': status['title'],
                    'author': status['author'],
                    'available_copies': status['available_copies']
                })
        return available_books

# 使用例
library = Library("中央図書館")

# 本の追加
book1 = Book("B001", "Python入門", "山田太郎", "978-1234567890", 3)
book2 = Book("B002", "機械学習実践", "佐藤花子", "978-0987654321", 2)

library.add_book(book1)
library.add_book(book2)

# ユーザー登録
library.register_user("U001")
library.register_user("U002")

# 本の貸出
library.borrow_book("B001", "U001")
library.borrow_book("B001", "U002")

# 図書館レポート
report = library.get_library_report()
print(f"総蔵書数: {report['total_books']}")
print(f"登録ユーザー数: {report['total_users']}")
print(f"利用可能な本: {report['available_books']}")

# 利用可能な本のリスト
available = library.find_available_books()
print("\n利用可能な本:")
for book in available:
    print(f"- {book['title']} (残り{book['available_copies']}冊)")

問題11 解答

class StockPortfolio:
    def __init__(self, owner_name):
        self._owner_name = owner_name
        self.__holdings = {}  # 非公開属性 {symbol: {'quantity': int, 'purchase_price': float}}
        self.__transaction_history = []  # 非公開属性

    def buy_stock(self, symbol, quantity, purchase_price):
        """株を購入"""
        if quantity <= 0 or purchase_price <= 0:
            print("数量と価格は正の値である必要があります")
            return False

        if symbol in self.__holdings:
            # 既存の保有銘柄を更新
            total_quantity = self.__holdings[symbol]['quantity'] + quantity
            # 平均購入価格を計算
            total_cost = (self.__holdings[symbol]['quantity'] * 
                         self.__holdings[symbol]['purchase_price'] + 
                         quantity * purchase_price)
            average_price = total_cost / total_quantity

            self.__holdings[symbol]['quantity'] = total_quantity
            self.__holdings[symbol]['purchase_price'] = average_price
        else:
            # 新規銘柄を追加
            self.__holdings[symbol] = {
                'quantity': quantity,
                'purchase_price': purchase_price
            }

        # 取引履歴を記録
        transaction = {
            'type': 'BUY',
            'symbol': symbol,
            'quantity': quantity,
            'price': purchase_price,
            'total_amount': quantity * purchase_price,
            'timestamp': self._get_current_timestamp()
        }
        self.__transaction_history.append(transaction)

        print(f"{symbol}を{quantity}株、{purchase_price}円で購入しました")
        return True

    def sell_stock(self, symbol, quantity, sell_price):
        """株を売却"""
        if symbol not in self.__holdings:
            print(f"{symbol}は保有していません")
            return False

        if quantity <= 0 or sell_price <= 0:
            print("数量と価格は正の値である必要があります")
            return False

        if quantity > self.__holdings[symbol]['quantity']:
            print(f"保有数量を超える売却はできません。保有: {self.__holdings[symbol]['quantity']}株")
            return False

        # 売却処理
        purchase_price = self.__holdings[symbol]['purchase_price']
        profit_loss = (sell_price - purchase_price) * quantity

        self.__holdings[symbol]['quantity'] -= quantity

        # 保有数量が0になったら削除
        if self.__holdings[symbol]['quantity'] == 0:
            del self.__holdings[symbol]

        # 取引履歴を記録
        transaction = {
            'type': 'SELL',
            'symbol': symbol,
            'quantity': quantity,
            'price': sell_price,
            'total_amount': quantity * sell_price,
            'profit_loss': profit_loss,
            'timestamp': self._get_current_timestamp()
        }
        self.__transaction_history.append(transaction)

        action = "利益" if profit_loss > 0 else "損失"
        print(f"{symbol}を{quantity}株、{sell_price}円で売却しました")
        print(f"{action}: {abs(profit_loss):.0f}円")
        return True

    def _get_current_timestamp(self):
        """現在のタイムスタンプを取得(保護メソッド)"""
        from datetime import datetime
        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    def get_current_value(self, current_prices):
        """現在の評価額を計算"""
        total_value = 0
        total_cost = 0

        for symbol, holding in self.__holdings.items():
            if symbol in current_prices:
                current_price = current_prices[symbol]
                current_value = holding['quantity'] * current_price
                cost_basis = holding['quantity'] * holding['purchase_price']

                total_value += current_value
                total_cost += cost_basis
            else:
                print(f"警告: {symbol}の現在価格が不明です")

        total_profit_loss = total_value - total_cost
        return {
            'total_cost': total_cost,
            'total_value': total_value,
            'total_profit_loss': total_profit_loss,
            'return_rate': (total_profit_loss / total_cost * 100) if total_cost > 0 else 0
        }

    def get_holdings_detail(self, current_prices=None):
        """保有銘柄の詳細を取得"""
        holdings_detail = []

        for symbol, holding in self.__holdings.items():
            detail = {
                'symbol': symbol,
                'quantity': holding['quantity'],
                'purchase_price': holding['purchase_price'],
                'cost_basis': holding['quantity'] * holding['purchase_price']
            }

            if current_prices and symbol in current_prices:
                current_price = current_prices[symbol]
                current_value = holding['quantity'] * current_price
                profit_loss = current_value - detail['cost_basis']

                detail.update({
                    'current_price': current_price,
                    'current_value': current_value,
                    'profit_loss': profit_loss,
                    'return_rate': (profit_loss / detail['cost_basis'] * 100) if detail['cost_basis'] > 0 else 0
                })

            holdings_detail.append(detail)

        return holdings_detail

    def get_portfolio_summary(self):
        """ポートフォリオの概要を取得"""
        total_holdings = len(self.__holdings)
        total_transactions = len(self.__transaction_history)

        # 最近の取引
        recent_transactions = self.__transaction_history[-5:] if self.__transaction_history else []

        return {
            'owner_name': self._owner_name,
            'total_holdings': total_holdings,
            'total_transactions': total_transactions,
            'recent_activity': len(recent_transactions)
        }

    def get_performance_report(self, current_prices):
        """パフォーマンスレポートを生成"""
        current_value = self.get_current_value(current_prices)
        holdings_detail = self.get_holdings_detail(current_prices)

        # トップパフォーマーとワーストパフォーマー
        if holdings_detail and all('profit_loss' in holding for holding in holdings_detail):
            top_performer = max(holdings_detail, key=lambda x: x['profit_loss'])
            worst_performer = min(holdings_detail, key=lambda x: x['profit_loss'])
        else:
            top_performer = worst_performer = None

        return {
            'portfolio_value': current_value['total_value'],
            'total_profit_loss': current_value['total_profit_loss'],
            'total_return_rate': f"{current_value['return_rate']:.2f}%",
            'top_performer': top_performer,
            'worst_performer': worst_performer,
            'risk_level': self._assess_risk_level(holdings_detail)
        }

    def _assess_risk_level(self, holdings_detail):
        """リスクレベルを評価(保護メソッド)"""
        if not holdings_detail:
            return "低"

        # 簡易的なリスク評価(銘柄数に基づく)
        num_holdings = len(holdings_detail)
        if num_holdings == 1:
            return "高"
        elif num_holdings <= 3:
            return "中"
        else:
            return "低"

# 使用例
portfolio = StockPortfolio("山田太郎")

# 株の購入
portfolio.buy_stock("AAPL", 10, 15000)
portfolio.buy_stock("GOOGL", 5, 250000)
portfolio.buy_stock("AAPL", 5, 16000)  # 追加購入

# ポートフォリオ概要
summary = portfolio.get_portfolio_summary()
print(f"保有者: {summary['owner_name']}")
print(f"保有銘柄数: {summary['total_holdings']}")

# 現在価格(仮定)
current_prices = {
    "AAPL": 17000,
    "GOOGL": 260000
}

# 現在の評価額
current_value = portfolio.get_current_value(current_prices)
print(f"評価額: {current_value['total_value']:.0f}円")
print(f"損益: {current_value['total_profit_loss']:.0f}円")
print(f"リターン率: {current_value['return_rate']:.2f}%")

# 保有銘柄詳細
holdings = portfolio.get_holdings_detail(current_prices)
print("\n保有銘柄詳細:")
for holding in holdings:
    print(f"{holding['symbol']}: {holding['quantity']}株, "
          f"損益: {holding['profit_loss']:.0f}円")

# パフォーマンスレポート
performance = portfolio.get_performance_report(current_prices)
print(f"\nリスクレベル: {performance['risk_level']}")

問題12 解答

import json
from datetime import datetime, timedelta

class HealthMonitoringSystem:
    def __init__(self, user_id):
        self._user_id = user_id
        self.__vital_signs = {}  # 非公開属性 {date: {vital_type: value}}
        self.__medical_history = []  # 非公開属性
        self.__access_log = []  # 非公開属性
        self.__is_data_encrypted = True

    def __log_access(self, action, accessed_by="system"):
        """アクセスを記録(非公開メソッド)"""
        log_entry = {
            'timestamp': datetime.now(),
            'action': action,
            'accessed_by': accessed_by,
            'user_id': self._user_id
        }
        self.__access_log.append(log_entry)

    def __validate_vital_sign(self, vital_type, value):
        """バイタルサインの値を検証(非公開メソッド)"""
        ranges = {
            'heart_rate': (40, 200),
            'blood_pressure_systolic': (80, 200),
            'blood_pressure_diastolic': (50, 120),
            'body_temperature': (35.0, 42.0),
            'blood_oxygen': (70, 100)
        }

        if vital_type not in ranges:
            return False, f"未知のバイタルサインタイプ: {vital_type}"

        min_val, max_val = ranges[vital_type]
        if min_val <= value <= max_val:
            return True, "正常範囲内"
        else:
            return False, f"異常値: {value} ({vital_type})"

    def record_vital_sign(self, vital_type, value, timestamp=None):
        """バイタルサインを記録"""
        if timestamp is None:
            timestamp = datetime.now()

        date_key = timestamp.strftime("%Y-%m-%d")

        # 値の検証
        is_valid, message = self.__validate_vital_sign(vital_type, value)
        if not is_valid:
            print(f"警告: {message}")
            # 実際のシステムでは例外を投げるか、記録しない

        if date_key not in self.__vital_signs:
            self.__vital_signs[date_key] = {}

        if vital_type not in self.__vital_signs[date_key]:
            self.__vital_signs[date_key][vital_type] = []

        record = {
            'value': value,
            'timestamp': timestamp,
            'is_abnormal': not is_valid
        }

        self.__vital_signs[date_key][vital_type].append(record)
        self.__log_access(f"record_vital_sign:{vital_type}")

        print(f"{vital_type}を記録しました: {value}")
        return True

    def add_medical_event(self, event_type, description, severity="medium", timestamp=None):
        """医療イベントを追加"""
        if timestamp is None:
            timestamp = datetime.now()

        event = {
            'event_type': event_type,
            'description': description,
            'severity': severity,
            'timestamp': timestamp,
            'resolved': False
        }

        self.__medical_history.append(event)
        self.__log_access("add_medical_event")

        print(f"医療イベントを記録しました: {event_type}")
        return True

    def __analyze_trends(self, vital_type, days=7):
        """トレンドを分析(非公開メソッド)"""
        end_date = datetime.now()
        start_date = end_date - timedelta(days=days)

        values = []
        dates = []

        current_date = start_date
        while current_date <= end_date:
            date_key = current_date.strftime("%Y-%m-%d")
            if date_key in self.__vital_signs and vital_type in self.__vital_signs[date_key]:
                daily_values = [record['value'] for record in self.__vital_signs[date_key][vital_type]]
                if daily_values:
                    values.append(sum(daily_values) / len(daily_values))
                    dates.append(current_date)
            current_date += timedelta(days=1)

        if len(values) < 2:
            return None

        # 簡易的なトレンド分析
        if values[-1] > values[0] * 1.1:
            trend = "上昇"
        elif values[-1] < values[0] * 0.9:
            trend = "下降"
        else:
            trend = "安定"

        return {
            'vital_type': vital_type,
            'trend': trend,
            'current_value': values[-1],
            'change_percentage': ((values[-1] - values[0]) / values[0]) * 100,
            'data_points': len(values)
        }

    def __detect_anomalies(self, vital_type, value):
        """異常値を検出(非公開メソッド)"""
        thresholds = {
            'heart_rate': {'low': 60, 'high': 100},
            'body_temperature': {'low': 36.0, 'high': 37.5},
            'blood_oxygen': {'low': 95, 'high': 100}
        }

        if vital_type not in thresholds:
            return False

        threshold = thresholds[vital_type]
        return value < threshold['low'] or value > threshold['high']

    def get_health_summary(self, days=30):
        """健康サマリーを取得"""
        end_date = datetime.now()
        start_date = end_date - timedelta(days=days)

        vital_stats = {}
        anomalies = []
        trends = []

        # 主要なバイタルサインの分析
        for vital_type in ['heart_rate', 'body_temperature', 'blood_oxygen']:
            trend = self.__analyze_trends(vital_type, days)
            if trend:
                trends.append(trend)

            # 最新の値を取得
            latest_value = self.get_latest_vital_sign(vital_type)
            if latest_value:
                if self.__detect_anomalies(vital_type, latest_value):
                    anomalies.append({
                        'vital_type': vital_type,
                        'value': latest_value,
                        'timestamp': datetime.now()
                    })

        # 医療イベントの統計
        recent_events = [event for event in self.__medical_history 
                        if event['timestamp'] >= start_date]

        return {
            'user_id': self._user_id,
            'period': f"{days}日間",
            'vital_signs_monitored': len([vt for vt in self.__vital_signs.keys()]),
            'recent_anomalies': len(anomalies),
            'medical_events': len(recent_events),
            'trends': trends,
            'health_status': self.__assess_overall_health(anomalies, recent_events),
            'recommendations': self.__generate_recommendations(anomalies, trends)
        }

    def get_latest_vital_sign(self, vital_type):
        """最新のバイタルサインを取得"""
        if not self.__vital_signs:
            return None

        latest_date = max(self.__vital_signs.keys())
        if vital_type in self.__vital_signs[latest_date]:
            records = self.__vital_signs[latest_date][vital_type]
            if records:
                return records[-1]['value']
        return None

    def __assess_overall_health(self, anomalies, medical_events):
        """全体的な健康状態を評価(非公開メソッド)"""
        if not anomalies and not medical_events:
            return "良好"
        elif len(anomalies) <= 2 and len(medical_events) == 0:
            return "注意必要"
        else:
            return "要医療相談"

    def __generate_recommendations(self, anomalies, trends):
        """推奨事項を生成(非公開メソッド)"""
        recommendations = []

        if anomalies:
            recommendations.append("異常なバイタルサインが検出されました。医療専門家に相談してください。")

        for trend in trends:
            if trend['trend'] == "上昇" and trend['vital_type'] == 'heart_rate':
                recommendations.append("心拍数が上昇傾向にあります。ストレス管理を見直してください。")
            elif trend['trend'] == "下降" and trend['vital_type'] == 'blood_oxygen':
                recommendations.append("血中酸素濃度が低下傾向にあります。呼吸練習を検討してください。")

        if not recommendations:
            recommendations.append("現在の健康状態は良好です。現状維持を心がけてください。")

        return recommendations

    def get_access_log(self, requester="doctor"):
        """アクセスログを取得(限定公開)"""
        if requester not in ["doctor", "system_admin"]:
            return "アクセス権限がありません"

        self.__log_access("view_access_log", requester)

        recent_logs = self.__access_log[-10:]  # 直近10件のみ
        formatted_logs = []

        for log in recent_logs:
            formatted_logs.append({
                'timestamp': log['timestamp'].strftime("%Y-%m-%d %H:%M:%S"),
                'action': log['action'],
                'accessed_by': log['accessed_by']
            })

        return formatted_logs

    @property
    def user_id(self):
        return self._user_id

    @property
    def data_encryption_status(self):
        return "有効" if self.__is_data_encrypted else "無効"

# 使用例
health_system = HealthMonitoringSystem("U12345")

# バイタルサインの記録
health_system.record_vital_sign("heart_rate", 72)
health_system.record_vital_sign("body_temperature", 36.8)
health_system.record_vital_sign("blood_oxygen", 98)
health_system.record_vital_sign("heart_rate", 85)  # 少し高い

# 異常値の記録(テスト)
health_system.record_vital_sign("heart_rate", 45)  # 低すぎる
health_system.record_vital_sign("body_temperature", 38.5)  # 発熱

# 医療イベントの記録
health_system.add_medical_event("checkup", "定期健康診断", "low")

# 健康サマリーの取得
summary = health_system.get_health_summary(7)
print(f"ユーザーID: {summary['user_id']}")
print(f"健康状態: {summary['health_status']}")
print(f"検出された異常: {summary['recent_anomalies']}件")

print("\n推奨事項:")
for recommendation in summary['recommendations']:
    print(f"- {recommendation}")

print(f"\nデータ暗号化: {health_system.data_encryption_status}")

# アクセスログ(医師権限)
access_log = health_system.get_access_log("doctor")
print(f"\n最近のアクセス:")
for log in access_log:
    print(f"{log['timestamp']} - {log['action']} by {log['accessed_by']}")