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

## 今日の進捗

- 進行状況：38-48ページ
- 本日学んだことの中で、よく忘れるところ、知らなかったところを書いていきます。

### 引数に対してイテレータを使うときには確実さを尊ぶ

- イテレータやジェネレータで反復処理して、StopIteration例外がすでに起きているなら、もう一度反復しても何の結果も得られない

``` code
numbers = [1, 2, 3, 4, 5]

def iter_square(numbers):
    for number in numbers:
        yield number ** 2

iter = iter_square(numbers)
for x in iter:
    print(x)
# StopIteration例外が起きているため、2回目のfor文は何も出力しない
for x in iter:
    print(x)
```

出力結果

``` result
1
4
9
16
25
```

すでに尽きてしまったイテレータに対して反復処理をしても、何のエラーも生じないため、非常に紛らわしい。この問題を解決するために、入力イテレータを明示的に尽きるまで動かし、内容全体リストにコピーしてしまう。リストには反復処理が何度でもできるので、このような紛らわしさは無くなる。

``` code
numbers = [1, 2, 3, 4, 5]

def iter_square(numbers):
    for number in numbers:
        yield number ** 2

iter = iter_square(numbers)
iter_list = list(iter)
for x in iter_list:
    print(x)
for x in iter_list:
    print(x)
```

出力結果

``` code
1
4
9
16
25
1
4
9
16
25
```

しかり、リストを用いるため、入力イテレータのコピーの内容が大きくなればクラッシュしかねない。これを回避するするために、イテレータプロトコルを実装した新たなコンテナクラスを定義する。

``` code
numbers = [1, 2, 3, 4, 5]

class SampleClass(object):
    def __init__(self, numbers):
        self.numbers = numbers

    def __iter__(self):
        for number in self.numbers
            yield number

def func_square(numbers):
    result = []
    for value in numbers:
        square_value = value ** 2
        result.append(square_value)
    return result

num = SampleClass(numbers)
result = func_square(num)
print(result)
```

### 可変長位置引数を使って、見た目をすっきりさせる

- スター引数（*args）を使うと、関数呼び出しがずっと巣kkりして、見た目のノイズが減らせる
- 関数はdef文で*argsを使うことにより、可変個数の位置引数を受け入れることができる
- *演算子を関数に用いて、シーケンスからの要素を位置引数として使うことができる
- *演算子をジェネレータと一緒に使うと、プログラムがメモリを使い果たしてクラッシュすることがある
- 新たに位置仮引数を、*args受け入れている関数に追加すると、発見が困難なバグを生み出してしまう可能性がある

例えば、デバッグ情報のログを取る際に、固定個数の引数だと、メッセージと値のリストとを取る関数が必要になる。

``` code
def log(message, values):
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print('%s: %s' % (message, values_str))

log('MY numbers are', [1, 2])
log('Hi there', [])
```

値が無い時も空リストを渡さなくてはならなく目障り。Pythonでは最後の位置引数の名前に*を付けることで、オプションにすることが可能

``` code
def log(message, *values):
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print('%s: %s' % (message, values_str))

log('MY numbers are', [1, 2])
log('Hi there')
```

すでにリストがあって、logのような可変個引数関数を呼び出したいなら、*演算子を使って呼び出せる。

``` code
log('HELLO', *list)
# *をつけないとリストのまま出力される
log('HELLO', list)
```

出力結果

``` result
HELLO: 1, 2, 3
HELLO: [1, 2, 3]
```

可変長位置引数には2つ問題がある。

1. 関数に渡される前に常にタプルに変換されること
    - 呼び出し元がジェネレータで*演算子を使っていれば、それが尽きるまでイテレーションされるため、メモリを大量に消費してプログラムをクラッシュする恐れがあるということ
2. すべての呼び出し元を修正しないと、関数に対して新たな位置引数を追加することができないこと

### キーワード引数にオプションの振る舞いを与える

- 関数の引数は、位置またはキーワードによって指定できる
- キーワード引数は、位置引数だけでは紛らわしい場合に、各引数の目的を明らかにする
- デフォルト値を設定したキーワード引数は、関数がすでに他から呼び出されている場合でも、その関数に新たな振る舞いを追加することを容易にする
- オプションのキーワード引数は、位置ではなくキーワードで常に引き渡すべきである