今時のPythonはこう書く2020

# はじめに
システム作ってるとかライブラリ作ってるみたいなある程度**Pythonを綺麗に[^1]書くことが求められる**方々に向けた記事です。
(機械学習系のライブラリを使うためにPython書いてる方とか、初学者の方にはちょっとあわないかも知れません)

綺麗に書くための作法の難しさって**共有が面倒**なところだと思うんですよね。その書き方は間違いじゃない、間違いじゃないけどもっといい書き方があるぞみたいなやつってなかなか指摘し辛いですし、じゃあ1人に対してレビューしたら他のメンバーにはどう伝える？そもそも伝える必要？俺の工数は？みたいになりがちです。
一番いいのはこういう時はこう書く！みたいなドキュメントを作って「ドキュメント違反です」ってレビューをしてあげることなんですが、まーそれもそれで超面倒じゃないですか。なのでこの記事がそのドキュメントの代わり、とまではいかなくとも礎くらいになればいいなと思って書きました。

ポエム終わり。あと仕事でPython書くなら全員[*What's New in Python*](https://docs.python.org/ja/3/whatsnew/index.html)くらい読め

# 本題
以降の章題は
"{重要度} {機能名}【{必要なバージョン}】"
というフォーマットを用います。重要度については
★★★ … 絶対使って欲しい
☆★★ … 使うと嬉しい
☆☆★ … 人による
くらいのイメージでお願いします。

## ★★★ [型アノテーション](https://docs.python.org/ja/3/whatsnew/3.5.html#pep-484-type-hints)【3.6～】
おなじみの。関数の引数や戻り値、変数に型を書くことができます。
よく言われるのが「ただのコメントだから強制力はない」ということでそれは事実そうなのですが、実行時にエラーは出なくとも[pylance](https://devblogs.microsoft.com/python/announcing-pylance-fast-feature-rich-language-support-for-python-in-visual-studio-code/)や[mypy](http://mypy-lang.org/)を使えばエディタ上で警告を出してくれますし、何より後から読む人のことを考えれば上と下のコードどちらが良いかは一目瞭然でしょう。
現代において(少なくとも関数の戻り値については)型アノテーションは最低限のマナー。
**型アノテーションをつけない人は挨拶できない人と同じ**です。


```py
from datetime import date
from urllib.request import urlopen
from urllib.parse import urlencode
import json

URL = "https://disclosure.edinet-fsa.go.jp/api/v1/documents.json"

def request_count_of_submitted_docs(date_):
    query = urlencode({"date": date_})
    resp = json.load(urlopen(f"{URL}?{query}"))
    return resp["metadata"]["resultset"]["count"]

print(request_count_of_submitted_docs(date(2020, 11, 20)))
>>> 504
```

↓

```py
...

def request_count_of_submitted_docs(date_: date) -> int:
    query: str = urlencode({"date": date_})
    resp: dict = json.load(urlopen(f"{URL}?{query}"))
    return resp["metadata"]["resultset"]["count"]

...
```

(VS Code上で見るとより分かりやすいですね)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/79eb4886-7630-1385-7cd9-ec32421182a8.png)

さて、ここまではよく聞く話なのですが、これ関連の話でどうしても私がお伝えしたいことがあります。

**[typingライブラリのドキュメント](https://docs.python.org/ja/3/library/typing.html)を読んでください。**

それなりに量があるうえバージョンアップのたびにドンドン増えるので、そんな書き方あったの！？みたいな話がぼろぼろ見つかるはずです。紹介します系の記事で知った気になるだけじゃもったいねえ。以下に私がよく使うものをいくつかピックアップします。

### [Final](https://docs.python.org/ja/3/library/typing.html#typing.Final) (3.8～)
従来のアッパースネークは定数として扱うみたいな~~ふわふわした~~不文律ではなくキチンと定数を型チェッカーに通知できます。
上の例で言うと

```py
from typing import Final

URL: Final[str] = "https://disclosure.edinet-fsa.go.jp/api/v1/documents.json"
URL = "https://disclosure.edinet-fsa.go.jp/api/v1/documents"  # 型チェッカーに怒られる
```

こう。
サブクラスで上書きするのもダメです。

```py
from typing import Final

class Connection:
    TIMEOUT: Final[int] = 10

class FastConnector(Connection):
    TIMEOUT = 1  # 怒られる
```

### [ジェネリック型エイリアス](https://docs.python.org/ja/3/library/typing.html#user-defined-generic-types)
型変数ってクラスとか関数だけじゃなくて型エイリアスに使っても面白いぜ！みたいな話です。
例えばGo風にエラートラップを書きたい時、

```py:3.9以降でのみ動きます。3.8以前はtupleをtyping.Tupleで置き換えてください
from typing import TypeVar, Union
from datetime import date
from urllib.request import urlopen
from urllib.parse import urlencode
from urllib.error import URLError
import json

T = TypeVar("T")
Err = Union[tuple[T, None], tuple[None, Exception]]

URL = "https://disclosure.edinet-fsa.go.jp/api/v1/documents.json"

def request_count_of_submitted_docs(date_: date) -> Err[int]:
    query: str = urlencode({"date": date_})
    try:
        resp: dict = json.load(urlopen(f"{URL}?{query}"))
    except URLError as err:
        return None, err
    return resp["metadata"]["resultset"]["count"], None

cnt, err = request_count_of_submitted_docs(date(2020, 11, 20))
if err:
    print(err)
print(cnt)
```

型エイリアスにも`[]`をつけられるのはアツいですよね。

### [@overload](https://docs.python.org/ja/3/library/typing.html#typing.overload)
ご存じの通りPythonにオーバーロードはありませんが、型チェッカーにオーバーロードっぽいものを伝えることだけは出来るようになっています。
例えば先ほどから使用している関数は「日付を渡すとその日EDINETに提出された書類の数を返す」ものでしたが、これを「日付を渡さない場合は日付ごとの提出数を辞書で返す」ようにしましょう。

```py:EDINETに迷惑なので実装はしません
@overload
def request_count_of_submitted_docs() -> Err[dict[date, int]]:
    ...

@overload
def request_count_of_submitted_docs(date_: date) -> Err[int]:
    ...

def request_count_of_submitted_docs(date_=None):
    # 実装はここに書く
```

こうすると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/84598e29-1dbe-5278-680e-20b4c1bdd8dc.png)
引数の有無だけでキチンと型チェッカーが戻り値の型を自動判別してくれるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/33694a87-3145-29b5-b700-f570e5b5aa38.png)

###[NamedTuple](https://docs.python.org/ja/3/library/typing.html#typing.NamedTuple)
ちょっとした構造体のためにわざわざクラス書きたくね～を叶えるための[**collections.namedtuple**](https://docs.python.org/ja/3/library/collections.html#namedtuple-factory-function-for-tuples-with-named-fields)を更に使いやすくしたものです。

```py
from typing import NamedTuple

class Address(NamedTuple):
    row: int
    col: int

class Range(NamedTuple):
    start: Address
    end: Address
```

宣言これだけ。欲しければDocstringsもプロパティもつけられます。

```py
s = Address(2, 4)
e = Address(6, 8)
rng = Range(s, e)

print(rng[0][1])
>>> 4
print(rng.start.col)
>>> 4
```

どう考えてもペアでしか使わないだろみたいな値、ついついタプルで渡してしまいがちですが積極的に型をつけていきましょう。

### [Protocol](https://docs.python.org/ja/3/library/typing.html#typing.Protocol) (3.8～)
なんかこれ私も勘違いしてたんですが構造的部分型自体は以前からサポートしていたらしいですね。
マジかと思って試してみたら

```py:3.7で検証
from collections.abc import Container

class Hoge:
    def __contains__(self, item):
        return True

print(isinstance(Hoge(), Container))
>>> True
```

マジでした。
で、以前は`collections.abc`みたいな組み込みのABCでしか構造的部分型を使えなかったのが3.8からは自作のクラスでもOKになりましたよっていうのが話の流れみたいです。

```py
from typing import Protocol, runtime_checkable

@runtime_checkable
class Greeter(Protocol):
    def greet(self):
        ...

class Japanese:
    def greet(self):
        print("おはよー！こんちわー！こんばんわー！おやすみー！")

print(issubclass(Japanese, Greeter))
>>> True
```

構造的部分型(静的ダックタイピング)の何がいいって「インターフェースに対してプログラミングしろ」とかいう古来より伝わるアレが感覚的にできることですよね。例えばよく見るこういう型アノテーション↓

```py
def concat(prefix: str, words: list[str]) -> str:
    return ", ".join(f"{prefix} {word}" for word in words)

print(concat("a", ["dog", "cat"]))
>> a dog, a cat
```

その`words`は本当に`list`が欲しいのか？って思いません？
この「意味もなく`list`型でとる」書き方には大きく分けて2つの問題点があります。

1つは単純に使い勝手が悪いこと。この場合は`for`で回せて`str`が取れればなんでもいいわけですから、タプルもセットもイテレータだって実際には渡せるわけです。渡せるものをわざわざ絞って書くのは関数の使い勝手を損ねます。

2つめは結構見落としがちですが、`list`型を受け入れる関数はリストに対する操作なら**なんでも**できます。例えばリストには`append`とか`sort`みたいな破壊的操作のできるメソッドが備わってるわけですが、そういった操作をしないという保証は`list`を受け入れる限りできません。関数を使う人にとっては結構なリスクです。

というわけでこの`concat`関数が取る`words`は**`for`で回せることだけを保証したい**。

```py:これも3.9からです。3.8以前はtypingからIterableをimportしてください
from collections.abc import Iterable

def concat(prefix: str, words: Iterable[str]) -> str:
    return ", ".join(f"{prefix} {word}" for word in words)
```

それがこの書き方です。
`for`で回せるとは即ち`__iter__`が実装されているオブジェクトということなので、「`__iter__`が実装されている型」を総称できる`Iterable`型をインポートします。そして`concat`は`for`で回せること**だけ**が保証されたオブジェクトに対して`for`**だけ**を行う。これってつまり`__iter__`というインターフェースに対するプログラミングですよね。

これが！あらゆるインターフェースに対してできるようになる！
それが`Protocol`登場の嬉しさなわけです。

(※ちなみにですが[**collections.abc**](https://docs.python.org/ja/3/library/collections.abc.html)のページにメソッドとABC(インターフェース)の対応表が乗っています。typingのページと併せて必読です。)


## ★★★ [dataclasses](https://docs.python.org/ja/3/library/dataclasses.html)【3.7～】
```
   △￥▲
 [　㊤皿㊤]    がしゃーん
／|＿肉＿|＼    がしゃーん
   _|　|_

dataclassだよ
__init__, __repr__, __eq__, __hash__,
__lt__, __le__, __gt__, __ge__,
__setattr__, __delattr__
を自動で付与してくれるすごいやつだよ
```

マジですごい。こいつ抜きでクラス宣言なんてもうダルすぎてやってられません。
基本的な使い方は

```py
from dataclasses import dataclass

@dataclass
class Hoge:
    hoge: str
    fuga: int
    piyo: bool
```

これだけです。
`__init__`も`__repr__`も`dataclass`がよしなにしてくれるので

```py
hoge = Hoge("hoge", 4649, False)
print(hoge)
>>> Hoge(hoge='hoge', fuga=4649, piyo=False)
```

こうなります。
比較として従来の宣言を置いておきますね。

```py:めんどくせえ…
class Hoge:
    def __init__(self, hoge: str, fuga: int, piyo: bool):
        self.hoge = hoge
        self.fuga = fuga
        self.piyo = piyo

hoge = Hoge("hoge", 4649, False)
print(hoge)
>>> <__main__.Hoge object at 0x000001E3CB8F4460>
```

`dataclass`を使うメリットは大きく2つです。
1つはご覧のように特殊メソッドを書く手間が省けること。
そして2つ目が型アノテーションを**強制できる**こと。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245527/9ded826d-4631-1118-1926-c956567238cd.png)
このようにアノテーションを書かないとエラーになるので、自然と型を書く文化が身につきます。

詳細な使い方については@tag1216さんのこちら↓の記事が詳しすぎるので特に私から書くことはありません。
[Python3.7からは「Data Classes」がクラス定義のスタンダードになるかもしれない](https://qiita.com/tag1216/items/13b032348c893667862a)

なのでちょっとした小話だけ。

```py
@dataclass
class Hoge:
    hoge: str
    fuga: InitVar[int]

    def __post_init__(self, fuga):
        self.piyo = bool(fuga)
```

のように`__post_init__`の中で属性を定義してしまうと`dataclass`が作ってくれる諸々の特殊メソッドの中に`piyo`が反映されません。

```py
hoge = Hoge("hoge", 4649)
fuga = Hoge("hoge", 4649)
print(hoge == fuga)
>>> True
fuga.piyo = False
print(hoge == fuga)
>>> True # [㊤皿㊤　]！？
```

こういった場合も必ず最初に全てのフィールドを定義するようにしましょう。

```py
from dataclasses import dataclass, InitVar, field

@dataclass
class Hoge:
    hoge: str
    fuga: InitVar[int]
    piyo: bool = field(init=False)

    def __post_init__(self, fuga):
        self.piyo = bool(fuga)

hoge = Hoge("hoge", 4649)
fuga = Hoge("hoge", 4649)
print(hoge == fuga)
>> True
fuga.piyo = False
print(hoge == fuga)
>> False # [㊤皿㊤　]ｂ
```

## ☆★★ [代入式](https://docs.python.org/ja/3/whatsnew/3.8.html#assignment-expressions)【3.8～】

`:=`セイウチ演算子です。

```py
from typing import Optional
from secrets import randbelow

def perhaps_hoge() -> Optional[str]:
    if round(randbelow(9) / 10):
        return "hoge"

hg = perhaps_hoge()
if hg:
    print(f"my answer: {hg}")
```

これが

```py

from typing import Optional
from secrets import randbelow

def perhaps_hoge() -> Optional[str]:
    if round(randbelow(9) / 10):
        return "hoge"

if (hg := perhaps_hoge()):
    print(f"my answer: {hg}")
```

こうなる。

1行じゃんって思うかもしれませんが結構使うんですよコレ。
`if`があるところなら何でも使えるので

```py:三項演算子
print(f"my answer: {hg if (hg := perhaps_hoge()) else 'None'}")
>>> my answer: None
```

```py:内包表記
hgs = [hg for _ in range(10) if (hg := perhaps_hoge())]
print(f"my answer: {', '.join(hgs)}")
>>> my answer: hoge, hoge, hoge, hoge, hoge
```

```py:ifじゃないけどwhileも
while not (hg := perhaps_hoge()):
    print(f"my answer: {hg}")
>>> my answer: hoge
>>> my answer: hoge
```

意外にも頻度が高い。
ですがドキュメントにもある通り

> セイウチ演算子の使用は、複雑さを減らしたり可読性を向上させる綺麗なケースに限るよう努めてください。

なので濫用は禁物です。
内包表記とか三項演算子にも言えることですが、これらは**可読性を高める**という本来の目的を忘れないことが大切ですね。

セイウチくんとのお約束！
![](https://upload.wikimedia.org/wikipedia/commons/2/22/Pacific_Walrus_-_Bull_%288247646168%29.jpg)


## ☆★★ [フォーマット済み文字列リテラル](https://docs.python.org/ja/3/whatsnew/3.6.html#pep-498-formatted-string-literals)【3.6～】
さっきから地味にちょいちょい使ってる`f`から始まるアレです。

```py:
from secrets import randbelow

rnd = randbelow(9)
# どっちも同じ
print("num {rnd}".format(rnd=rnd))
print(f"num {rnd}")
>>> num 2
```

f文字列は内部的に`format`を使うので基本的には全部f文字列で書いて欲しいんですが、この項では逆に**`format`を使った方がいい**場面についてお話します。

```py
from typing import Iterator, Iterable
from enum import Enum

class FMT(Enum):
    greater = "up {num}"
    less = "down {num}"
    equal = "stay"

def format_diffseq(seq: Iterable[int]) -> Iterator[str]:
    def format_(num: int) -> str:
        if num > 0:
            fmt = FMT.greater
        elif num < 0:
            fmt = FMT.less
        else:
            fmt = FMT.equal
        return fmt.value.format(num=abs(num))

    return map(format_, seq)

seq = [9, 10, 3, 100, 100]
dseq = (y - x for x, y in zip(seq, seq[1:]))
fdseq = format_diffseq(dseq)
print(*fdseq, sep="\n")
>>> up 1
>>> down 7
>>> up 97
>>> stay
```

このプログラムは[この問題](https://atcoder.jp/contests/past201912-open/tasks/past201912_b)の回答を一部抜粋したもので、`[9, 10, 3, 100, 100]`という数列の[階差数列](https://ja.wikipedia.org/wiki/%E9%9A%8E%E5%B7%AE%E6%95%B0%E5%88%97)を規定のフォーマットに変換しています。

こういう既定の文字列があってそれに変数を代入するタイプのやつってf文字列でやろうとするとゴチャるんですよね。

```py:フォーマットをかける関数がフォーマット自体を持ってしまうのはどうよ…？
def format_(num: int) -> str:
    if num > 0:
        return f"up {num}"
    if num < 0:
        return f"down {num}"
    return "stay"
```

実際の開発ではエラーメッセージをこのように後からフォーマットで処理することが多いです。

なおf文字列は3.8で[ちょっとした機能追加](https://docs.python.org/ja/3/whatsnew/3.8.html#f-strings-support-for-self-documenting-expressions-and-debugging)が入ったのですが、私はあまり使わないので割愛します。多分サーバーサイドでPython使ってる人とかがデバッグ目的で使うんだと思う。

## ☆☆★ [高レベルのasyncio API](https://docs.python.org/ja/3/library/asyncio-api-index.html)【3.7～】

私はあんまり使わないんですが、多分非同期処理の書き方も3.5と3.7からでかなり違っている、はず。

基本的には非同期処理を書く必要が生じうる処理については大体高レベルのAPIが用意されているので(以下[**asyncio**](https://docs.python.org/ja/3/library/asyncio.html)から引用)、自分でイベントループを作ったり閉じたりする必要はない、はず。
>
- 並行に Python コルーチンを起動 し、実行全体を管理する
- ネットワーク IO と IPC を執り行う
- subprocesses を管理する
- キュー を使ってタスクを分散する
- 並列処理のコードを 同期 させる

例として[**Streams**](https://docs.python.org/ja/3/library/asyncio-stream.html)を使って非同期にWeb APIを叩いてみます。
この記事の一番最初のサンプルコードでやってるやつの非同期版ですね。

```py
import asyncio
from datetime import date
from urllib.parse import urlsplit, urlencode
import json

URL = "https://disclosure.edinet-fsa.go.jp/api/v1/documents.json"

async def request_count_of_submitted_docs(date_: date) -> int:
    url = urlsplit(f"{URL}?{urlencode(dict(date=date_))}")
    reader, writer = await asyncio.open_connection(url.hostname, 443, ssl=True)
    writer.write(f"GET {url.geturl()} HTTP/1.0\n\n".encode("latin-1"))
    resp = await reader.read()
    writer.close()
    parsed = json.loads(resp.split(b"\r\n\r\n")[-1])
    return parsed["metadata"]["resultset"]["count"]

async def main():
    counts = await asyncio.gather(
        *(
            request_count_of_submitted_docs(date_)
            for date_ in (
                date(2020, 9, 1),
                date(2020, 10, 10),
                date(2020, 11, 20),
            )
        )
    )
    print(*counts, sep="\n")

asyncio.run(main())
>>> 157
>>> 0
>>> 504
```

こういうのも日本語で探すと全然情報ないんですけど、公式ドキュメントを読む感じとにかく**エントリーポイントを作って`asyncio.run`に投げろ！**というのが最近の書き方みたいです。私も詳しくないのでこのあたりで許してください。

# おわりに

というわけで3.6以降のめぼしい書き方をザッとさらってみました。
こうして見るとかなり綺麗に書くための道具が揃ってきたな感がありますよね。
時代によって全く異なる役割を期待されてきたPythonにいよいよ**大規模開発で使いたい**というニーズが乗ってきたんだなという流れを感じます。

最近のPythonはバージョンアップのペースも上がって追いつくのが大変ですが、今後もこの流れはドンドン加速していくはずなので頑張ってキャッチアップしていきたいと思います。

**Pythonを綺麗に書こう！**

[^1]: 保守性(可読性含む)を高く、とも言います