【改善編】簡単なツール作成を通してPython3を学ぶ

# お題
[前回](https://qiita.com/sky0621/items/5868b19934187571d0cd)、Rubyで書いた簡易ツール「ローカルJSONファイルに保存するキーバリューストア」のPython3版。
シリーズ物としては、もともと複数言語で同じ内容のプログラムを書いて比較した「[簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)」からの改善版。

# 試行Index
- 第１回：[簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)
- 第２回：[【改善編】簡単なツール作成を通してRubyを学ぶ](https://qiita.com/sky0621/items/5868b19934187571d0cd)
- 第３回：[【改善編】簡単なツール作成を通してPython3を学ぶ](https://qiita.com/sky0621/items/27c610e08391c7cc83ea)
- 第４回：[【改善編】簡単なツール作成を通してGolangを学ぶ](https://qiita.com/sky0621/items/cb14aa67ef327922c0a1)
- 第５回：[【改善編】簡単なツール作成を通してJavaを学ぶ](https://qiita.com/sky0621/items/f19f5191754f61a4d4c3)
- 第６回：[【改善編】簡単なツール作成を通してScalaを学ぶ](https://qiita.com/sky0621/items/a68ae8b64d4856d1a195)
- 第７回：[簡単なツール作成を通してRustを学ぶ](https://qiita.com/sky0621/items/86d9159bc1930618162c)
- 第８回：[【改善編】簡単なツール作成を通してRustを学ぶ](https://qiita.com/sky0621/items/a70cc7a9453795e0c9e9)

# 更新履歴
- 【2019/09/21】コメントにならってプログラム修正

# 実装・動作確認端末
### # 言語バージョン
```
$ python3 --version
Python 3.6.8
```

### # IDE - PyCharm
```
PyCharm 2019.2 (Professional Edition)
Build #PY-192.5728.105, built on July 24, 2019
```

# 実践

## 要件
アプリを起動すると、キーバリュー形式でテキスト情報をJSONファイルに保存する機能を持つコンソールアプリ。
オンメモリで保持していた点だけ除けば前回と同じ仕様なので詳細は以下参照。
https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff#要件

### ソース全量
https://github.com/sky0621/book_py/tree/v0.2.1

## 解説

### 全ソースファイル

|Pythonソース|説明|
|---|---|
|main.py|アプリ起動エントリーポイント|
|store_info.py|キーバリュー情報を保存するストア(JSONファイル)に関する情報を扱う。<br>現状は「ファイル名」だけ保持|
|commands.py|キーバリューストアからの情報取得や保存、削除といった各コマンドを管理。<br>コマンドの増減に関する影響は、このソースに閉じる。|
|command.py|各コマンドに共通のインタフェース(と言いたいがPythonにはインタフェースが無いので普通のclass)|
|save.py|キーバリュー情報の保存を担う。|
|get.py|指定キーに対するバリューの取得を担う。|
|list.py|全キーバリュー情報の取得を担う。|
|remove.py|指定キーに対するバリューの削除を担う。|
|clear.py|全キーバリュー情報の削除を担う。|
|help.py|ヘルプ情報の表示を担う。|
|end.py|アプリの終了を担う。|

### [main.py]アプリ起動エントリーポイント

```python:[main.py]
from commands import Commands
from store_info import StoreInfo


def main():
    commands = Commands(StoreInfo("store.json"))

    print("Start!")

    running = True
    while running:
        running = commands.exec(input().split())


if __name__ == '__main__':
    main()
```

### [store_info.py]ストア情報の管理

```python:[store_info.py]
# Pythonには定数はない。ただ、全て大文字の変数を定数と扱うのが暗黙のルールとなっているらしい。
DEFAULT_STORE_NAME = "store.json"


class StoreInfo:
    # オブジェクト生成時に自動で呼ばれるメソッド（コンストラクタ扱い）
    # デフォルト引数が指定できる
    # インスタンス変数privateにする方法は無いが慣習的に「_」で始まる変数はprivate扱いとする
    def __init__(self, _store_name=DEFAULT_STORE_NAME):
        self._store_name = _store_name

    def get_name(self):
        return self._store_name
```

### [commands.py]各コマンドの管理

```python:[commands.py]
import os

from clear import Clear
from end import End
from get import Get
from help import Help
from list import List
from remove import Remove
from save import Save
from store_info import StoreInfo


class Commands:
    def __init__(self, store_info: StoreInfo):
        self.commands = {
            "end": End(store_info),
            "help": Help(store_info),
            "clear": Clear(store_info),
            "save": Save(store_info),
            "get": Get(store_info),
            "remove": Remove(store_info),
            "list": List(store_info),
        }
        if not os.path.isfile(store_info.get_name()):
            self.commands.get("clear").exec(self)

    def exec(self, cmds):
        if len(cmds) < 1:
            print("no target")
            return True

        c = self.commands.get(cmds[0])
        if c is None:
            print("no target")
            return True

        if len(cmds) == 1:
            return c.exec([])
        else:
            return c.exec(cmds[1:])
```

### [command.py]各コマンドの親クラス

```python:[command.py]
# Ruby同様、Pythonも言語仕様にinterfaceを持たないのでclassで定義
# このクラスをそのまま使うことは想定していないため、デフォルトの処理としてエラースローしておく


class Command:
    def exec(self, args):
        raise NotImplementedError
```

### 各コマンドクラス

各コマンドについては、ストア情報がオンメモリのハッシュからJSONに変わった点以外はやることは一緒。（なので説明省く。）

#### ■保存

```python:[save.py]
import json

from command import Command


class Save(Command):
    def exec(self, args):
        if len(args) != 2:
            print("not valid")
            return True

        with open(self.store_info.get_name(), 'r') as fr:
            json_data = json.load(fr)
            json_data[args[0]] = args[1]
            with open(self.store_info.get_name(), 'w') as fw:
                json.dump(json_data, fw)
        return True
```

#### ■１件取得

```python:[get.py]
import json

from command import Command


class Get(Command):
    def exec(self, args):
        if len(args) != 1:
            print("not valid")
            return True

        with open(self.store_info.get_name(), 'r') as fr:
            json_data = json.load(fr)
            if args[0] in json_data:
                print(json_data[args[0]])
        return True
```

#### ■全件取得

```python:[list.py]
import json

from command import Command


class List(Command):
    def exec(self, args):
        with open(self.store_info.get_name(), 'r') as fr:
            json_data = json.load(fr)
            print('"key","value"')
            for k, v in json_data.items():
                print(f'"{k}","{v}"')
        return True
```

#### ■１件削除

```python:[remove.py]
import json

from command import Command


class Remove(Command):
    def exec(self, args):
        if len(args) != 1:
            print("not valid")
            return True

        with open(self.store_info.get_name(), 'r') as fr:
            json_data = json.load(fr)
            if args[0] in json_data:
                del json_data[args[0]]
                with open(self.store_info.get_name(), 'w') as fw:
                    json.dump(json_data, fw)
        return True
```

#### ■全件削除

```python:[clear.py]
import json

from command import Command


class Clear(Command):
    def exec(self, args):
        with open(self.store_info.get_name(), 'w') as f:
            json.dump({}, f, ensure_ascii=False)
        return True
```

#### ■ヘルプ

```python:[help.py]
from command import Command


class Help(Command):
    def exec(self, args):
        msg = """
        [usage]
        キーバリュー形式で文字列情報を管理するコマンドです。
        以下のサブコマンドが利用可能です。

        save   ... keyとvalueを渡して保存します。
        get    ... keyを渡してvalueを表示します。
        remove ... keyを渡してvalueを削除します。
        list   ... 保存済みの内容を一覧表示します。
        clear  ... 保存済みの内容を初期化します。
        help   ... ヘルプ情報（当内容と同じ）を表示します。
        end    ... アプリを終了します。

        """
        print(msg)
        return True
```

#### ■アプリ終了

```python:[end.py]
from command import Command


class End(Command):
    def exec(self, args):
        print("End!")
        return False
```

# まとめ
次は、どの言語にしようか。