【Python】プロパティ(@property)でgetter/setterを実装する方法

Python

こんにちは、かつコーチです。

他の言語からPythonに来た方が戸惑いやすいのが、getter/setterの書き方の違いです。

「JavaみたいにgetName()、setName()って書かなくていいの?」

@propertyってdecoratorの一種?何が嬉しいの?」

この記事では、@propertyの基本から、値のチェックを行うsetterの実装、そして他言語のgetter/setterとの違いまでを解説します。

読み終える頃には、インスタンス変数を安全に扱う書き方が身につきます。

@propertyの基本

なぜgetter/setterが必要なのか

インスタンス変数は、そのままobj.name = "..."のように直接読み書きできます。

ただ、これでは「不正な値が入ってきてもチェックできない」という問題があります。

例えば、年齢に-5のようなマイナス値が入っても、Pythonは何もエラーを出してくれません。

そこで、値の取得・設定時にチェック処理を挟む仕組みとしてgetter/setterが使われます。

メソッド呼び出し型のgetter/setter

まずは、他言語でもよく見る「メソッドとして呼び出す」getter/setterの書き方です。

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

    def get_age(self):
        return self._age

    def set_age(self, value):
        if value < 0:
            raise ValueError("年齢は0以上である必要があります")
        self._age = value


person = Person(20)
print(person.get_age())

person.set_age(25)
print(person.get_age())

実行結果は次の通りです。

20
25

self._ageのように先頭にアンダースコアをつけた変数名は、「直接は触らず、メソッド経由でアクセスしてほしい」という慣習的な合図です。

この書き方でも動作しますが、person.get_age()のように毎回カッコをつけて呼ぶ必要があり、少し冗長です。

@propertyでgetterを実装する

Pythonでは、@propertyを使うことで、メソッドをまるでインスタンス変数のように扱えます。

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

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


person = Person(20)

print(person.age)

実行結果は次の通りです。

20

ageメソッドに@propertyをつけることで、person.age()ではなくperson.ageという、カッコなしの書き方でアクセスできるようになります。

呼び出し側からは、通常のインスタンス変数と見分けがつかない形でgetterを提供できるのが、@propertyのメリットです。

setterの実装(@x.setter)

@property.setterで代入時の処理を追加する

@propertyだけでは、値を読み取ることしかできません。

値を代入できるようにするには、@プロパティ名.setterというデコレータをつけたメソッドを追加します。

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

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

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


person = Person(20)
print(person.age)

person.age = 25
print(person.age)

実行結果は次の通りです。

20
25

person.age = 25という、通常のインスタンス変数への代入と同じ書き方をしているにもかかわらず、内部ではageのsetterメソッドが呼ばれ、チェック処理が実行されています。

不正な値を代入するとエラーで防げる

setterの中でチェック処理を書いておけば、不正な値の代入をその場でブロックできます。

person = Person(20)

person.age = -5

実行すると、次のエラーメッセージが表示されます。

ValueError: 年齢は0以上である必要があります

もし@propertyを使わず普通のインスタンス変数のままだったら、person._age = -5はそのまま通ってしまい、後になって「年齢がマイナスの状態で計算がおかしい」というバグの発見が遅れていたはずです。

@propertyとsetterを組み合わせることで、値が入るタイミングでチェックを一元化できます。

読み取り専用のプロパティを作る

setterを定義しなければ、そのプロパティは読み取り専用になります。

計算結果など、外部から直接変更されては困る値に使えます。

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

    @property
    def area(self):
        return self.width * self.height


rect = Rectangle(4, 5)
print(rect.area)

rect.area = 100

実行結果は次の通りです。

20

そして、代入しようとした行で次のエラーが発生します。

AttributeError: property 'area' of 'Rectangle' object has no setter

areaはsetterを持たないため、代入しようとするとPython側が自動的にブロックしてくれます。

他言語のgetter/setterとの対比

Java・C#との書き方の違い

JavaやC#では、getAge()setAge()のように、明示的なメソッド呼び出しでgetter/setterを実装するのが一般的です。

Pythonでも同じ書き方は可能ですが、@propertyを使うことで、呼び出し側は通常の変数アクセスと同じ書き方のまま、裏側でチェック処理を挟めます。

観点Java/C#のgetter/setterPythonの@property
呼び出し方obj.getAge() / obj.setAge(25)obj.age / obj.age = 25
見た目メソッド呼び出しと分かる変数アクセスと区別がつかない
後から追加最初から用意するのが一般的後から@property化しても呼び出し側のコードを変えずに済む

最初は普通の変数、後からpropertyに変える

Pythonらしい設計としてよく紹介されるのが、「最初は普通のインスタンス変数として作り、チェックが必要になった時点で@propertyに切り替える」という進め方です。

# 最初はシンプルに
class Person:
    def __init__(self, age):
        self.age = age


# チェックが必要になったら、呼び出し側のコードを変えずに切り替えられる
class Person:
    def __init__(self, age):
        self._age = age

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

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

person.ageという呼び出し方は変わらないため、利用側のコードを一切修正せずに、後からチェック処理を追加できます。

最初からgetter/setterを用意する言語との、大きな考え方の違いです。

よくあるつまずきポイント・エラー対処

setterのメソッド名を間違えてプロパティが変更できない

筆者が実際にハマったのが、setterのメソッド名をage以外にしてしまったケースです。

❌ Before(意図通りに動かない書き方)

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

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

    @age.setter
    def set_age(self, value):  # メソッド名がageと一致していない
        if value < 0:
            raise ValueError("年齢は0以上である必要があります")
        self._age = value


person = Person(20)
person.age = 25

実行すると、次のエラーメッセージが表示されます。

AttributeError: property 'age' of 'Person' object has no setter

@age.setterとデコレータには正しくageを指定できているのですが、その下のメソッド名をset_ageにしてしまったため、Python側は「setterが未定義」と判断してしまいます。

✅ After(正しい書き方)

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

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

    @age.setter
    def age(self, value):  # getterと同じ「age」に揃える
        if value < 0:
            raise ValueError("年齢は0以上である必要があります")
        self._age = value


person = Person(20)
person.age = 25
print(person.age)

@propertyのgetterと@プロパティ名.setterのsetterは、必ず同じメソッド名にする必要があります。

@age.setterと書いた場合、その直下のメソッド名も必ずageに揃えることを覚えておきましょう。

まとめ

この記事のポイント

  • @propertyをつけると、メソッドをインスタンス変数のように呼び出せる
  • @プロパティ名.setterで、代入時にチェック処理を挟むsetterを実装できる
  • setterを定義しなければ、そのプロパティは読み取り専用になる
  • getterとsetterのメソッド名は必ず一致させる必要がある
  • 最初は普通の変数として作り、必要になったら@propertyに切り替えるのがPythonらしい設計

次に読むべき記事

  • クラスとオブジェクト指向の基本
  • 継承とオーバーライドの書き方
  • デコレータの基本:関数を拡張する仕組み

タグ: Python, 中級者向け, 基本文法

タイトルとURLをコピーしました