Effective Python 学習備忘録 6日目 【6/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ページ

## 今日の進捗

- 進行状況：56-60ページ
- 第3章：クラスと継承
- 本日学んだことの中で、よく忘れるところ、知らなかったところを書いていきます。


### 辞書やタプルで記録管理するよりもヘルパークラスを使う

辞書やタプルで記録管理するのは、使いやすい反面、複雑になってしまうと可読性を著しく下げてしまう。
一例として、あるお店の売り上げを記録するクラスを定義する。

``` code
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))
```

出力結果

``` result
850.0
```

このクラスを拡張して、商品ごとの売上を管理するようにする場合を考える。

``` code
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店の売り上げの平均を算出
```

出力結果

``` result
Apple: 110.0
Orange: 90.0
```

さらに、賞味期限が近いものは値引きするという機能を実装する。

``` code
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は通常のタプルと異なり、キーワード引数でも指定可能。

``` code
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())
```

## まとめ

- 値が他の辞書や長いタプルであるような辞書は作らない
- 辞書が複雑になったら、複数のヘルパークラスを使うように変更する