はじめに
Twitterで一時期流行していた 100 Days Of Code なるものを先日知りました。本記事は、初学者である私が100日の学習を通してどの程度成長できるか記録を残すこと、アウトプットすることを目的とします。誤っている点、読みにくい点多々あると思います。ご指摘いただけると幸いです!
今回学習する教材
-
- 8章構成
- 本章216ページ
今日の進捗
- 進行状況:91-95ページ
- 第4章:メタクラスと属性
- 本日学んだことの中で、よく忘れるところ、知らなかったところを書いていきます。
属性をリファクタリングする代わりに@property
を考える
@property
を使う事で、単純な数値属性だったものを、呼び出し元を一切変えることなく、その場で必要な計算を行えるよう変更することができます。
ここで、@property
を使った場合と普通の実装を比較するために、水漏れバケツからの水割り当てクラスについての例を見てみます。
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を使って必要な水を吐き出させる。
if deduct(bucket, 99):
print('Had 99 quota')
else:
print('Not enough for 99 quota')
print(bucket)
出力結果
Had 99 quota
Bucket(quota=1) # 残りのバケツ内の水の量
ここでさらに3の水を吐き出そうとすると
if deduct(bucket, 3):
print('Had 3 quota')
else:
print('Not enough for 3 quota')
print(bucket)
出力結果
Not enough for 3 quota
Bucket(quota=1)
バケツよりも多くの量の水を排出しようとすると、そこから進めなくなり、バケツの状況が全く見えないことがこの実装の問題です。
なので、割り当て量と消費された水の量を変数に記録するようにクラスを改良します。
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
先ほどと同じ、コードを実行します。
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)
出力結果
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
で逐次的に改良しつつ、まとめてリファクタリングした方が効率がいいと理解しました。