すべてのPythonistaに知ってほしいimport順序をプロジェクト内で統一させる方法

# この記事 is 何？ (TL;DR)
- Pythonのimport文の順序が人によってバラバラ問題を解決する
- [isort](https://github.com/timothycrosley/isort)を使ってimportをソートする
- pre-commitを使ってimport順序を守ってないコードはcommitできないようにする

# Pythonのimport順序が規約で定まっていない問題
Pythonのimport順序については、[PEP8](https://github.com/python/peps/blob/master/pep-0008.txt)に以下のように記載されてます。

```
- Imports are always put at the top of the file, just after any module
  comments and docstrings, and before module globals and constants.

  Imports should be grouped in the following order:

  1. Standard library imports.
  2. Related third party imports.
  3. Local application/library specific imports.

  You should put a blank line between each group of imports.
```

つまり、以下の順に空行を挟んで書けばOKなんですね！
1. 標準ライブラリ
2. サードパーティライブラリ
3. 自作ライブラリ

・・・え？
標準ライブラリ同士での順番はどうしたらいいの？
`from pathlib import Path` と `import sys` はどっちが先なの？

実はPython規約では、標準ライブラリ同士での順序などについては決められていないのです！
つまり、プロジェクト内で統一する基準を決めてあげないと、人によってバラバラな書き方をしてしまうことになります！

# isortを使った解決策
[isort](https://github.com/timothycrosley/isort)というツールを使用してプロジェクト内での統一ルールを決めて運用していきます。

1. `pip install isort` でisortをインストールする
2. （デフォルト設定以外の設定で運用したい場合、） `~/.isort.cfg` ファイルを編集する
3. `isort -rc .` を実行して、プロジェクト内の全ファイルを定めたルールでソートする

デフォルト設定（.isort.cfgファイルを作成しない場合）だと以下のようにソートされるようになります！
デフォルトでもPEP8に沿ってソートしてくれていい感じです:relaxed:

```python:ソート前
import os
import sys
from pathlib import Path
import numpy as np
import pandas as pd
import boto3
import mylib1
import mylib2
```

```python:ソート後
import os
import sys
from pathlib import Path

import boto3
import numpy as np
import pandas as pd

import mylib1
import mylib2
```

# おまけ：ソートされていないファイルをコミットできないようにする
pre-commitを使って、ソートされていないファイルをコミットできないようにしたい場合は以下のようにファイルを編集します！
参考： https://gist.github.com/acdha/8717683
1. `cp .git/hooks/pre-commit.sample .git/hooks/pre-commit` してpre-commitファイルを作成する
2. pre-commitファイルを以下の内容で上書きする

※プロジェクトによって固有の値になりうる箇所をTODOとして記載している(2箇所)ので、そこを修正する必要があります。
※ `which python` や `which isort` の結果のパスを指定して上げればOKです。

```python:pre-commit
#!/usr/bin/env PYTHONIOENCODING=utf-8 [TODO: プロジェクトのPython環境を記載する]
# encoding: utf-8
"""Git pre-commit hook which lints Python, JavaScript, SASS and CSS"""

from __future__ import absolute_import, print_function, unicode_literals

import os
import subprocess
import sys

FS_ENCODING = sys.getfilesystemencoding()


def check_linter(cmd, files, **kwargs):
    if not files:
        return
    print('Running %s' % cmd[0])
    return subprocess.check_output(cmd + files, stderr=subprocess.STDOUT, **kwargs).decode(FS_ENCODING)


def filter_ext(extension, files, exclude=None):
    files = [f for f in files if f.endswith(extension)]
    if exclude is not None:
        files = [i for i in files if exclude not in i]
    return files


def lint_files(changed_files):
    changed_files = [i.strip() for i in changed_files.splitlines() if '/external/' not in i]

    changed_extensions = {ext for root, ext in map(os.path.splitext, changed_files)}

    if '.py' in changed_extensions:
        py_files = filter_ext('.py', changed_files)
        check_linter(['[TODO: プロジェクトのisortパスを記載する]', '-c'], py_files)

    if '.js' in changed_extensions:
        check_linter(['eslint'], filter_ext('.js', changed_files, exclude='.min.'))

    if '.scss' in changed_extensions:
        try:
            check_linter(['scss-lint'], filter_ext('.scss', changed_files))
        except subprocess.CalledProcessError as exc:
            if exc.returncode == 1:
                # scss-lint rc=1 means no message more severe than a warning
                pass
            else:
                raise

    if '.css' in changed_extensions:
        check_linter(['csslint'], filter_ext('.css', changed_files, exclude='.min.'))


if __name__ == "__main__":
    os.chdir(os.path.join(os.path.dirname(__file__), '..', '..'))
    changed_files = subprocess.check_output('git diff --cached --name-only --diff-filter=ACM'.split())
    changed_files = changed_files.decode(FS_ENCODING)

    try:
        lint_files(changed_files)
    except subprocess.CalledProcessError as exc:
        print('Quality check failed:', file=sys.stderr)
        print(' '.join(exc.cmd), file=sys.stderr)
        if exc.output:
            output = exc.output.decode(FS_ENCODING)
            print('\t', '\n\t'.join(output.splitlines()), sep='', file=sys.stderr)
        sys.exit(1)
```

pre-commitファイルを作成して、isort順序が指定された方法でないファイルが有った場合、以下のエラーメッセージが出てcommitに失敗します。

```
コミットはエラーで失敗しました
0 file committed, 1 file failed to commit: [invalid!!]test commit
ERROR: /Users/xxx/PycharmProjects/isort_test/incorrect_isort.py Imports are incorrectly sorted.
```

上記のエラーにてコミットに失敗した場合は、 `isort [ソートしたいファイルパス]` を実行してimport順序をソートすればcommitできるようになります！

# おまけ：PyCharm使用者向け
PyCharmでいちいちコマンドでisortを実行したくない場合は、コマンドを外部ツールとして登録することができます！
Community版でも使用可能です！
参考： https://github.com/timothycrosley/isort/issues/258

1. Pycharm > Preferences > External Tools > + を選択する
2. ツールを編集する画面にて、以下の画像のように設定する
3. importをソートしたいファイルを開いて、右クリック > External Tools > isort current file

※Pycharmを日本語化しているので一部画像が異なるかもしれません。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/190807/21bfeb9e-92af-796e-253c-f998cbd33ce5.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/190807/fd4994cb-70ce-6ef8-2ee5-c8df14db64fb.png)
![isort.gif](https://qiita-image-store.s3.amazonaws.com/0/190807/3ff4137c-cdbb-c7bd-11af-c05fa7c04fa2.gif)

# まとめ
The Zen of Pythonに則って、たったひとつの冴えたやり方をチームで模索していきましょう！
マサカリ大歓迎です！


> There should be one-- and preferably only one --obvious way to do it.
何かいいやり方があるはずだ。誰が見ても明らかな、たったひとつのやり方が。
　　　　　　　　　　　　　　　　　　　　　　　　by The Zen of Python