Pythonの@property

クラスの中で@propertyのデコレータを使うと、
属性として取り出すことが可能になります。

```py
class Point:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self._x 

    @property
    def y(self):
        return self._y

    @property
    def sum(self):
        return self._x + self._y
    
point = Point(10, 20)
print(point.x, point.y, point.sum)
# 10 20 30
```

この状態で再代入しようとしてもエラーが出てしまいます。
xを15に置き換えようとします。

```py
point.x = 15
```
すると下記のエラーがでます。

![2021-06-12_19h03_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428239/0817abd2-70af-7ab1-bdbe-a7f2b1aaa225.png)


## setterの設定

再代入でエラー表示させないためにsetterを設定していきます。

```py
class Point:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value
    
    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        self._y = value

    @property
    def sum(self):
        return self._x + self._y
    
point = Point(10, 20)
print(point.x, point.y, point.sum)
# 10 20 30
```

このように各プロパティでsetterを用意すると再代入が可能になります。

```py
point.x = 15
print(point.x, point.y, point.sum)
# 15 20 35
```


## setterの設定を限定させる

例えば、0より大きい値しか入れさせたくない場合は
下記のようにsetterを書いてあげます。

```py
@x.setter
def x(self, value):
    if value <= 0:
        raise ValueError(f'xは0より大きい値にしてください。入力値：{value}')
    self._x = value
```


全体のコードは下記になります。


```py
class Point:
    def __init__(self, x, y):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        if value <= 0:
            raise ValueError(f'xは0より大きい値にしてください。入力値：{value}')
        self._x = value
    
    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        self._y = value

    @property
    def sum(self):
        return self._x + self._y
```



ただし、この方法だとインスタンス時にエラーが表示されません。

```py
point = Point(-10, 20) # 負の値を入れてもエラーにならない
print(point.x, point.y, point.sum)
```

インスタンス時にsetterを動かして入力制限をする方法を次に記載します。

## インスタンス化時点で引数の制限を実装する方法

親クラスを作成して継承させるとインスタンス時の引数も判定してくれます。

```py
class Meta:
    def __init__(self, x, y):
      self.x = x
      self.y = y

class Point(Meta):
    def __init__(self, x, y):
        super().__init__(x, y)
        
    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        if value <= 0:
            raise ValueError(f'xは0より大きい値にしてください。入力値：{value}')
        self._x = value

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        self._y = value

    @property
    def sum(self):
        return self._x + self._y
```

このように設定した場合、インスタンス時の引数でxに負の値を渡すとエラーが生じます。

```py
# こちらは問題なく動く
point = Point(10, 20)
print(point.x, point.y, point.sum)
# 10 20 30

# 負の値を渡すとエラーになる
point = Point(-10, 20)
# ValueError: xは0より大きい値にしてください。入力値：-10
```


## どのようなときにプロパティの設定をするのか？

これは完全に自分で考えた内容なので、違うかもしれない前提で読んでいただきたいと思います。

プロパティを設定する場合は大きく2つあるかなと思います。

1. 参照と変更が簡単に可能になる
2. メソッドなどで指定しなくても変更したら自動計算されて出力が可能

2に関しては上記例題のsumの例です。

前提として、プロパティの設定をする際は`setter`と一緒に使わないと意味がないです。
なぜなら`@property`を設定しなくてもコンストラクタの内容は
属性として取得可能だからです。

pandasの例だと`DataFrame.columns`がわかりやすいと思います。
項目名を変えるときは下記のように変更が可能です。

```py
print(df.columns) #これで項目名一覧の取得が可能
df.columns = ['new_col1', 'new_col2', 'new_col3']
```

自分で作成しclassでも活用したいなと思いつつもどういうときに使用するかが
明確でなかったので、言語化してみました。