閉じる

Pythonのpropertyでゲッター・セッター卒業|@property装飾子を解説

Pythonでのクラス設計に慣れてくると、JavaなどでおなじみのgetXやsetXのメソッドスタイルに違和感を覚える方も多いです。

Pythonでは@propertyを使うことで、見た目は単なる属性アクセスのまま、裏側で柔軟な制御やバリデーションを行うことができます。

本記事では、「ゲッター・セッターのメソッドから卒業して、Pythonicなproperty設計へ移行する」ための考え方と実装パターンを丁寧に解説します。

Pythonのpropertyとは何か

propertyで実現するPythonicなゲッター・セッター

Pythonのpropertyは、属性アクセスを通じてゲッターやセッターのロジックを隠蔽する仕組みです。

従来のJava的なスタイルでは、次のようにメソッドで値を取得・設定します。

  • 取得時にobj.get_name()
  • 設定時にobj.set_name("Taro")

一方、Pythonicなスタイルでは以下のように書きます。

  • 取得時にobj.name
  • 設定時にobj.name = "Taro"

表にすると次のようになります。

スタイル取得の書き方設定の書き方説明
Java風メソッドobj.get_x()obj.set_x(v)メソッド呼び出しで明示的に操作します
Python属性(素の状態)obj.xobj.x = v単なるフィールドアクセスです
Python + propertyobj.xobj.x = v見た目は属性、裏で関数ロジックが動きます

propertyを使うと、見た目はシンプルな属性アクセスのまま、後からバリデーションやログ出力などのロジックを追加できる点が大きな魅力です。

@property装飾子の基本構文と使い方

Pythonでは、@property装飾子をメソッドに付けることで、そのメソッドを「属性のように」参照できるようになります

基本形は次の通りです。

Python
class Person:
    def __init__(self, name):
        # 実際の保存先は「_name」という内部用属性にします
        self._name = name

    @property
    def name(self):
        """nameのgetter"""
        # ここにログ出力などのロジックを入れてもよい
        return self._name

    @name.setter
    def name(self, value):
        """nameのsetter"""
        # ここでバリデーションなどを行う
        if not value:
            raise ValueError("nameは空文字にはできません")
        self._name = value

このクラスは次のように利用します。

Python
p = Person("Taro")

# getterの呼び出し (内部的にはp.name()ではなく、p.nameという属性アクセス)
print(p.name)          # → "Taro"

# setterの呼び出し (内部的にはp.name(value)ではなく、代入によって呼ばれます)
p.name = "Jiro"

# バリデーションに引っかかる例
p.name = ""            # ValueErrorが発生
実行結果
Taro
Traceback (most recent call last):
  ...
ValueError: nameは空文字にはできません

外側から見ると単なるp.nameという属性にしか見えませんが、内部ではgetter・setterメソッドが動いていることがポイントです。

@propertyによるゲッター実装

シンプルなgetterの例とメリット

まずは最もシンプルなgetterだけを定義するケースを見ていきます。

以下の例では、外部からはageという属性のように見えるものの、内部実装を隠蔽していることに注目してください。

Python
class User:
    def __init__(self, name, age):
        # 実体は先頭にアンダースコアを付けた内部用属性に保持します
        self._name = name
        self._age = age

    @property
    def age(self):
        """年齢(age)のgetter"""
        # 単純に内部属性を返すだけ
        return self._age

利用例:

Python
user = User("Taro", 20)

print(user.age)   # 内部で user.age() ではなく、@propertyのメソッドが呼ばれるイメージ
実行結果
20

このように、最初は「単純に値を返すだけ」のgetterとして定義しておき、後から次のような変更を加えても呼び出し側のコードは変更不要です。

  • ログ出力を追加する
  • キャッシュを利用する
  • 別フィールドから値を組み立てて返す

一度propertyとして公開してしまえば、インターフェースはobj.attrのまま、中身のロジックだけを差し替えられる点が大きなメリットです。

計算プロパティでの@property活用

@propertyは単に内部フィールドを返すだけでなく、その場で計算した値を返す「計算プロパティ」を定義するのにも非常に向いています。

Python
class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        """幅のgetter"""
        return self._width

    @property
    def height(self):
        """高さのgetter"""
        return self._height

    @property
    def area(self):
        """面積(計算プロパティ)"""
        # 必要に応じて、ここに丸め処理や単位変換などのロジックを追加できます
        return self._width * self._height

このクラスを使う時は、次のようにシンプルに書けます。

Python
rect = Rectangle(3, 4)

print(rect.width)   # 3
print(rect.height)  # 4
print(rect.area)    # 12  (内部で width * height を計算)
実行結果
3
4
12

面積を関数rect.area()でなくrect.areaと書けることで、「値っぽさ」が明確になり、読みやすさが向上します

また、将来、計算方法を変えたくなっても呼び出し側のコードを変える必要がありません。

外部APIやDBアクセスを隠すgetter設計

より実践的な場面として、外部APIやデータベースから値を取得し、それをpropertyで見せるという設計もよく使われます。

Python
import time

class ProfileService:
    """外部APIからプロフィール情報を取得するクラスのダミー実装"""

    def fetch_profile(self, user_id):
        # 本来はHTTPリクエストやDBクエリを投げる処理が入る想定です
        print("外部サービスにアクセスしています...")
        time.sleep(0.2)  # ネットワーク待機を模擬
        return {"user_id": user_id, "nickname": "taro123"}


class User:
    def __init__(self, user_id, profile_service):
        self._user_id = user_id
        self._profile_service = profile_service
        self._cached_profile = None

    @property
    def profile(self):
        """プロフィール情報を返すgetter(外部サービスアクセスをラップ)"""
        # 一度取得したらキャッシュする例
        if self._cached_profile is None:
            self._cached_profile = self._profile_service.fetch_profile(self._user_id)
        return self._cached_profile

利用例:

Python
service = ProfileService()
user = User(user_id=1, profile_service=service)

# 最初のアクセス時だけ外部サービスにアクセスし、2回目以降はキャッシュを返します
print(user.profile)
print(user.profile)
実行結果
外部サービスにアクセスしています...
{'user_id': 1, 'nickname': 'taro123'}
{'user_id': 1, 'nickname': 'taro123'}

このように、propertyを使えば「user.profile」という自然な形で、裏側の外部APIアクセスやキャッシュ戦略を完全に隠すことができます。

呼び出し側からは「属性にアクセスしている」ようにしか見えず、実装の複雑さを意識せずに済みます。

@propertyによるセッター・バリデーション

@x.setterでのsetter定義方法

@propertyのセッターは、getterと同じ名前のメソッドに@<名前>.setterを付けて定義するのがポイントです。

Python
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius  # 内部表現は摂氏

    @property
    def celsius(self):
        """摂氏のgetter"""
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        """摂氏のsetter"""
        # ここでバリデーションや変換処理が可能
        self._celsius = float(value)

    @property
    def fahrenheit(self):
        """華氏のgetter(計算プロパティ)"""
        return self._celsius * 9 / 5 + 32

利用例:

Python
temp = Temperature(25)

print(temp.celsius)     # 25.0
print(temp.fahrenheit)  # 77.0

# setterで値を更新
temp.celsius = "30"     # 文字列でもfloatに変換される
print(temp.celsius)     # 30.0
print(temp.fahrenheit)  # 86.0
実行結果
25
77.0
30.0
86.0

getterとsetterは「同じ名前」で1つの属性を構成し、呼び出し側には扱いやすいインターフェースを提供します

内部実装では、別の単位に変換したり、型をそろえたりといった柔軟な処理が可能です。

型チェック・値チェックによる入力バリデーション

セッターの中で、型や値の範囲などをチェックし、不正な値が入らないようにすることができます。

次の例では、年齢を0〜150の範囲に制限しています。

Python
class Person:
    def __init__(self, age):
        self._age = None
        self.age = age  # ここでもsetter経由でバリデーションを通す

    @property
    def age(self):
        """年齢のgetter"""
        return self._age

    @age.setter
    def age(self, value):
        """年齢のsetter(型と値のチェック付き)"""
        # 型チェック
        if not isinstance(value, int):
            raise TypeError("ageはint型で指定してください")

        # 値の範囲チェック
        if not (0 <= value <= 150):
            raise ValueError("ageは0〜150の範囲で指定してください")

        self._age = value

利用例:

Python
p = Person(30)
print(p.age)

# 不正な型
try:
    p.age = "30"
except Exception as e:
    print("エラー(型):", e)

# 不正な値
try:
    p.age = 200
except Exception as e:
    print("エラー(値):", e)
実行結果
30
エラー(型): ageはint型で指定してください
エラー(値): ageは0〜150の範囲で指定してください

このように、セッターにバリデーションを集約しておくと、クラスの利用者がどこから値を変更しても必ずチェックが通るため、安全性が高まります。

読み取り専用プロパティ(read-only property)の作り方

読み取り専用プロパティは、getterだけ定義し、setterを定義しないことで実現できます。

Python
class Order:
    def __init__(self, order_id, total_price):
        self._order_id = order_id
        self._total_price = total_price

    @property
    def order_id(self):
        """注文IDの読み取り専用property"""
        return self._order_id

    @property
    def total_price(self):
        """合計金額の読み取り専用property"""
        return self._total_price

利用例:

Python
order = Order(order_id="A001", total_price=5000)

print(order.order_id)
print(order.total_price)

# 読み取り専用プロパティに代入しようとすると、AttributeErrorが発生します
try:
    order.order_id = "A002"
except AttributeError as e:
    print("エラー:", e)
実行結果
A001
5000
エラー: can't set attribute

「読み取り専用にしたい属性はgetterのみ」「変更可能にしたい属性はgetterとsetterの両方を定義」というシンプルなルールで管理できます。

副作用を伴うsetterの設計上の注意点

propertyのセッター内で、次のような副作用を実装することも技術的には可能です。

  • ログを出力する
  • データベースを更新する
  • 外部APIに通知を送る
  • ファイルに書き込む

しかし、setterは「単なる代入」に見えるため、重い処理や予期しにくい副作用を入れすぎると、コードの見通しが悪くなります

簡単な例を見てみます。

Python
class Config:
    def __init__(self, path):
        self._path = path
        self._content = ""

    @property
    def content(self):
        return self._content

    @content.setter
    def content(self, value):
        """ファイルにも即座に書き込むsetter(あまり推奨しない例)"""
        self._content = value
        # セッターのたびにファイル書き込み(副作用)
        with open(self._path, "w", encoding="utf-8") as f:
            f.write(self._content)

このような設計だと、何気ないconfig.content = "..."の代入がディスク書き込みを伴うため、パフォーマンスやテストのしやすさに影響します。

現場では、次のようなガイドラインを意識するとよいです。

  • 単なる代入で重い処理を隠さない(明示的なメソッドsave()などに切り出す)
  • 外部への通知などは、イベントハンドラやサービス層で行い、entityのpropertyには埋め込まない
  • どうしてもproperty内で副作用を行う場合は、ドキュメントやdocstringでしっかり明記する

propertyのセッターは「軽量な検証や整形」に留め、重い副作用は明示的なメソッドに任せるという設計が、長期的には保守性を高めます。

ゲッター・セッター卒業のベストプラクティス

Java風getter/setterとの比較と移行方法

まず、Java風のgetter/setterスタイルと@propertyスタイルをコードで比較してみます。

Python
# Java風のPythonコード(非Pythonicな例)

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

    def get_name(self):
        return self._name

    def set_name(self, value):
        if not value:
            raise ValueError("nameは空文字にはできません")
        self._name = value

これを@propertyを使ったPythonicなスタイルに書き換えると、次のようになります。

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

    @property
    def name(self):
        """nameのgetter"""
        return self._name

    @name.setter
    def name(self, value):
        """nameのsetter"""
        if not value:
            raise ValueError("nameは空文字にはできません")
        self._name = value

呼び出しコードの比較:

Python
p1 = PersonJavaStyle("Taro")
print(p1.get_name())
p1.set_name("Jiro")

p2 = PersonPythonic("Taro")
print(p2.name)
p2.name = "Jiro"
実行結果
Taro
Taro

移行の際には、次のようなステップを踏むと安全です。

  1. まず既存のget_x/set_xを残したまま、@propertyを追加する
  2. 内部でpropertyから既存メソッドを呼ぶようにして、挙動をそろえる
  3. 呼び出し側コードを少しずつobj.x形式に書き換える
  4. 最終的にget_x/set_xを非推奨(deprecated)にしてから削除する

段階的に移行することで、大きな破壊的変更を避けながらPythonicなインターフェースに整えていけます。

propertyとカプセル化・インターフェース設計

オブジェクト指向設計では、「内部の実装詳細(フィールド構造)を隠し、外から見えるインターフェースを安定させる」ことが重要です。

Pythonは言語仕様として完全なアクセス制限(例えばprivate)を持ちませんが、propertyを使うことで「事実上のカプセル化」を実現できます。

よく使われる約束事は次の通りです。

  • 先頭に_を付けた属性は「内部用」(例: _name)
  • 公開したいインターフェースは、propertyやメソッドで提供する(例: name)

この設計にしておくと、次のような変更があっても、呼び出し側に影響を与えずにすみます。

  • 内部で持つフィールド名や構造を変える
  • 値を計算で求めるように変える
  • キャッシュ戦略やバリデーションルールを差し替える

「最初から全部public属性でいいや」としてしまうと、後からの仕様変更のたびに呼び出し側コードを修正する羽目になりやすいため、長期的な運用を見据えたクラスではpropertyをうまく使うことが大切です。

既存クラスにpropertyを導入するリファクタリング手順

既存コードベースにpropertyを導入する際は、小さなステップで進めると安全です。

簡単な例を使って、典型的な流れを示します。

ステップ1: 内部属性とpublic属性の切り分け

まずは、直接アクセスされている属性を内部用にリネームし、そのままpublicな属性を残します。

Python
# 変更前
class Item:
    def __init__(self, price):
        self.price = price   # 直接アクセスされている

# 変更1: 内部用属性へリネームし、互換性のためにpublicを残す
class Item:
    def __init__(self, price):
        self._price = price   # 内部用
        self.price = price    # 既存コード用(一時)

ステップ2: propertyを定義する

次に、propertyでpriceを提供し、内部で_priceを使うようにします。

Python
class Item:
    def __init__(self, price):
        self._price = price

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

    @price.setter
    def price(self, value):
        if value < 0:
            raise ValueError("priceは0以上である必要があります")
        self._price = value

この段階で、既存のitem.priceアクセスはそのまま動きつつ、バリデーションなどのロジックを挟めるようになります。

ステップ3: 呼び出し側を整理する

もしget_price()set_price()のようなメソッドが既にある場合、内部実装をpropertyに委譲します。

Python
class Item:
    # ... (propertyは前述と同じ)

    # 既存メソッドを残しつつ、内部ではpropertyを利用
    def get_price(self):
        return self.price

    def set_price(self, value):
        self.price = value

呼び出し側では少しずつget_price()/set_price()からitem.priceに書き換えていきます。

ステップ4: 古いメソッドを段階的に廃止

十分な移行期間を経て、get_price()/set_price()が使われなくなったら、非推奨にしてから削除します。

大規模なシステムでは、ログを仕込んで利用状況を確認するのも有効です。

dataclassとpropertyを併用するパターン

Python 3.7以降では@dataclassを使うことで、ボイラープレート(コンストラクタなど)を自動生成しつつ、一部のフィールドに対してpropertyで高度なロジックを追加する、という設計がよく使われます。

Python
from dataclasses import dataclass, field

@dataclass
class Product:
    name: str
    _price: int = field(repr=False)  # 内部用フィールドとして定義(reprに出さない)

    @property
    def price(self) -> int:
        """価格のgetter"""
        return self._price

    @price.setter
    def price(self, value: int) -> None:
        """価格のsetter(バリデーション付き)"""
        if not isinstance(value, int):
            raise TypeError("priceはint型で指定してください")
        if value < 0:
            raise ValueError("priceは0以上で指定してください")
        self._price = value

利用例:

Python
p = Product(name="Apple", _price=100)
print(p)          # __repr__には_priceは表示されない
print(p.price)    # 100

p.price = 150     # setter経由で更新
print(p.price)

try:
    p.price = -1
except Exception as e:
    print("エラー:", e)
実行結果
Product(name='Apple')
100
150
エラー: priceは0以上で指定してください

このように、「大部分はdataclassでシンプルに」「一部の重要な属性だけpropertyで制御」という組み合わせは、現代的なPythonコードで非常によく見られるパターンです。

まとめ

@propertyを使うことで、getX・setXスタイルのメソッドに頼らず、シンプルな属性アクセスのままゲッター・セッターの役割を果たせるようになります。

単純な値の取得だけでなく、計算プロパティ、外部APIやDBアクセスの隠蔽、バリデーション付きセッター、読み取り専用プロパティなど、柔軟な設計が可能です。

既存クラスも段階的にpropertyへ移行できますので、Java風のgetter/setterから卒業し、Pythonicで保守性の高いインターフェース設計へシフトしていく足がかりとして活用してみてください。

クラスとオブジェクト指向

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!