Effective Python 学習備忘録 15日目 【15/100】

# はじめに

Twitterで一時期流行していた 100 Days Of Code なるものを先日知りました。本記事は、初学者である私が100日の学習を通してどの程度成長できるか記録を残すこと、アウトプットすることを目的とします。誤っている点、読みにくい点多々あると思います。ご指摘いただけると幸いです！

# 今回学習する教材

- [Effective Python](https://www.amazon.co.jp/Effective-Python-%E2%80%95Python%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%82%92%E6%94%B9%E8%89%AF%E3%81%99%E3%82%8B59%E9%A0%85%E7%9B%AE-Brett-Slatkin/dp/4873117569/ref=sr_1_3?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=Effective+python&qid=1609408832&sr=8-3)

    - 8章構成
    - 本章216ページ

# 今日の進捗

- 進行状況：91-95ページ
- 第4章：メタクラスと属性
- 本日学んだことの中で、よく忘れるところ、知らなかったところを書いていきます。

## 属性をリファクタリングする代わりに`@property`を考える

`@property`を使う事で、単純な数値属性だったものを、呼び出し元を一切変えることなく、その場で必要な計算を行えるよう変更することができます。

ここで、`@property`を使った場合と普通の実装を比較するために、水漏れバケツからの水割り当てクラスについての例を見てみます。

```python
from datetime import timedelta, datetime

class Bucket(object):
    def __init__(self, period):
        '''
        Parameters
        ----------
        period: int
            バケツから水が漏れきるまでの時間を設定
        '''

        self.period_delta = timedelta(seconds=period)  # バケツから水が漏れきるまでの時間を設定
        self.reset_time = datetime.now()               # 現在時刻を reset_time に代入
        self.quota = 0

    def __repr__(self):                                # __repr__ は文字列で出力結果を返す特殊メソッド
        return 'Bucket(quota=%d)' % self.quota

def fill(bucket, amount):
    '''
    ばけつに amount 分の水を入れる
    ばけつの水が漏れ切っていた場合にはifブロックを処理する

    Parameters
    ----------
    bucket: Bucket 
    amount: int   
    '''

    now = datetime.now()
    if now - bucket.reset_time > bucket.period_delta: # 初期化時からfillを呼び出すまでの時間の方が、period_delta に設定された時間より大きい場合にこのブロックの処理をする
        bucket.quota = 0                              
        bucket.reset_time = now                       # 現在時刻を reset_time に代入
    bucket.quota += amount                            # quota に amount を追加

def deduct(bucket, amount):
    '''
    ばけつからこぼれ切っていない、かつ、水の量が amount より多い場合、amount分水を吐き出す

    Parameters
    ----------
    bucket: Bucket 
    amount: int

    Returns
    -------
    True or False
    '''
    now = datetime.now()
    if now - bucket.reset_time > bucket.period_delta:
        return False
    if bucket.quota - amount < 0:
        return False
    bucket.quota -= amount
    return True

bucket = Bucket(60)    # バケツから漏れ切るまで60秒
fill(bucket, 100)      # 100の水をばけつに追加
print(bucket)
# Bucket(quota=100)
```

deductを使って必要な水を吐き出させる。

```python
if deduct(bucket, 99):
    print('Had 99 quota')
else:
    print('Not enough for 99 quota')
print(bucket)
```

出力結果

```result
Had 99 quota
Bucket(quota=1)  # 残りのバケツ内の水の量
```

ここでさらに3の水を吐き出そうとすると

```python
if deduct(bucket, 3):
    print('Had 3 quota')
else:
    print('Not enough for 3 quota')
print(bucket)
```

出力結果

```result
Not enough for 3 quota
Bucket(quota=1)
```

バケツよりも多くの量の水を排出しようとすると、そこから進めなくなり、バケツの状況が全く見えないことがこの実装の問題です。
なので、割り当て量と消費された水の量を変数に記録するようにクラスを改良します。

```python
class Bucket(object):
    def __init__(self, period):
        self.period_delta = timedelta(seconds=period)
        self.reset_time = datetime.now()
        self.max_quota = 0                            # 最大量を記録
        self.quota_consumed = 0                       # 排出された水の量を記録

    def __repr__(self):
        return ('Bucket(max_quota=%d, quota_consumed=%d)' % (self.max_quota, self.quota_consumed))

    # @propertyメソッドを使って、追加した新たな属性を計算
    @property
    def quota(self):
        return self.max_quota - self.quota_consumed
    
    # fillとdeductで使われているクラスのインターフェースに合致するように処理を書く
    @quota.setter
    def quota(self, amount):
        delta = self.max_quota - amount
        if amount == 0:
            # 新たなピリオドのため、割り当て量をリセット
            self.quota_consumed = 0
            self.max_quota = 0
        elif delta < 0:
            # 新たなピリオドのため適当量を入れる
            assert self.quota_consumed == 0
            self.max_quota = amount
        else:
            # ピリオド内で適当量で消費される
            assert self.max_quota >= self.quota_consumed
            self.quota_consumed += delta
```

先ほどと同じ、コードを実行します。

```python
bucket = Bucket(60)
fill(bucket, 100)
print(bucket)


if deduct(bucket, 99):
    print('Had 99 quota')
else:
    print('Not enough for 99 quota')
print(bucket)


if deduct(bucket, 3):
    print('Had 3 quota')
else:
    print('Not enough for 3 quota')
print(bucket)
```

出力結果

```result
Bucket(quota=100)
Had 99 quota
Bucket(quota=1)
Not enough for 3 quota
Bucket(quota=1)
Bucket(max_quota=100, quota_consumed=0)
Had 99 quota
Bucket(max_quota=100, quota_consumed=99)
Not enough for 3 quota
Bucket(max_quota=100, quota_consumed=99)
```

`@property`を使うことで、内部の状況が見えるようになりました。

クラスをいじるならば、fillやdeductに対応するメソッドを普通に追加すればいいのでは？と思ったのですが、実問題では、`@property`を使って逐次的により良いモデルにしていくことが多いそうです。
`@property`を使いすぎて、コードが読みにくくなってきたら、そのクラスとすべての呼び出し元をリファクタリングすることを考える時期のようです。
ちまちまクラスを改良するよりも、`@property`で逐次的に改良しつつ、まとめてリファクタリングした方が効率がいいと理解しました。