Pythonの「痒いところに手が届くなぁ・・・」と思う機能

日々のプログラミングに役立つPythonのテクニックや小ネタ、小技をまとめました。

`Python 3.8.0`で動作確認しています。

## 3値以上の比較
```python
1 == 2 == 3  # -> False
1 < 2 < 3 < 4  # -> True
```

## 時間(datetime/date)の比較
マジックメソッドのおかげで柔軟な比較が出来ます。

参考：[[Python] 独自クラスで比較演算ができるようにする](https://www.yoheim.net/blog.php?q=20171002)

```python
from datetime import date

feb1 = date(2020, 2, 1)
feb2 = date(2020, 2, 2)
feb3 = date(2020, 2, 3)

feb1 < feb2 <= feb2 < feb3  # -> True
```

## 時間の最大/最小
```python
from datetime import date

# 意図的に逆順にしてます
dates = [
    date(2020, 2, 3),
    date(2020, 2, 2),
    date(2020, 2, 1),
]

min(dates)  # -> datetime.date(2020, 2, 1)
max(dates)  # -> datetime.date(2020, 2, 3)
```

## 時間の計算
```python
# Input
from datetime import datetime

start = datetime(2020, 2, 1, 10)
goal = datetime(2020, 2, 3, 12)

t = goal - start
print(f'あなたの記録は{t.days}日と{t.seconds}秒です')

# Output
'あなたの記録は2日と7200秒です'
```

## 時間を連想配列のキーにする
日時ベースの集計をする時に便利です。

```python
# Input
from datetime import date

counts = {
    date(2020, 2, 1): 0,
    date(2020, 3, 1): 0,
}
counts[date(2020, 2, 1)] += 1
counts[date(2020, 2, 1)] += 1
counts[date(2020, 3, 1)] += 1

print(counts)

# Output
{datetime.date(2020, 2, 1): 2, datetime.date(2020, 3, 1): 1}
```

## キーが連想配列に含まれるか
```python
d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}

print('foo' in d)  # -> True
```

## 連想配列からキーを抽出
```python
d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}
print(list(d))  # -> ['foo', 'bar', 'baz']
```

## 連想配列から値を抽出
```python
d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}
print(list(d.values()))  # -> [1, 2, 3]
```

## 連想配列からキーと値のペアを抽出

```python
d = {
    'foo': 1,
    'bar': 2,
    'baz': 3,
}

for key, value in d.items():
    print(key, value)

# Output
foo 1
bar 2
baz 3
```

## 2重配列を連想配列へ変換
```python
# Input
l = [
    ['Yamada', 'baseball'],
    ['Tanaka', 'soccer'],
    ['Sato', 'tennis'],
]
dict(l)

# Output
{'Yamada': 'baseball', 'Tanaka': 'soccer', 'Sato': 'tennis'}
```

## 多重配列のループ
```python
# Input
rows = [
    ['yamada', 20],
    ['tanala', 18],
    ['sato', 18],
]

for name, age in rows:
    print(f'{name}さんは{age}歳です')
else:
    print('紹介終わり')

# Output
'yamadaさんは20歳です'
'tanalaさんは18歳です'
'satoさんは18歳です'
'紹介終わり'
```

## 多重配列から必要な要素だけ取り出しつつループ
```python
# Input
l = [
    ['Yamada', 'Taro', 20, 'baseball'],
    ['Tanaka', 'Jiro', 18, 'circle'],
]

# 先頭を取り出す
for last_name, *others in l:
    print(last_name, others)
print()

# 末尾を取り出す
for *others, circle in l:
    print(circle, others)
print()
    
# 最初の2要素を取り出す
# (他の要素が要らない場合はダブルアンダースコアを指定するのがPython流)
for last_name, first_name, *__ in l:
    print(last_name, first_name)

# Output
Yamada ['Taro', 20, 'baseball']
Tanaka ['Jiro', 18, 'circle']

baseball ['Yamada', 'Taro', 20]
circle ['Tanaka', 'Jiro', 18]

Yamada Taro
Tanaka Jiro
```

このような事もできます。

```python
# Input
l = [
    ['a', 'b', 'c', ['d', 'e', 'f']],
]

for one, *__, (*__, two) in l:
    print(one, two)

# Output
a f
```

## カウンター付きループ
```python
# Input
rows = [
    ['Yamada', 20],
    ['Tanaka', 18],
    ['Sato', 16],
]

for i, (name, age) in enumerate(rows, start=1):
    print(f'{i}行目 : 氏名={name}, 年齢={age}')

# Output
'1行目 : 氏名=Yamada, 年齢=20'
'2行目 : 氏名=Tanaka, 年齢=18'
'3行目 : 氏名=Sato, 年齢=16'
```

## 連想配列のキー無しエラーを防ぐ
いちいち`if 'キー' in dict`と書く必要が無くなります。

```python
d = {
    'Yamada': 20,
    'Tanaka': 18
}
d.get('Yamada')  # -> 20
d.get('Sato')  # -> None
d.get('Sato', '年齢なし')  # -> 年齢なし
```

## 配列/連想配列を展開して関数へ渡す
```python
# Input
def func(a, b, c=None, d=None):
    print(a)
    print(b)
    print(c)
    print(d)


l = ['aaa', 'bbb']
d = {'c': 'ccc', 'd': 'ddd'}

func(*l, **d)

# Output
aaa
bbb
ccc
ddd
```

## all関数
配列の全要素がTrueと評価出来るならTrueを返してくれます。

```python
l = [
    True,
    1,
    "foo",
]
all(l)  # -> True

l = [
    True,
    1,
    "",
]
all(l)  # -> False
```

## any関数
配列の要素の内どれか1つでもTrueと評価出来るならTrueを返してくれます。

```python
l = [
    False,
    0,
    "foo",
]
any(l)  # -> True

l = [
    False,
    0,
    "",
]
any(l)  # -> False
```

### any関数の注意点
以下の例では計算コストが高い処理(`heavy_task`)が無駄に3回も実行されています。

```python
# Input
def heavy_task(ret):
    print(ret)
    return ret


if any([heavy_task(True), heavy_task(False), heavy_task(False)]):
    print('Complete')

# Output
True
False
False
Complete
```

この場合はifを使ったほうが効率的です。

```python
# Input
def heavy_task(ret):
    print(ret)
    return ret


if heavy_task(True) or heavy_task(False) or heavy_task(False):
    print('Complete')

# Output
True
Complete
```

## 集合演算(Set型)
### 重複要素を削除する
```python
# Input
l = ['yamada', 'tanaka', 'yamada', 'sato']
print(set(l))

# Output
{'sato', 'tanaka', 'yamada'}
```

### 重複要素を抜き出す(積集合)
```python
# Input
l1 = ['yamada', 'tanaka', 'yamada', 'sato']
l2 = ['tanaka']
print(set(l1) & set(l2))

# Output
{'tanaka'}
```

### 結合した上で重複を削除する(和集合)
```python
# Input
l1 = ['yamada', 'tanaka']
l2 = ['yamada', 'sato']
print(set(l1) | set(l2))

# Output
{'sato', 'tanaka', 'yamada'}
```

### 他方から自身に含まれる要素を削除する(差集合)
```python
# Input
l1 = ['yamada', 'tanaka']
l2 = ['tanaka']
print(set(l1) - set(l2))

# Output
{'yamada'}
```

## collections / itertools パッケージ
「このループ処理、愚直に書けば出来るけど面倒くさい・・・」という時に便利なのが`collections`と`itertools`パッケージです。

### collections.Counter
要素の出現回数をカウントし、多い順にソートしてくれます。

```python
# Input
import collections

l = ['a', 'b', 'c', 'a', 'a', 'c']
c = collections.Counter(l)
print(c.most_common())

# Output
[('a', 3), ('c', 2), ('b', 1)]
```

### collections.defaultdict
連想配列に対し、キーの有無に関係なく処理を実行出来るようになります。

何を言っているか分からないと思いますのでコード例をご確認ください。

```python
# Input
import json
import collections

# defaultdict()の引数には関数(callable)ならなんでも渡せる
groups = collections.defaultdict(list)

# 普通の連想配列だと "baseballというキーは存在しない" といったエラーが発生する
groups['baseball'].append('yamada')
groups['tennis'].append('tanaka')
groups['baseball'].append('sato')

print(json.dumps(groups))

# Output
{"baseball": ["yamada", "sato"], "tennis": ["tanaka"]}
```

defaultdictが特に便利だと感じるのは何らかの集計処理を作成する時です。
いちいちキーの存在を確認しなくて良いため処理がシンプルです。

```python
# Input
import collections

products = ['ノート', 'えんぴつ', 'けしごむ']

sales = [
    ['ノート', 1],
    ['けしごむ', 2],
    ['けしごむ', 1],
]

aggregates = collections.defaultdict(int)

for product, cnt in sales:
    aggregates[product] += cnt

print('--- 本日の販売実績 ---')

for product in products:
    print('%s : %d個' % (product, aggregates[product]))

# Output
--- 本日の販売実績 ---
ノート : 1個
えんぴつ : 0個
けしごむ : 3個
```

`関数(callable)ならなんでも渡せる`という特性を利用すると、多重連想配列もシンプルに実装できます。

```python
# Input
import json
from collections import defaultdict

nested = defaultdict(lambda: defaultdict(int))

nested['a']['a'] += 1
nested['a']['a'] += 1
nested['a']['b'] += 1
nested['b']['c'] += 1

print(json.dumps(nested))

# Output
{"a": {"a": 2, "b": 1}, "b": {"c": 1}}
```

### itertools.product
2個以上の配列から「組み合わせ」を網羅的に生成してくれます。

```python
# Input
import itertools

a = ['a1', 'a2']
b = ['b1', 'b2', 'b3']
c = ['c1']

list(itertools.product(a, b, c))

# Output
[('a1', 'b1', 'c1'),
 ('a1', 'b2', 'c1'),
 ('a1', 'b3', 'c1'),
 ('a2', 'b1', 'c1'),
 ('a2', 'b2', 'c1'),
 ('a2', 'b3', 'c1')]
```

### itertools.chain.from_iterable
2次元配列を1次元配列に変換してくれます(flatten)。

```python
# Input
import itertools

l = [
    ['a1', 'a2'],
    ['b1', 'b2', 'b3'],
]

list(itertools.chain.from_iterable(l))

# Output
['a1', 'a2', 'b1', 'b2', 'b3']
```

## ioパッケージ
ファイルっぽく扱える仮想オブジェクトを提供してくれます。

テストを書く時に重宝します。

```python
# Input
import io


def writer(f, text):
    f.write(text)


def printer(f):
    print(f.read())


sio = io.StringIO()

writer(sio, 'foo\n')
writer(sio, 'bar\n')
writer(sio, 'baz\n')

sio.seek(0)
printer(sio)

# Output
foo
bar
baz
```

## タプル(tuple)
`タプル(tuple)`は配列と似ていますが、不変(immutable)であるため値の再代入が行えません。

不変であるが故に、連想配列のキーとして利用できます。

```python
yamada = ('Yamada', 'Taro')  # これがタプル
tanaka = ('Tanaka', 'Jiro')  # これもタプル

print(yamada[0])  # -> Yamada

yamada[0] = 'Tanaka'  # TypeError: 'tuple' object does not support item assignment

ages = {
    yamada: 20,
    tanaka: 18,
}

print(ages[tanaka])  # -> 18
```

この特性を利用すると、類似データのグルーピングを効率的に行なえます。

```python
# Input
from collections import defaultdict

data = [
    {'circle': 'baseball', 'name': 'yamada', 'age': 10},
    {'circle': 'baseball', 'name': 'sato', 'age': 10},
    {'circle': 'baseball', 'name': 'suzuki', 'age': 11},
    {'circle': 'tennis', 'name': 'tanaka', 'age': 10},
]

per_circle_age = defaultdict(list)

for v in data:
    k = (v['circle'], v['age'])  # (サークル名, 年齢) というタプルを生成
    per_circle_age[k].append(v['name'])  # タプルをキーにして集計

for (circle, age), members in per_circle_age.items():
    print(f'{circle}に所属している{age}歳のメンバー：{members}')

# Output
"baseballに所属している10歳のメンバー：['yamada', 'sato']"
"baseballに所属している11歳のメンバー：['suzuki']"
"tennisに所属している10歳のメンバー：['tanaka']"
```

## dataclassesパッケージ
Python3.7から使える機能です。

コンストラクタにちまちまと`self.name = name`の様な代入処理を書かなくて良いので便利です。

```python
import dataclasses


# frozen=True とすると不変(immutable)なオブジェクトとして扱える
@dataclasses.dataclass(frozen=True)
class User:
    last_name: str
    first_name: str
        
    def full_name(self):
        return f'{self.last_name} {self.first_name}'
        
        
yamada = User(last_name='Yamada', first_name='Taro')
tanaka = User(last_name='Tanaka', first_name='Jiro')

yamada.full_name()  # -> Yamada Taro

# 簡単に連想配列へ変換出来ます
dataclasses.asdict(yamada)  # -> {'last_name': 'Yamada', 'first_name': 'Taro'}

# 比較が可能です
yamada2 = User(last_name='Yamada', first_name='Taro')
yamada == yamada2  # -> True
yamada == tanaka  # -> False
yamada in [yamada2]  # -> True

# "frozen=True" とした場合は値の再代入は出来ません
yamada.last_name = 'Sato'  # -> FrozenInstanceError: cannot assign to field 'last_name'

# immutableなので連想配列のキーとして使えます
d = {yamada: 'foo', tanaka: 'bar'}

# 集合演算(Set型)も可能です
{yamada, tanaka} & {yamada2}  # -> {User(last_name='Yamada', first_name='Taro')}
```