Effective Pythonまとめ 2. List and Dictionaries (Item11~Item18)

Pythonのコーディング力を引き上げるために、[Effective Python Second Edition（英語版）](https://www.amazon.co.jp/dp/B07ZG18BH3/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)を用いて学習しています。
（日本語版は訳が機械的でわかりづらいというレビューが多かったので、英語版を利用することにしました。）
今回は、第二章のList and Dictionaries (Item11~Item18)について、要点をまとめていきます。

:::note warn
個人的に解釈した内容を記述しているため、本の内容と厳密には一致しない表現が含まれる可能性があります。
:::

#  Item11: Know How to Slice Sequences

 ### 基本的なsliceの使い方
Pythonでは、list, str, bytes型などをスライスする際には、`somelist[start:end]`が利用できる。
この際、startに指定したindexに対応する要素は実行結果に含まれ、endに指定したindexに対応する要素は含まれない。

```python
a = ['a', 'b', 'c']
print(f"a[1:2]: {a[1:2]}") # a[1:2]: ['b']
```

### シーケンスの最初の要素から、最後の要素まで取得する際の注意点

シーケンスの最初のインデックスから要素を取得したい場合、startに0を指定することで取得可能である。
しかし、pythonではstartをに値を指定しないことで同じことが実現できる。
冗長さを避けるためにも、シーケンスの最初のインデックスから要素を取得したい場合には、startの値を空白にすべきである。

```python
a = ['a', 'b', 'c']
assert a[0:2] == a[:2] # True
```

最後の要素まで取得する場合にも同様のことが言える。
シーケンスの最後の要素まで取得したい場合、endに`len(some sequence)`を指定することも可能だが、
冗長さを避けるためにも、endの値は空白にすべきである。

```python
a = ['a', 'b', 'c']
assert a[1:len(a)] == a[1:] # True
```

### start, endに負の値を指定した際の挙動

startやendに負の値を指定することも可能である。例えば`-1`を指定した場合は、シーケンスの一番最後の要素が該当する。
`startに指定したindexに対応する要素は実行結果に含まれ、endに指定したindexに対応する要素は含まれない`という点は負の値を指定した場合にも同じなので注意。

```python
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
print(f"a[-3:]: {a[-3:]}") # a[-3:]: ['f', 'g', 'h']
print(f"a[:-1]: {a[:-1]}") # a[:-1]: ['a', 'b', 'c', 'd', 'e', 'f', 'g']
print(f"a[-3:-1]: {a[-3:-1]}") # a[-3:-1]: ['a', 'b', 'c', 'd', 'e', 'f', 'g']
```
### シーケンスの要素数を超える値を指定した場合の挙動

仮に、シーケンスの要素数を超える値を指定して、要素を直接取得しようとした場合exceptionが発生する。

```python
a = ['a', 'b', 'c']
print(f"a[10]: {a[10]}") # IndexError: list index out of range
```

しかし、sliceの場合には、exceptionは発生せず、存在する範囲で値を取得する。

```python
a = ['a', 'b', 'c']
print(f"a[:10]: {a[:10]}")  # a[:10]: ['a', 'b', 'c']
print(f"a[-10:]: {a[-10:]}")  # a[-10:]: ['a', 'b', 'c']
```

### リストをsliceした結果を別の変数に代入した場合の挙動

あるリストをsliceした結果をある別の変数に代入した場合、元のリストとは別の新しいリストが作成され、
sliceの結果によって作成されたリストの値を書き換えても、元のリストには影響を及ぼさない。

```python
a = ['a', 'b', 'c', 'd']
b = a[1:3]
print(f"b Before change: {b}")  # b Before change: ['b', 'c']
b[0] = 1
print(f"b After change: {b}")  # b After change: [1, 'c']
print(f"No change : {a}")  # No change : ['a', 'b', 'c', 'd']
```
### sliceで指定した範囲と代入する値の長さが異なる場合の挙動

sliceで指定した範囲と代入する値の長さは同じである必要はない。
代入する値の長さの方が大きければ、sliceで指定された範囲は拡張され、

```python
a = ['a', 'b', 'c', 'd']
a[1:2] = [1, 2, 3, 4, 5, 6]
print(f"a: {a}")  # a: ['a', 1, 2, 3, 4, 5, 6, 'c', 'd']
```
代入する値の長さの方が小さければ、sliceで指定された範囲は縮小する。

```python
a = ['a', 'b', 'c', 'd']
a[1:3] = [1]
print(f"a: {a}")  # a: ['a', 1, 'd']
```

#  Item12: Avoid Striding and Slicing in a Single Expression

### スライスのストライドの使い方
スライスする際には、`somelist[start:end:stride]`　のように書くことでインデックスをどれだけ進めるかを指定することもできる。
strideの値がインデックスを進める増分。

```python
x = ['red', 'orange', 'yellow', 'green', 'blue', 'purple']
odds = x[::2]
evens = x[1::2]
print(odds) # ['red', 'yellow', 'blue']
print(evens) # ['orange', 'green', 'purple']
```

### ストライドを利用する際の注意点
startとendのインデックスの指定とともにstrideを指定した場合、挙動が非常に理解しづらい。
特に以下のような、ストライドが負の値の場合にはより理解が難しい

```python
x = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
print(x[-2::-2])  # ['g', 'e', 'c', 'a']
print(x[2:2:-2])  # []
```
そのため、可能な限りストライドは正の数である方が望ましい。

もし、どうしてもstartとendとともにストライドを利用しなければならない場合、
以下のようにストライドの結果を変数に入れた後、スライスすることを検討する。

```python
x = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
y = x[::2]
z = y[1:-1]
print(y)  # ['a', 'c', 'e', 'g']
print(z)  # ['c', 'e']
```
ただし、一つ中間の変数に代入することで、時間やメモリを必要とする。
そのような余裕がない場合には、組み込みモジュールのitertoolsのissliceメソッドの利用を検討する。

#  Item13: Prefer Catch-All Unpacking Over Slicing

### catch-all unpackingを使うシーン
packingの一つの制限として、事前にunpackする対象のシーケンスの長さを知っておく必要があること。
unpackした結果の個数と、それを代入する個数が一致しない下記のような場合、例外が発生する。

```python
car_args = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
car_args_descending = sorted(car_args, reverse=True)
oldest, second_oldest = car_args_descending  # ValueError: too many values to unpack (expected 2)
```
この例でoldestとsecond_oldestを取得したい場合には、以下のような方法が利用できる。

```python
oldest = car_args_descending[0]
second_oldest = car_args_descending[1]
others = car_args_descending[2:]
print(oldest, second_oldest, others)  # 20 19 [15, 9, 8, 7, 6, 4, 1, 0]
```
ただし、このような書き方は視覚的にうるさく、取得する境界をインデックスで指定しているためエラーを引き起こす原因にも容易になりうる。
このような場合に、catch-all unpackingは有用である。

### catch-all unpackingの使い方

以下のように、変数の前にアスタリスク`*`をつけることで、*がついていない変数に入った値の残りの値を代入することができる。
以下の例では変数名にothersを利用しているが、任意の変数名に対して利用できる。

```python
oldest, second_oldest, *others = car_args_descending
print(oldest, second_oldest, others)  # 20 19 [15, 9, 8, 7, 6, 4, 1, 0]
```
このような方法は、短く、読みやすく、エラーにもつながりにくい。

また、以下のように任意の場所で `*`を利用できる。

```python
oldest, *others, youngest = car_args_descending
print(oldest, youngest, others)  # 20 0 [19, 15, 9, 8, 7, 6, 4, 1]
*others, second_youngest, youngest = car_args_descending
print(youngest, second_youngest, others)  # 0 1 [20, 19, 15, 9, 8, 7, 6, 4]
```

しかし、以下のように、
*がつく変数にのみunpackしたり、一度のunpackで複数の*を利用することはできない。（unpackの対象が単一階層の場合）

```python
*others = car_args_descending  # SyntaxError: starred assignment target must be in a list or tuple
first, *middle, *second_middle, last = car_args_descending  # SyntaxError: two starred expressions in assignment
```
他にも、下記のような特徴を持つ。
- 多重階層のunpackの場合には、一度のunpackで各階層に対する*が利用できる
- *の部分は常にリストのインスタンスが返却され、入る要素がない場合には、空のリストが返る
- iteratorに対しても利用可能

#  Item14: Sort by Complex Criteria Using the key Parameter

### sort関数の基本
sortメソッドは要素が組み込みのタイプのリストの中身の順序を並び替えることができる

```python
numbers = [93, 86, 11, 68, 70]
numbers.sort()
print(numbers)  # [11, 68, 70, 86, 93]
```

### sort関数の並べ替えのための基準について

ソートする対象の要素がオブジェクトの場合には、特殊なメソッドを定義しなければsortメソッドは機能しない。
以下のようにkeyパラメータにソートしたい属性を返却する関数を指定することでソートが可能で、このような方法が特殊なメソッドを定義するよりも一般的である。

```python
class Tool:
    def __init__(self, name, weight):
        self.name = name
        self.weight = weight

    def __repr__(self):
        return f'Tool({self.name!r}, {self.weight})'

tools = [
    Tool('level', 3.5),
    Tool('hammer', 1.25),
    Tool('screwdriver', 0.5),
    Tool('chisel', 0.25),
]

tools.sort(key=lambda x: x.name)
print(tools)  # [Tool('chisel', 0.25), Tool('hammer', 1.25), Tool('level', 3.5), Tool('screwdriver', 0.5)]
tools.sort(key=lambda x: x.weight)
print(tools)  # [Tool('chisel', 0.25), Tool('screwdriver', 0.5), Tool('hammer', 1.25), Tool('level', 3.5)]
```

keyパラメーターに指定するヘルパー関数からタプルを返せば、複数の基準でソートすることが可能になる。
タプルの1つ目の位置の値が同じだった場合、2つ目の位置の値で比較する。

```python
tools = [
    Tool('level', 3.5),
    Tool('hammer', 1.25),
    Tool('screwdriver', 0.25),　 # wightがchiselと同じため、nameで比較する
    Tool('chisel', 0.25),
]

tools.sort(key=lambda x: (x.weight, x.name))
print(tools)  # [Tool('chisel', 0.25), Tool('screwdriver', 0.25), Tool('hammer', 1.25), Tool('level', 3.5)]
```

-が利用できる型（intなど）であれば、ソート順を逆にすることができる。

```python
tools = [
    Tool('level', 3.5),
    Tool('hammer', 1.25),
    Tool('screwdriver', 0.25),
    Tool('chisel', 0.25),
]

tools.sort(key=lambda x: (-x.weight, x.name))
print(tools)  # [Tool('level', 3.5), Tool('hammer', 1.25), Tool('chisel', 0.25), Tool('screwdriver', 0.25)]
```
reverseパラメーターでも、ソート順の昇順、降順を操作することができるが、全ての基準に同じ順序が適用される。
```python
tools.sort(key=lambda x: (x.weight, x.name), reverse=True)
print(tools)  # [Tool('level', 3.5), Tool('hammer', 1.25), Tool('screwdriver', 0.25), Tool('chisel', 0.25)]
```

複数回sortメソッドをkeyとreverseをうまく使いながら複数回呼び出すことで基準を自由に組み合わせてソートできる。
```python
tools.sort(key=lambda x: (x.name))  # Name ascending
tools.sort(key=lambda x: (x.weight), reverse=True)  # Weight descending
print(tools)  # [Tool('level', 3.5), Tool('hammer', 1.25), Tool('chisel', 0.25), Tool('screwdriver', 0.25)]
```

#  Item15: Be Cautious When Replying on dict Insertion Ordering

### dictのイテレーションの順序について
python3.7以降はdictのイテレーションの順序はkeysが追加された順番になる。（python3.7以前では順序が保証されていなかった）

### dictのような振る舞いをするオブジェクトのイテレーションの順序にについて

pythonは実際にはdictではないが、dictのような振る舞いをするオブジェクトを生成することができる。
dictは追加された順番通りになるが、このようなオブジェクトは、イテレーションの順番が追加された順番であるということは保証されない。（クラス内で、__iter__メソッドが実装されている場合など）
このようなオブジェクトを扱う際に、イテレーションの順番と挿入の順番が一致しない事による問題を防ぐためには次の3つの方法が取れる。

1. イテレーションの順序に依存しない実装をする方法　この方法が一番保守的で、丈夫な解決策
2. 最上部に明示的に型をチェックする関数を追加し、合わなければエラーを返す方法 この方法は1.のやり方よりもよいパフォーマンスになるかもしれない
3. 型アノテーションをつけることで、dict型を強制する方法 この方法が安全性とパフォーマンスのベストな組み合わせと言える

#  Item16: Prefer get Over in and KeyError to Handle Missing Dictionary Keys

- setdefault メソッドは、キーが存在すれば値を取得し、なければ、キーと第二引数に指定したデフォルト値をdictに追加してから値を返す。
- setdefaultメソッドでキーが存在しない場合に追加された要素は、コピーではなく代入をするため、元の変数が書き変われば代入先の値も変更されてしまい、思わぬトラブルに繋がりやすい。
- setdefaultメソッドは、ごく稀なケースでgetメソッドの代わりに利用する余地はあるが、基本的にはこのような場合にも、defalutdictを使うべきのことがほとんどである。
- ディクショナリのキーの存在有無を考慮しつつ要素の取得をしたい場合の方法は4つある
    1. in表記を利用した存在チェック 
	2. KeyError exceptionを利用した例外処理
	3. getメソッドの利用
	4. setdefaultメソッドの利用
- 4.のsetdefaultメソッドはgetを利用するよりも短く記述できるが、名前と実行内容が一致していないため誤解を招きやすく可読性を下げる可能性があるので注意

#  Item17: Prefer defaultdict Over setdefault to Handle Missing Items in Internal State

### Things to remember
1. 潜在的なキーの任意のsetを管理するためのdictionaryを作ろうとしている場合、もし条件を満たすのであれば、collections組み込みモジュールのdefaultdictインスタンスを使うのが望ましい
2. 任意のキーのディクショナリが渡され、その作成を制御しない場合は、getメソッドを使用してそのアイテムにアクセスする方が好ましい。 ただし、コードが短くなるいくつかの状況では、setdefaultメソッドの使用を検討する価値がある

#  Item18: Know How to Construct Key-Dependent Default Values with __missing__

### Things to remember
1. dictのsetdefaultメソッドの利用は、引数が渡せないためデフォルト値の作成に高い計算コストがかかる場合や、例外が発生する可能性がある場合には不適切である
2. defaultdictに渡される関数は引数を取れないので、アクセスされているキーに応じてデフォルト値を設定することはできない
3. アクセスされているキーを知った上でデフォルト値を組み立てる必要がある場合のために、dictを継承した独自のクラスに__missing__メソッドを定義することができる

***

後半かなり雑になってしまったので、また気が向けばどこかで直したいと思います。
今回は以上となります。

***


参考文献
[Effective Python: 90 Specific Ways to Write Better Python (Effective Software Development Series) (English Edition)](https://www.amazon.co.jp/dp/B07ZG18BH3/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)