Pythonコーディングで私が気をつけてること：コメント・型アノテーション・データクラス・列挙型（enum）

## 概要

私は普段Pythonでコーディングをすることが多いです。
Pythonはざっと書いてもなんとなく動いてくれる事が多いので、私はついつい適当に書き流してしまいます。
ただ、そういうコードを後から見直すときや、他人に見せるときは、「これ、何してるんだっけ...」とか「この関数、どうやって使うんだっけ...」とかなってしまいます。
というわけで、備忘録として「取り敢えずこれらに気を付けていれば、ある程度はわかりやすくなる」という事柄を列挙しておきます。

## コメント

**処理内容をコメントするのではなく、処理目的を記述する**

処理内容は、関数名や変数名で追えるようにしておき、それだけでは追いづらい処理目的を自然言語でコメントするよう意識しています~~が度々忘れます~~。

例えると以下のようなコードです。

```python:処理内容を記述しているコメント例
# 1/4に縮小する ← 関数名や変数名から想像できる
small_image = resize(image, scale=1/4)
```

```python:処理目的を記述しているコメント例
# モデルに流し込むにあたって安定してメモリに乗せるために縮小する
small_image = resize(image, scale=1/4)
```

処理内容をコメント化するのが間違っているとは思いません。複数行に跨ったり、最適化のためにパッと見で理解できないコードになっていたりする時に、処理内容のコメントがあるのはとても嬉しいです。

ただ、上の例のように、コードを読むのが手間でない時にコードと同じ内容をコメントするのは冗長かと思っています。

「処理を行う動機や目的はコードから追いづらい」という意識を持ってコメントを書くと、時間が経ってから見返しても思い出しやすいコードになるかなと思っています。

## 型アノテーション

**関数の引数と返り値だけでもやっておいて損はない**

Pythonでの型アノテーションはコメント+αで、実行時の型の一致を保証してくれるものではありませんが、それでも可能な限りアノテーションするべきだと思っています。

### 型アノテーションすると良い場所

```python:アノテーション例
# メソッドの引数と返り値に対するアノテーションはとても良いもの（メソッドの中身を見なくて良くなる）
class Person:
    def __init__(self, first_name: str, last_name: str, age: int):  # メソッドの引数に対するアノテーション
        self._name: str = first_name + ' ' + last_name  # 変数に対するアノテーション
        self._age = age

    def is_older_than(self, age: int) -> bool:  # メソッドの返り値に対するアノテーション
        return self._age > age
```

特に公開している関数の引数と返り値は型アノテーションされていないと使用方法が分からないので、必須かなと思っています。もちろん`docstring`で記述しても良いと思います。ある程度高機能なエディタなら`docstring`もパースしてくれるはずなので。

### 日常的に使う型アノテーション

`int`や`float`のような組み込み型を持つ変数や、クラスをインスタンス化した変数を型アノテーションする際の方法がこちらです。


```python:日常的に使う型のアノテーション
age: int = 0
weight: float = 0.0
name: str = 'string'
is_student: bool = True

taro: Person = Person('taro', 'suzuki', 20)
```

他の組み込み型で、よく使うものと言えば、`list`や`dict`や`tuple`があります。
これらも同じように

```python:
friends: list = [daisuke, tomoko]
parents: tuple = (mother, father)
contacts: dict = {'email': 'xxx@mail', 'phone_number': 'XXXX'}
```
のようにアノテーション可能ですが、[typingモジュール](https://docs.python.org/3/library/typing.html)を使えば、より細かくアノテーションできます。
例えば上の例だと`friends`がリストであることは分かりますが、具体的にどんな要素を入れればよいのか、が分かりません。`typing`を使って要素までアノテーションする方法がこちらです。

### typingを使った日常的に使う型の詳細アノテーション

```python:typingを使った日常的に使う型の詳細アノテーション
from typing import List, Tuple, Dict  # 型アノテーションのためのimport

friends: List[Person] = [daisuke, tomoko]  # Personインスタンスを要素に持つリスト
parents: Tuple[Person, Person] = (mother, father)  # Personインスタンスを2つ要素に持つタプル
contacts: Dict[str, str] = {'email': 'xxx@mail', 'phone_number': 'XXXX'}  # keyがstr, valueがstrである辞書
```

`typing`によって、より詳細な型アノテーションが可能になりました。
個人的には、特に`Dict`の型アノテーションでkeyとvalueにアノテーションがあると、どのような辞書型を期待しているのか分かるのでとても安心します。

また、これらは入れ子構造でアノテーションすることも可能です。例えば、emailアドレスや電話番号を複数持つ人もいるはずです。そうすると、`contacts`のvalueは`str`ではなく、`List[str]`にしたくなってきます。そういう時は

```python:
# keyがstr, valueがstrのリストである辞書
contacts: Dict[str, List[str]] = 
    {
        'email': 
            ['xxx@mail', 'yyy@mail'], 
        'phone_number':
            ['XXXX', 'YYYY']
     }
```

のようにアノテーションすることが可能です。

### typingを使った便利な型アノテーション

`typing`は上で挙げたアノテーション以外にも様々なアノテーションを可能にします。よく使うものとして、`Union`と`Optional`を紹介します。

#### Union[A, B, C]: A, B, Cのどれか

`Person`インスタンスの体重を変化させる関数を書くことを考えてみます。とても単純な実装ですが、受け取った重さ分だけ体重を変化させます。

```python:
class Person:
    ...
    def update_weight(self, weight: float) -> float
        self.weight += weight
        return self.weight
```

一見よさそうですが、変化分として`float`しか受け付けないのは少し悲しい気もします。`int`も受け取れるとちょっと便利かもしれません。`int`もしくは`float`ならOK,という型アノテーションをしたくなります。そういった「この中のどれかの型ならOK」という時に使えるのが`Union`です。

```python:
class Person:
    ...
    def update_weight(self, weight: Union[int, float]) -> float
        self.weight += weight
        return self.weight
```

これにより、`int`も`float`も`update_weight`の引数に入れることが可能になります。

#### Optional[A]: AもしくはNone

`Union`を使うようになると、よくコード中に`Union[A, None]`という表記をするようになることに気付くかもしれません。

例えば、職業を表す`Occupation`クラスを定義したとします。`Person`クラスに、その人の職業を持たせることにしましょう。しかし、ひょっとしたら`Person`が学生で、職業がないかもしれません。職業は`Occupation`もしくは`None`である、ということにしたいです。こういう時に`Union`が使えますね。

```python:
class Person:
    def __init__(..., occupation: Union[Occupation, None]):
        self._occupation = occupation
```

また、別の例としてその人のパスポートIDを文字列として取得する関数をはやしたいとします。しかしパスポートを持っていないかもしれません。空文字列を返すのも一つの手でしょう。しかし、存在しないことをはっきりさせたいなら、`None`を返すことを検討するのもいいかもしれません。

```python:
class Person:
    def get_passport_id(self) -> Union[str, None]:
        if self.has_passport:
            return self._passport._id
        else:
            return None
```

こういった型アノテーションをする機会が頻繁になってくると、だんだんめんどくさくなってきます。そんなときのために、`Optional`が用意されています。`Optional`は、`Optional[A]`のように使用し、`A`もしくは`None`という意味になります。`Optional[A] = Union[A, None]`です。

`Optional`を使うと、先程の例はこうなります。

```python:
class Person:
    def __init__(..., occupation: Optional[Occupation]):
        self._occupation = occupation

    def get_passport_id(self) -> Optional[str]:
        if self.has_passport:
            return self._passport._id
        else:
            return None
```

`Optional`という単語が割り振られたからか、ちょっとコードの意図が伝わりやすくなっている気がします。

### typingを使った便利かもしれない型アノテーション

そこまで使うわけではないですが、あると便利かもしれない

#### NewType: 新たな型の作成

新しい型を定義することができます。例えば、`int`型の`person_id`から、該当する`Person`インスタンスを探してくる関数として、

```python:
def find_by_id(person_id: int) -> Person:
    ...
```
のような表記ができると思います。ここでは引数名が`person_id`という分かりやすい名前なので、あまり混同しないかもしれませんが、うっかり、同じ`int`型で定義した`occupation_id`なんかを引数に渡してしまうミスをしてしまうかもしれません。
そういったうっかりミスを防ぐために、敢えて`PersonId`クラスを定義して

```python:
class PersonId(int):
    pass


def find_by_id(person_id: PersonId) -> Person:
    ...

p = find_by_id(PersonId(10))
```

としても良いかもしれませんが、これはインスタンス作成のオーバーヘッドが生じます。
`NewType`を使用すると、

```python:
from typing import NewType


PersonId = NewType('PersonId', int)

def find_by_id(person_id: PersonId) -> Person:
    ...

p = find_by_id(PersonId(10))
```
と書けますが、これは実行時には何もせず10を返す関数が呼ばれるだけなのでオーバーヘッドが小さいです。また、コードの意味として、`int`だけどここは別の型として定義し直すことでヒューマンエラーを防いでます、というのが見えやすい気がします。

#### TypeAlias: 複雑な型に対する別名

例えば`Dict[str, List[str]]`くらいなら「えーと、`str`がkeyで`str`が要素のリストがvalueの辞書だな」と読めるかもしれませんが、これが`List[Dict[str, Union[int, float, None]]]`とかなってくるとよく分からなくなってきますし、この型をやり取りする関数で毎回これだけの型アノテーションをつけるのはしんどいです。そういう際に、TypeAliasを使えば、

```python:TypeAlias
TypeReportCard = List[Dict[str, Union[int, float, None]]]]

def set_report_card(report_card: TypeReportCard):
    ...

set_report_card([{'math': 9.5}, {'english': 7}, {'science': None}])
# 間違い例 -> set_report_card(TypeReportCard([{'math': 9.5}, {'english': 7}, {'science': None}])) 
```
のようにスッキリ書けます。単にエイリアスを作るだけで、特別にimportは必要ないです。`NewType`とは違って、あくまでエイリアスなので実際に引数として用いる際に、エイリアス名で包む必要はありません。

[PEP 484](https://www.python.org/dev/peps/pep-0484/)を一読するのも面白いかもしれません


## データクラス

**辞書型を使う際は、一度データクラスも検討してみる**

ある人の連絡先をデータとして持ちたくなったので、辞書型で表現してみます。

```python:contacts_辞書

contacts: Dict[str, Optional[str]] = 
    {
        'email': 'xxx@mail',
        'phone_number': None
     }
```

これだけ見ると別になんてないよくあるコードです。
ある日、「連絡先に電話番号が設定されているかどうか」を知りたくなったとします。

```python:
def has_phone_number(contacts: Dict[str, Optional[str]]) -> bool:
    return contacts.get('phone_number', None) is not None
#    return 'phone_number' in contacts and contacts['phone_number'] is not None でも良い
```

特に問題なく動くと思います。２週間後、この関数が再び必要になりました。型アノテーションのおかげで`contacts`がどのようなデータか思い出すことができ、無事関数を呼び出すことに成功します。

```python
has_phone_number({'email': 'xxx@mail', 'phone-number': 'XXXX'})
```

しかし、この関数の返り値は、`False`になります。よく見ると`phone_number`を`phone-number`にしてしまっています。そのせいで、`phone_number`が存在しないことになり、結果が`False`になります。`Dict[str, Optional[str]]`の型アノテーションでは、必要なkeyの名前までは分からなかったので、2週間前に決めたkey名を正確に思い出すことが出来なかったのです。

この例はすぐ上に`has_phone_number`の実装が書いてあるので分かりやすいかもしれません。しかし、もしこの関数の実装場所が離れていたら？ 結果が`False`となっていることにすぐ気付けなかったら？ デバッグはしんどくなると思います。

コード中に出てくる直接埋め込まれた定数はなるべく避ける、というのが定番ですが、辞書型のkeyに関しても注意が必要です。

そういう際は一度[データクラス](https://docs.python.org/3/library/dataclasses.html)を検討すると良いかもしれません。

#### データクラスによる辞書の置き換え

```python:データクラス
# dataclassで代替させる
import dataclasses


@dataclasses.dataclass
class Contacts:
    email: Optional[str]
    phone_number: Optional[str]
```

`dataclasses.dataclass`は`__init__`の自動生成を行ってくれるので、クラスとしての表記は上で十分です。
他にも、`__eq__`の自動生成、`__repr__`の自動生成など、様々な機能を備えていますが今回は割愛します。

上のようにデータクラスとして連絡先を定義すると、`has_phone_number`は下のように実装できます。

```python
c = Contacts(email='xxx@mail', phone_number='XXXX')

def has_phone_number(contacts: Contacts) -> bool:
    return contacts.phone_number is not None
```

このようにデータクラスのフィールドとしてアクセスするので、（エディタがチェックやサジェストをサポートしてくれるので）タイプミスをすることがなくなります。

また、`Dict[str, Optional[str]]`で定義されていた頃と異なり、key名（フィールド名）が固定され、さらに各keyごとに型がつくことになったので、どのようなデータを要求しているのかがより具体化されています。

#### 補足: NamedTupleによる辞書の置き換え

データクラスは、Python3.7からの機能なので、もし3.6以下を使う場合は、`typing`の`NamedTuple`を検討するのもありかもしれません。`NamedTuple`はこのように使います。

```python:NamedTuple
# NamedTupleで代替させる
from typing import NamedTuple


class Contacts(NamedTuple):
    email: Optional[str]
    phone_number: Optional[str]

c = Contacts(email='xxx@mail', phone_number='XXXX')

def has_phone_number(contacts: Contacts) -> bool:
    return contacts.phone_number is not None
```

記述はほとんど`dataclass`と変わりませんが、`dataclass`はより細かな設定が可能なパワフルさがあるのに比べ、`NamedTuple`はあくまで名前付きタプルというだけです。

## 列挙型（enum）

最後は[列挙型](https://docs.python.org/3/library/enum.html)です。

ある変数がAかBかCしか取り得ないんだよな...という時にとても便利です。例えば、こういうコードを流し書きしてしまうことが私はよくあります。

```python
def display(object_shape: str):
    if object_shape == 'circle':
        ...
    elif object_shape == 'rectangle':
        ...
    elif object_shape == 'triangle':
        ...
    else:
        raise NotImplementedError

display('circle')
```

状態に応じて処理を分けるときにこんな感じで書いてしまい、後から状態の管理が煩雑化したり、状態名が分からなくて実装を探したり...ということをやってしまいます。また型だけ見ると`str`なら何でも良いように見えますが、実際はいくつかの文字列以外はエラーになります。

そういう時に、列挙型（`enum`）を使うとスッキリ書けるかもしれません。

```python:列挙型
from enum import Enum


class ObjectShape(Enum):
    CIRCLE = 0
    RECTANGLE = 1
    TRIANGLE = 2

def display(object_shape: ObjectShape):
    if object_shape is ObjectShape.CIRCLE:
        ...
    elif object_shape is ObjectShape.RECTANGLE:
        ...
    elif object_shape is ObjectShape.TRIANGLE:
        ...
    else:
        raise NotImplementedError

display(ObjectShape.CIRCLE)
```

これでタイプミスの心配はなくなりましたし、型を見れば何が許されているのかが一目瞭然です。
また、今回は`Enum`の識別子に全て手動で番号を割り振りましたが、この番号には特に意味がないはずなので、

```python:auto
import enum


class ObjectShape(enum.Enum):
    CIRCLE = enum.auto()
    RECTANGLE = enum.auto()
    TRIANGLE = enum.auto()
```
のように自動でユニークな値を割り振ってもらうのがベストかなと思います。