Help us understand the problem. What is going on with this article?

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

お題

前回、Rubyで書いた簡易ツール「ローカルJSONファイルに保存するキーバリューストア」のPython3版。
シリーズ物としては、もともと複数言語で同じ内容のプログラムを書いて比較した「簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ」からの改善版。

試行Index

更新履歴

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

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

[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]ストア情報の管理

[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]各コマンドの管理

[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]各コマンドの親クラス

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


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

各コマンドクラス

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

■保存

[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

■1件取得

[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

■全件取得

[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

■1件削除

[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

■全件削除

[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

■ヘルプ

[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

■アプリ終了

[end.py]
from command import Command


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

まとめ

次は、どの言語にしようか。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away