最近流行りのエンジニアから特殊メソッドについて教わった話　〜サンドイッチ〜

# 特殊メソッドってなにそれ美味しいの？ 
pythonでダブルアンダースコアを見るとソワソワする方いらっしゃいませんか。
初学者の私はこれまで逃げてきました。
直感的にわかってないから使う発想すら出てこないんですよね。
そこで最近懇意にしているエンジニアG…4さんに教えてもらおうと思い立ちました。






####  ■*特殊メソッドを可能な限り多く含んだクラスを作ってほしい。サンドイッチの値段計算・具の追加などなどを盛り込んでみて。*

G：　特殊メソッド（マジックメソッド）を多く含んだ、サンドイッチの具材と値段を管理するためのクラスを作ります。__str__、__len__、__getitem__、__setitem__、__delitem__、__iter__、__contains__、__iadd__、__add__ などの特殊メソッドを使います。

```python:python
class Sandwich:
    def __init__(self, base_price=300):
        self.ingredients = {}
        self.base_price = base_price

    def __str__(self):
        ingredients_str = ', '.join(f"{name} (¥{price})" for name, price in self.ingredients.items())
        return f"Sandwich [Total Price: ¥{self.get_total_price()}] with {ingredients_str}"

    def __len__(self):
        return len(self.ingredients)

    def __getitem__(self, ingredient):
        return self.ingredients.get(ingredient, 0)

    def __setitem__(self, ingredient, price):
        self.ingredients[ingredient] = price

    def __delitem__(self, ingredient):
        if ingredient in self.ingredients:
            del self.ingredients[ingredient]

    def __iter__(self):
        return iter(self.ingredients)

    def __contains__(self, ingredient):
        return ingredient in self.ingredients

    def __iadd__(self, ingredient_price_pair):
        ingredient, price = ingredient_price_pair
        self.ingredients[ingredient] = price
        return self

    def __add__(self, other):
        new_sandwich = Sandwich()
        new_sandwich.ingredients = {**self.ingredients, **other.ingredients}
        return new_sandwich

    def get_total_price(self):
        return self.base_price + sum(self.ingredients.values())


# 使用例
sandwich1 = Sandwich()
sandwich1['Tomato'] = 100
sandwich1['Lettuce'] = 50
sandwich1 += ('Chicken', 200)
print(sandwich1)  # "Sandwich [Total Price: ¥650] with Tomato (¥100), Lettuce (¥50), Chicken (¥200)"

sandwich2 = Sandwich()
sandwich2['Avocado'] = 150
sandwich2['Bacon'] = 200
print(sandwich2)  # "Sandwich [Total Price: ¥650] with Avocado (¥150), Bacon (¥200)"

combo = sandwich1 + sandwich2
print(combo)  # "Sandwich [Total Price: ¥1200] with Tomato (¥100), Lettuce (¥50), Chicken (¥200), Avocado (¥150), Bacon (¥200)"
```

このクラスでは、サンドイッチに具材を追加したり、具材を取り出したり、具材が含まれているか確認したり、具材を削除したり、具材の一覧をイテレーションしたり、サンドイッチを組み合わせたりできます。価格は、ベース価格に具材の価格を加えたものとして計算します。



####  ■*ドリンクとポテトもよろしく。セット価格は多少負けてくれよな*

G：　さらにサンドイッチにポテトとドリンクを追加して、それをセットとして価格を計算し、セット価格に対して割引を適用するクラスを作成します。ここでは、SetMealクラスを作り、それにSandwichクラスのインスタンス、ポテトとドリンクの価格をセットし、それらの合計に対して割引率を適用します。

```python:python
class SetMeal:
    def __init__(self, sandwich, potato_price=200, drink_price=150, discount_rate=0.9):
        self.sandwich = sandwich
        self.potato_price = potato_price
        self.drink_price = drink_price
        self.discount_rate = discount_rate

    def get_total_price(self):
        total_price = self.sandwich.get_total_price() + self.potato_price + self.drink_price
        return total_price * self.discount_rate


set_meal = SetMeal(blt)
print(set_meal.get_total_price())  # 出力: 900.0
```

この例では、サンドイッチのベース価格が300円、ベーコンを200円、レタスを50円、トマトを100円、ポテトを200円、ドリンクを150円とし、それらの合計金額に対して10%の割引（0.9倍）を適用しています。そのため、最終的な価格は900円になります。価格、割引率等は適切に設定してください。

####  ■*優秀すぎる…それぞれの特殊メソッドをpython初心者のために解説してほしい*
G：　もちろんです、以下に各特殊メソッドの説明を記述します。
    
| 特殊メソッド | 説明 |
|:-:|:-:|
|\_\_init__(self, ...) | インスタンスが生成される時に実行される初期化メソッドです。ここでインスタンスの属性を設定します。  |
|\_\_str__(self) |  インスタンスをstr()で文字列に変換した時やprint関数で出力したときに呼び出されます。このメソッドで返す文字列が表示されます。 |
| \_\_len__(self) |  len()関数が呼び出された時に実行されます。集合の要素数を返します。   |
| \_\_getitem__(self, key)| インスタンスに対して角括弧([])を用いてアクセスしたときに呼び出されます。keyに対応する値を返します。  |
| \_\_setitem__(self, key, value)  | インスタンスに対して角括弧([])を用いて値を設定した時に呼び出されます。keyに対応する場所にvalueを設定します。 |
| \_\_delitem__(self, key)  |  delステートメントが使用されたときに実行されます。keyに対応する項目を削除します。 |
| \_\_iter__(self)  | インスタンスがイテレータとして使用される時（forループなど）に呼び出されます。イテレータオブジェクトを返します。 |
| \_\_contains__(self, item)  |  in演算子が使用されたときに実行されます。itemが存在する場合はTrue、存在しない場合はFalseを返します。 |
| \_\_iadd__(self, other)   | +=演算子が使用された時に実行されます。インスタンスにotherを加える操作を定義します。 |
| \_\_add__(self, other)  | +演算子が使用された時に実行されます。selfとotherを足し合わせた新しいインスタンスを返します。  |    

ただし、すべての特殊メソッドを必ず実装する必要はなく、クラスの設計目的により適切な特殊メソッドを選んで実装します。




（*・・・ってこの解説でわかってたら苦労しないのよ*）

####  ■*BLTはいくら？*
G：　　Sandwich クラスを使用してBLT（ベーコン、レタス、トマト）のサンドイッチの価格を計算するには、以下のように実装できます。各特殊メソッドがどのように作動するかを示すために、サンドイッチとセットメニューに関するさらなる操作を考えてみましょう。
具材の価格は仮に設定します。
```python:python
blt = Sandwich()
```
この例では、新しいサンドイッチを作るためにSandwichクラスのインスタンスを作成します。このとき、__init__メソッドが呼び出され、インスタンスが初期化されます。

```python:python
blt['Bacon'] = 200
blt['Lettuce'] = 50
blt['Tomato'] = 100
print(blt.get_total_price())  # 出力: 650

```
サンドイッチに具材を追加します。このとき、__setitem__メソッドが呼び出され、具材とその価格が追加されます。ベーコンを200円、レタスを50円、トマトを100円と設定し、サンドイッチのベース価格が300円なので、合計は650円となります。具材の価格は状況や地域により変動するため、具体的な価格を設定する際には、適切な価格を設定してください。


もしあなたがトマトが苦手で、それをサンドイッチから削除したいと思った場合、delキーワードを使って削除すると、__delitem__が動作します。

```python:python
del blt['Tomato']
```
### G:  これにより、サンドイッチからトマトが削除されます。

####  ■*は？それってただのBLじゃ・・・*

サンドイッチの具材の数を知りたい場合、len()関数を使って確認できます。これにより__len__が動作し、サンドイッチの具材の数が出力されます。
```python:python
print(len(blt))   # 出力: 2
```



サンドイッチの全ての具材を一つずつ調べたい場合、forループを使ってイテレーションできます。これにより__iter__が動作します。サンドイッチの具材が一つずつ出力されます。

```python:python
for ingredient in blt:
    print(ingredient) #出力：　Bacon　Lettuce
```

特定の具材がサンドイッチに含まれているかどうかを調べたい場合、inキーワードを使って確認できます。これにより__contains__が動作します。もしベーコンがサンドイッチに含まれていれば、その旨が出力されます。
```python:python
if 'Bacon' in blt:
    print('Bacon is in the sandwich!')　#出力：'Bacon is in the sandwich!'
```


セットメニューに新たにアイテムを追加したい場合、+=を用いて追加できます。これにより__iadd__が動作し、デザートがセットメニューに追加されます。。

```python:python

dessert = Item('Ice Cream', 300)
set_meal += dessert
```





##  ■おわり
イメージしやすい実例から、特定の操作に対応してクラスの挙動をカスタマイズする役割を果たすための特殊メソッドなんだなと、大雑把に速習できました。（挙動確認できていないかも…悪しからず）

おまけ　__next__を使ってそれっぽいサンドイッチを作ってくれます。
```python:python
class Sandwich2:
    PRICE_TABLE = {
    'base': 200,
    'lettuce': 20,
    'tomato': 30,
    'Bacon':30,
    'chicken': 50,
    'shrimp': 60,
    'cheese': 40,
    'avocado': 30,
    'plain_bun': 10,
    'honey_oats': 15,
    'under_Bun':10
    }
    def __init__(self, ingredients):
        self.ingredient_repr = {
            "Bacon": "~~~~~~~~~~~~","shrimp": " S S S S S ","chicken": " GG GG GG G ",
            "lettuce": "W^W^W^W^W^W^","tomato": "@ @ @ @","avocado": " C C C C C ",
            "cheese": "============",
            "plain": "..::::::::..","honey_oats": "..::****::..", "under_Bun":"============"
        }
        self.ingredients = ingredients
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index >= len(self.ingredients):
            raise StopIteration
        ingredient = list(self.ingredients)[self.index]
        self.index += 1
        return self.ingredient_repr.get(ingredient, "Invalid Ingredient")

    # サンドイッチ作り
    def make_sandwich(self):
        return "\n".join(str(i) for i in self)
    # 価格計算
    def calculate_price(self):
        total = Sandwich2.PRICE_TABLE['base']
        for ingredient, amount in self.ingredients.items():
            total += Sandwich2.PRICE_TABLE[ingredient] * amount
        return total


ingredients = {"honey_oats":1, "cheese":1, "Bacon":1, "chicken":1, "lettuce":2, "under_Bun":1}#レタス倍量で
my_sandwich = Sandwich2(ingredients)
print(my_sandwich.calculate_price())
print(my_sandwich.make_sandwich())
#出力
"
385
..::****::..
============
~~~~~~~~~~~~
 GG GG GG G 
W^W^W^W^W^W^
============


```