はじめに
Twitterで一時期流行していた 100 Days Of Code なるものを先日知りました。本記事は、初学者である私が100日の学習を通してどの程度成長できるか記録を残すこと、アウトプットすることを目的とします。誤っている点、読みにくい点多々あると思います。ご指摘いただけると幸いです!
今回学習する教材
-
Effective Python
- 8章構成
- 本章216ページ
今日の進捗
- 進行状況:56-60ページ
- 第3章:クラスと継承
- 本日学んだことの中で、よく忘れるところ、知らなかったところを書いていきます。
辞書やタプルで記録管理するよりもヘルパークラスを使う
辞書やタプルで記録管理するのは、使いやすい反面、複雑になってしまうと可読性を著しく下げてしまう。
一例として、あるお店の売り上げを記録するクラスを定義する。
class SimpleSalesRecord(object):
def __init__(self):
self._record = {}
def add_shop_id(self, shop_id):
self._record[shop_id] = []
def report_record(self, shop_id, price):
self._record[shop_id].append(price)
def average_price(self, shop_id):
records = self._record[shop_id]
return sum(records) / len(records)
record = SimpleSalesRecord()
record.add_shop_id(111)
record.report_record(111, 90)
record.report_record(111, 80)
print(record.average_price(111))
出力結果
850.0
このクラスを拡張して、商品ごとの売上を管理するようにする場合を考える。
class ByitemSalesRecord(object):
def __init__(self):
self._record = {}
def add_shop_id(self, shop_id):
self._record[shop_id] = {} # リストから辞書に変更
def report_record(self, shop_id, item, price):
by_item = self._record[shop_id]
record_list = by_item.setdefault(item, [])
record_list.append(price)
def average_price(self, shop_id):
by_item = self._record[shop_id]
total, count = 0, 0
for prices in by_item.values():
total += sum(prices)
count += len(prices)
return total / count
record = ByitemSalesRecord()
record.add_shop_id(111) # shop_idが111の店を追加
record.report_record(111, 'Apple', 100) # 111店の売り上げを追加
record.report_record(111, 'Apple', 120) # 111店の売り上げを追加
print(record.average_price(111)) # 111店の売り上げの平均を算出
出力結果
Apple: 110.0
Orange: 90.0
さらに、賞味期限が近いものは値引きするという機能を実装する。
class DiscountSalesRecord(object):
def __init__(self):
self._record = {}
def add_shop_id(self, shop_id):
self._record[shop_id] = {} # リストから辞書に変更
def report_record(self, shop_id, item, price, weight):
by_item = self._record[shop_id]
record_list = by_item.setdefault(item, [])
record_list.append((price, weight))
def average_price(self, shop_id):
by_item = self._record[shop_id]
price_sum, price_count = 0, 0
for item, prices in by_item.items():
item_avg, total_weight = 0, 0
for price, weight in prices:
price_sum += price * weight
price_count += 1
return price_sum / price_count
record = DiscountSalesRecord()
record.add_shop_id(111) # shop_idが111の店を追加
record.report_record(111, 'Apple', 100, 0.8) # 位置引数が何を意味するのか明確ではない
record.report_record(111, 'Apple', 120, 1)
print(record.average_price(111)) # 111店の売り上げの平均を算出
average_priceメソッドでは、ループの中にループがあり、読みにくくなっている。このような複雑さが生じた場合は、辞書とタプルからクラス階層にシフトすべき頃合いである。
クラスへのリファクタリング
上記のコードをクラスへリファクタリングしたコードは以下のようになる。
collectionモジュールのnamedtupleは通常のタプルと異なり、キーワード引数でも指定可能。
import collections
Record = collections.namedtuple('Record', ('price', 'weight'))
class Item(object):
def __init__(self):
self._records = []
def report_record(self, price, weight=1):
self._records.append(Record(price, weight))
def average_price(self):
total, total_weight = 0, 0
for record in self._records:
total += record.price * record.weight
return total / len(self._records)
class Shop_ID(object):
def __init__(self):
self._shop_items = {}
def item(self, name):
if name not in self._shop_items:
self._shop_items[name] = Item()
return self._shop_items[name]
def average_price(self):
total, count = 0, 0
for shop_item in self._shop_items.values():
total += shop_item.average_price()
count += 1
return total / count
class SalesRecord(object):
def __init__(self):
self._shop_id = {}
def shop_id(self, name):
if name not in self._shop_id:
self._shop_id[name] = Shop_ID()
return self._shop_id[name]
record = SalesRecord()
shop_111 = record.shop_id(111)
apple = shop_111.item('Apple')
apple.report_record(100, 0.8)
apple.report_record(100)
print(shop_111.average_price())
まとめ
- 値が他の辞書や長いタプルであるような辞書は作らない
- 辞書が複雑になったら、複数のヘルパークラスを使うように変更する