はじめに
Pythonでアプリを作成する上で、いい感じの環境を整えたいと思います。
今の時代でチーム開発するならこんな環境を作って開発するのかなーと妄想しつつ作成しました。
ちなみにチームでの開発経験はありません。
以下の内容を書いています。
- 環境
- windows
- VSCode
- Python3
- VSCodeとの連携
- 仮想環境(venv)
- Jupyter Notebook
- VSCodeの拡張機能の共有
- パッケージ管理(pip + Jupyter Notebook)
- フォーマッタ/リンター(flake8/black/isort)
- 型アノテーション(Pylance)
- アプリ管理
- ディレクトリ構成
- importのパス管理
- アプリの設定ファイル管理例
- テスト実装例
- Logger実装例
- 例外処理
- ライセンス
あくまで一例なので参考程度に見てください。
コード全体(github)
1. Pythonの仮想環境の管理
方針としてはPythonランチャーとvenvで行います。
PythonランチャーとvenvはPython3.3以降で標準でついてくるようです。
他の選択肢はAnaconda等があります。
Anacondaは商用利用での有償化など、規約の変更により条件が変わるかもしれないので公式サポートのツールを使ってみました。
参考
・pyenv、pyenv-virtualenv、venv、Anaconda、Pipenv。私はPipenvを使う。
・Anaconda パッケージリポジトリが「大規模な」商用利用では有償になっていた
1-1. Pythonのインストール(Pythonランチャー)
公式からPythonをインストールします。
インストールするバージョンは作成したいバージョン毎にインストールする必要があります。
インストール時に環境変数の追加は不要です。(Pythonランチャーが管理してくれます)
Pythonランチャーは環境変数にないPythonも認識します。
各バージョン情報はコマンドプロンプトから py コマンドで確認できます。
# Python3.8と3.9を入れた場合の表示例
> py --list-paths
Installed Pythons found by py Launcher for Windows
-3.9-64 C:\Users\poco\AppData\Local\Programs\Python\Python39\python.exe *
-3.8-64 C:\Users\poco\AppData\Local\Programs\Python\Python38\python.exe
参考:Pythonの複数バージョンの扱い方(Windowsの場合)
1-2. venvによる仮想環境の作成
特定のディレクトリを決め、その配下に仮想環境を作っていきます。
ディレクトリの場所は後でVSCodeにパスを通すのでどこでも大丈夫です。
# venv用のディレクトリを作成(例はホームディレクトリ)
> cd
> mkdir venv
> cd venv
# v3.9で仮想環境を作成する例("test_py39"が名前になります)
> py -3.9 -m venv test_py39
> dir /b
test_py39
2. VSCodeのインストール
IDEはVSCodeを使います。
公式からインストールします。
インストール後、以下の拡張機能を追加します。(他の拡張機能は後述)
- Japanese Language Pack for Visual Studio Code
- Python
2-1. プロジェクトディレクトリを開く
プロジェクトディレクトリを作成してVSCodeで開きます。
ここではCドライブ直下の sample_app を対象とします。(C:\sample_app\
)
VSCodeで ファイル → フォルダを開く → C:\sample_app\
を選択します。
2-2. VSCodeとvenvの紐づけ
VSCodeで ファイル → ユーザ設定 → 設定
の後、
設定の検索で python.venvPath
をいれて ~/venv
を設定します。
(~/venv
は先ほど作成したvenvのフォルダです)
これでvenvと紐づきましたが、確認のために hello.py を作成して開きます。
py ファイルを開くと左下に実行時のインタープリターが表示されます。
それをクリックし、venvの環境が表示されれば紐づいています。(それを選択します)
紐づけ後の実行ですが、VSCodeからpythonを実行するためには権限が必要です。
参考:【Python入門】venv仮想環境の使い方を習得してVS Codeでの開発をスムーズに!
Power Shell を管理者権限で実行し、Set-ExecutionPolicy RemoteSigned
を実行します。
(選択は Y
を入力)
PS C:\Windows\system32> Set-ExecutionPolicy RemoteSigned
実行ポリシーの変更
実行ポリシーは、信頼されていないスクリプトからの保護に役立ちます。実行ポリシーを変更すると、about_Execution_Policies
のヘルプ トピック (https://go.microsoft.com/fwlink/?LinkID=135170)
で説明されているセキュリティ上の危険にさらされる可能性があります。実行ポリシーを変更しますか?
[Y] はい(Y) [A] すべて続行(A) [N] いいえ(N) [L] すべて無視(L) [S] 中断(S) [?] ヘルプ (既定値は "N"): Y
これでVSCode上でPythonが実行できるようになりました。
pyファイルを適当に開いて、メニューバーの 実行 → デバッグなしで実行
で実行できると思います。
2-3. Jupyter Notebookの導入(オプション)
任意ですが、Jupyter Notebook も使えると便利なので使えるようにしておきます。
拡張機能に Python
を入れると自動で Jupyter
も入ってると思います。(なければ入れてください)
メニューバーの 表示 → コマンドパレット → Jupyter: Create New Jupyter Notebook
で新しいノートブックを作成します。
ファイル名は hello.ipynb
としておきます。
ノートブックで実行するインタープリタは右上に表示されます。
また、初回の実行では ipykernel
のインストールを求められるのでインストールします。
3. 開発環境
3-1. settings.json
プロジェクト内の開発環境は settings.json で管理します。
表示 → コマンドパレット → 基本設定: ワークスペース設定を開く (JSON)
(英語:Preferences: Open Workspace Settings (JSON))
を選択します。
するとプロジェクトのディレクトリ配下に .vscode/settings.json
ができます。
この後の設定はこのsettings.jsonを編集する形で追加していきます。
3-2. 拡張機能の管理
VSCodeでは必要な拡張機能を推奨する機能があるので、それでプロジェクトに必要な拡張機能を管理します。
すでに追加されている拡張機能を 右クリック → ワークスペースの推奨事項に追加する
を選択します。(例えば Python
)
すると .vscode/extensions.json
ができます。
このファイルで必要な拡張機能が管理できます。
ただ、デフォルトだと推奨機能は無効化されているので、settings.json に以下を追加します。
{
(略)
"extensions.ignoreRecommendations": false, // 推奨拡張機能を表示
(略)
}
以降、プロジェクトで必要な拡張機能は extensions.json
に追加していきます。
3-3. Python実行環境の管理
VSCodeの左メニューから 実行とデバッグ
を選択し、launch.json ファイルを作成します
を押します。
そして、python → Python Files
と選択すると .vscode/launch.json
が作成されます。
{
// IntelliSense を使用して利用可能な属性を学べます。
// 既存の属性の説明をホバーして表示します。
// 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
}
]
}
このファイルで python の実行環境を管理します。
3-4. パッケージ管理
Jupyter Notebook による管理方法を書いてきます。
これはあまり一般的ではありません。
(venvで仮想環境を作っているので都度インストールすればいいかなと思っています)
他には配布等でバージョンを固定したい場合は requirements.txt
による管理でしょうか。
その他の選択肢としては以下を参照してください。
参考:2020 年の Python パッケージ管理ベストプラクティス
Jupyter Notebook による利点は1回実行すればすべてのパッケージがインストールできる点と、各ライブラリに対してコメントが書ける点です。
(後、実行ログも残ります)
# %%
# pipをインストールするセル
!pip install numpy
# %%
# 各バージョンを表示
!pip list
コード例はgithubの パッケージ.ipynb
ファイルを参照してください。
4.コーディング規約
4-1. フォーマッタ/リンター
コーディング規約を守る上で自動的に指摘・修正してくれるフォーマッタとリンターは便利です。
今回はPEP8に準拠している flake8 と black(とisort) を使います。
その他のリンターについての比較は以下を参照。
参考:2021年Python開発リンター導入のベストプラクティス
flake8の設定
settings.json
に以下を追加します。
{
(略)
"python.linting.enabled": true, // リンターの有効化
"python.linting.pylintEnabled": false, // defaultではpylintが有効なので無効化
"python.linting.flake8Enabled": true, // flake8を有効化
"python.linting.lintOnSave": true, // ファイル保存時にリンター実行
// flake8への引数(追加設定)
"python.linting.flake8Args": [
"--max-line-length=119", // defaultは79、119はgit向け
],
(略)
}
pyファイル保存時に flake8 がインストールされていないと以下のメッセージが出るのでインストールします。
black/isortの設定
black/isortの設定は以下です。
{
(略)
"python.formatting.provider": "black", // 使うフォーマッタ
// pythonファイルのフォーマッタの設定
"[python]": {
"editor.formatOnSave": true, // ファイル保存時にフォーマッタ実行
"editor.codeActionsOnSave": {
// isortの実行(isortは標準でvscodeに入ってる)
"source.organizeImports": true
}
},
// blackへの引数(追加設定)
"python.formatting.blackArgs": [
"--line-length=119", // defaultは79、119はgit向け
],
(略)
}
pyファイル保存時に black がインストールされていないと以下のメッセージが出るのでインストールします。
4-2. 型アノテーション
型アノテーションによりPythonで明示的に変数の型を書くことができ、型チェックが可能になります。
型チェックツールとしてはPylanceを使いました。
(PylanceはVSCodeと同じマイクロソフトが開発しています)
まずは拡張機能でPylanceを入れます。(推奨機能にも追加しておきます)
{
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance" //追加
]
}
次に settings.json
に以下の設定を入れます。
{
(略)
// 型アノテーション(Pylance)
"python.analysis.typeCheckingMode": "basic"
(略)
}
basic
は型がある場所のみをチェック、strict
は型が指定していない箇所もチェックという感じらしいです。
5.構成管理
5-1. ディレクトリ構成例
アーキテクチャやフレームワークによって変わると思いますが、一例を書いておきます。
あくまで例となり、不要なディレクトリもあると思います。
.
├─ .vscode // vscodeに関する設定情報
│ ├─ extensions.json // 拡張機能情報
│ ├─ launch.json // 実行情報
│ └─ settings.json // 設定全般の情報
│
├─ setting // APIキーやログイン情報などのアプリの設定情報
│ ├─ setting_xxx.yml // xxx実行用(例えば本番とテストで異なる設定をファイルで分ける)
│ └─ setting_yyy.yml // yyy実行用
│
├─ src // ソースコード
│ ├─ common // 共通で使うコード置き場、utilsとかlibとかでもいいかも
│ ├─ setting
│ │ └─ Setting.py // 設定情報を扱うクラス(後述)
│ │
│ └─ module // メインコード置き場
│ ├─ module1.py
│ └─ module2.py
│
├─ tests // テストコード置き場
│ └─ module // テストコード配下はメインコードと同じ構造
│ ├─ test_module1.py
│ └─ test_module2.py
│
├─ log // ログ保存場所
├─ tmp // 一時ファイル置き場
├─ bin // (コンパイル済み)実行ファイル置き場
├─ examples // サンプルコード
├─ docs // ドキュメント関係
├─ data // 変化しないデータ置き場(マスターデータ等)
├─ .gitignore
├─ .env // 環境変数用
├─ LICENSE
├─ README.md
└─ パッケージ.ipynb // パッケージ管理用
5-2. gitignore
gitignoreでは共有したくないファイルを指定します。
以下のような感じで記載していきます。
# プロジェクト上、共有不要なファイル
log/
tmp/
bin/
.git
# プロジェクトと関係ないファイル
qiita.md
# python の gitignore、以下を参照
# https://github.com/github/gitignore/blob/master/Python.gitignore
ここで注意ですが、本記事では .env
は共有します。
(gitignoreにある時があるのでコメントアウト)
(VSCodeで環境変数を設定するために必要でした)
5-3. importパス管理
Pythonのimportは結構癖がある印象です。
具体的には実行ディレクトリ以上の階層には相対mportできず、しようとしたらimportパスを通して直接importする必要があります。
パスを追加する方法は sys.path に追加する方法と環境変数を追加する方法があります。
どちらがいいかは難しいようです。(PYTHONPATH vs. sys.path)
本記事では環境変数を追加する方法で実装し、importのルートディレクトリを src
で統一します。
VSCodeでの設定ですがまず .env
ファイルを作ります。
PYTHONPATH="src"
次に settings.json
に以下を追記します。
{
(略)
"python.envFile": "${workspaceFolder}/.env",
(略)
}
importパス管理(Jupyter Notebook)
Jupyter Notebook では、環境変数に追加する方法は少しややこしかったので sys.path に追加する方法にしています。
追加する場合は以下のコードを書きます。
import os
import sys
pwd = %pwd
# 階層はファイルの位置に合わせて修正
path = os.path.abspath(os.path.join(pwd, "../../src/"))
print(path)
sys.path.append(path)
5-4. 設定ファイル管理(setting)
ここの設定ファイルはアプリ側で使う設定を指します。
setting という単語を使っていますが、configでもいいと思います。
設定ファイルの記載はyaml形式にしています。
jsonでも問題ないですが、人が扱う場合はyamlの方が見やすい印象があるのでyamlにしています。
関係あるファイルは以下です。
.
├─ setting
│ ├─ setting_xxx.yml
│ └─ setting_yyy.yml
└─ src
└─ setting
└─ Setting.py
setting_xxx.yml が人が管理するファイルで、Setting.py がコード側の管理です。
また、yamlを使っているので pip install pyaml が必要になります。
以下各ファイルの記載例です。
# 例えば踏み台サーバへのログイン情報の記載例
HOST: xxx.xxx.xxx.xxx
PORT: 22
USER: hoge
PASSWORD: hoge
import os
import yaml
class Setting:
def __init__(self, name):
# name により使用用途を分ける
if name == "test":
fn = "setting_test.yml"
elif name == "web":
fn = "setting_web.yml"
else:
raise ValueError()
path = os.path.join(os.path.dirname(__file__), "../../setting", fn)
with open(path) as f:
self.data = yaml.safe_load(f)
# データルートディレクトリ
self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data"))
# tmpディレクトリ
self.tmp_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../tmp"))
# その他、グローバル変数などを管理してもいいかも
from setting.Setting import Setting
setting = Setting("test")
# 一時出力したい
import os
with open(os.path.join(setting.tmp_dir, "test.txt"), "w") as f:
f.write("test")
5-4. ライセンス
ライセンスはあまり詳しくありませんが、理由がない場合はライセンスなしより何かつけていたほうがいいそうです。
ライセンスがないコードは書いた人の著作物になるため、著作者の許可がないとコードの複製や再配布、改変ができなくなります。
本アプリではMITライセンスを設定しています。(LICENSEファイルが該当します)
MITライセンスを簡単に言うと、「改変でも、再配布でも、商用利用でも、有料販売でも、どんなことにでも自由に無料でつかうことができ、問題が起きても作者は何も責任を負いません」というライセンスです。
参考
・ライセンスをつけないとどうなるの?
・MITライセンスってなに?どうやって使うの?商用でも無料で使えるの?
6. コード管理
ソフトウェアアーキテクチャ(MVC等)においてメインに扱っていない領域を対象にしています。
具体的にはテスト、Logger、例外処理などです。
6-1. テスト
pytestを使う場合を記載します。
左の テスト中 → Configure Python Tests
を押し、 pytest (使うテストの種類) → tests (テストのディレクトリ)
を選びます。
すると settings.json に以下が書き込まれ、テストの設定が終わります。
直接書いても問題ありません。
{
(略)
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
(略)
}
設定後、テストを実施すると以下のようにテスト結果が表示されます。
テストのディレクトリ構成
テストは tests 配下に src 配下と同じ構成になるようにテストを作っています。
例えば omikuji/Omikuji.py に対応するテストは omikuji/test_Omikuji.py です。
.
├─ src
│ └─ omikuji
│ └─ Omikuji.py
│
└─ tests
└─ omikuji
└─ test_Omikuji.py
テストの検証(Jupyter Notebook)
テストを作成/検証するためにコードをちょっと動かしたいという場合があると思います。
そういう場合はNotebookが便利な気がします。
対応するテストと同じ名前で作成し、そこでコードを実行しています。
.
└─ tests
└─ omikuji
├─ test_Omikuji.ipynb ★
└─ test_Omikuji.py
6-2. Logger
出力設定
コンソールのみの出力
主に検証での実行など、一時的に見たい場合です。
どこでも使えるようにutils.pyに記載しています。
import logging
import warnings
def set_logger(name="", level=logging.DEBUG):
logger = logging.getLogger(name)
logger.setLevel(level)
formatter = logging.Formatter("%(asctime)s %(name)s %(funcName)s %(lineno)d [%(levelname)s] %(message)s")
# コンソール表示
ch = logging.StreamHandler()
ch.setLevel(level)
ch.setFormatter(formatter)
logger.addHandler(ch)
# ライブラリ別にログレベルを調整
#logging.getLogger("matplotlib").setLevel(logging.INFO)
# 余分なwarningを非表示
warnings.simplefilter("ignore")
ログフォーマッタの出力例は以下です。
2021-12-05 23:08:35,236 src.web.index omikuji 40 [DEBUG] test
ファイルへの出力
ファイルに出力したい場合の記載例です。
デバッグの場合と分けています。
logger = logging.getLogger("")
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter("%(asctime)s %(name)s %(funcName)s %(lineno)d [%(levelname)s] %(message)s")
if DEBUG:
# debugはコンソールとファイルに出力、ファイルは都度初期化
ch = StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)
logger.addHandler(ch)
logfile = "debug.log"
os.makedirs(os.path.dirname(logfile), exist_ok=True)
ch = FileHandler(logfile)
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)
logger.addHandler(ch)
else:
# 本番はファイルをローテーションで保存、コンソールはなし
# when=MIDNIGHT,backupCount=30 は、30日分のログを保存する
logfile = "web.log"
os.makedirs(os.path.dirname(logfile), exist_ok=True)
#ch = TimedRotatingFileHandler(logfile, when="D", backupCount=30)
ch = TimedRotatingFileHandler(logfile, when="MIDNIGHT", backupCount=30)
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)
logger.addHandler(ch)
追記:
when引数はDではなくMIDNIGHTが正解のようです。
TimedRotatingFileHandler コンストラクタの when引数に 'D' を指定してしまった場合の動き
記載側
ファイルの先頭に以下を記載するようにします。
import logging
logger = logging.getLogger(__name__)
このloggerを使って記載していきます。
logger.debug("test1")
logger.info("test2")
logger.warn("test3")
logger.error("test4")
logger.critical("test5")
出力例は以下です。
2021-12-06 23:53:27,222 __main__ <module> 10 [DEBUG] test1
2021-12-06 23:53:27,222 __main__ <module> 11 [INFO] test2
2021-12-06 23:53:27,222 __main__ <module> 12 [WARNING] test3
2021-12-06 23:53:27,223 __main__ <module> 13 [ERROR] test4
2021-12-06 23:53:27,223 __main__ <module> 14 [CRITICAL] test5
6-3. 例外処理
例外処理自体の設計はアプリに依存するため触れません。
ログへのトレースバックの出力記載例を書いておきます。
(よく書き方を調べるので)
import traceback
try:
a = 10 / 0
except Exception as e:
logger.warn(e.args)
logger.debug(traceback.format_exc())
raise # 例外をそのまま上に投げる
ログへの出力例は以下です。
2021-12-06 22:05:14,010 __main__ <module> 20 [WARNING] ('division by zero',)
2021-12-06 22:05:14,010 __main__ <module> 21 [DEBUG] Traceback (most recent call last):
File "c:\my_app\src\sample.py", line 18, in <module>
a = 10 / 0
ZeroDivisionError: division by zero
7. 実装例(おみくじ)
サンプルとして、ボタンを押すとサーバ側で結果を出し、クライアント側で表示するアプリを作成してみました。
コードは github を参照してください。
7-1. Webの実行(flask)
src/flask/run.py
を作成し、ここをflaskの実行ポイントとします。
launch.json
を開き、構成の追加を押します。
Pytho → flask → src/web/run.py
を選択すると以下のコードが書き込まれます。
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"env": {
"FLASK_APP": "src/web/run.py",
"FLASK_ENV": "development"
},
"args": [
"run",
"--no-debugger"
],
"jinja": true
}
実行する場合は、VSCode左の実行とデバッグより Python: Flask
を選択し、実行(緑三角)を押します。
(pythonファイルを単体で実行したい場合は Python: Current File
に戻します)
7-2. HTMLフォーマッタの追加
Webアプリでは Python だけでなく HTML も書くのでフォーマッタを追加します。
settings.json
に以下を追加します。
{
(略)
"[html]": {
"editor.formatOnSave": true, // ファイル保存時にフォーマッタ実行
},
(略)
}
さいごに
書いていたら内容が広い範囲にわたってしまいました。
書いてる途中でここら辺詳細分からないなーという項目もありましたが、折角なので一通り書いてみました。
おかしい実装をしている点もあるかと思いますので、参考程度に見ていただければと思います。