Thought Log

航空宇宙エンジニアの卵、もうすぐ就職

Pythonで家計簿を作る、その3の備忘録 Hatenaブログでのコード表示

今回の記事からPythonソースを載せる必要があるので、ソースの載せ方についてメモ。やりかたは結構色々あるみたいです。なおタブなどの日本語名称がややおかしいかもですがご了承ください(英語版ユーザ)。

①一番正規っぽい方法:はてな記法で>|python| |<

はてなブログの編集方法は”見たまま(WYSIWYG)”、”はてな記法”、”マークダウン”と三種類あり、僕のような新規ユーザはデフォルトで”見たまま”になりますが、正規っぽくコードを埋め込む方法は実はデフォルト以外の”はてな記法”と”マークダウン”だけが対象になります。これら二つであればコード用のタグが用意されており、その間にコードを書くことで自動的にSuntax highlightまでやってくれるようです。以下のブログなどを参考にしました。

kurokinomizuiwa.hatenablog.com

編集モードは一旦編集を始めてからでは行き来できるものではないため、正規でこれを使うのはこれからのブログを全て”はてな記法”というよくわからない記法で書く必要があります。若干のHTMLの知識がある自分にとっては”見たまま”の編集+細部を”HTML”モードで修正するスタイルが合っている(というよりどうせ勉強するなら汎用的なHTMLを使いたい)のでこの使い方はなしと判断しました。また上記のブログには、一旦コードだけをはてな記法の記事に書いて下書き保存し、プレビューのフォーマット済みコードを”みたまま”記法のブログにコピペする方法も紹介されていました。これは2度手間になりますし、スタイルの微調整(ハイライトの色など)ができなさそうです。以下がこの方式の結果です。

from os import path
import sqlite3

def read_db(c):
    for row in c.execute('SELECT * FROM flow_data'):
        print(row)

if __name__=='__main__':
    assert not path.exists('test.db') # ensure .db is a new file each run
    c = sqlite3.connect('test.db')
    c.execute('PRAGMA foreign_keys = 1;')
    read_db(c)
    c.close()

②一番簡単な方法:VS Codeからシンプルにコピペ

上の方法を試行錯誤しているうちにもっと簡単な方法ができることに偶然気がつきました。VS Codeは一年ほど前からctrl+cでplain textだけでなくsyntax highlightも含めたhtmlをクリップボードに保存するようです。なので単純にVS Codeのコードをコピペするだけで以下のようになります。

from os import path
import sqlite3

def read_db(c):
for row in c.execute('SELECT * FROM flow_data'):
print(row)

if __name__=='__main__':
assert not path.exists('test.db') # ensure .db is a new file each run
c = sqlite3.connect('test.db')
c.execute('PRAGMA foreign_keys = 1;')
read_db(c)
c.close()

これで見慣れたテーマのSyntax highlightでコードを見ることができ、自分にとっての可読性が格段と上がりました。しかしこの方法だとなぜか複数スペースもタブもスペース一個分に変換されてしまうようです。VS Codeはスペース←→タブの全変換も手軽にできるので色々試しましたが全てダメでした。Pythonにおいてはインデントの有無はコードが走るか走らないかに関わり、実際上のコードをVS Codeに貼り付けてもインデントエラーで動きませんでした。せっかくソースコードを貼るならコピペだけで動くように貼らないと意味がないと思ったので、この方法も断念しました。

③最終的に落ち着いた方法:highlight.jsの使用

HTMLモードで編集していた頃からhighlight.jsの存在には気付いていましたが、ヘッダやフッタにアクセスしようがないと思っていました。ところが以下のブログでこれを活用する方法を解説していました。

sw1227.hatenablog.com

これによって愛用するAtom One Dark(ブログ主さんと同じでした)のテーマを使用しつつ、インデントや行間高さなどの細部までコントロールできます。以下がこの方法での表示です。

from os import path
import sqlite3

def read_db(c):
    for row in c.execute('SELECT * FROM flow_data'):
        print(row)

if __name__=='__main__':
    assert not path.exists('test.db') # ensure .db is a new file each run
    c = sqlite3.connect('test.db')
    c.execute('PRAGMA foreign_keys = 1;')
    read_db(c)
    c.close()

ここで注意するべきなのがjsを使っているため編集は①、②と違いHTMLタブを使う必要があります。さらに”見たまま”タブではハイライトはされません。(プリビュータブでは見れます)それでもこの方式が一番優れていると判断し、今回の記事からはこれに従ってソースコードを埋め込もうと思います。

 

関連:Pythonで家計簿アプリを作る その3、データベース設計と実装 - Thought Log

Pythonで家計簿アプリを作る その3、データベース設計と実装

今回はデータベースに設計と実装について。家計簿アプリは家計簿データを中心に据えたプログラムであるので、これにはそれなりに時間を割きました。

DB設計方針

前回紹介したPrimary Key、データの型、制約、Foreign Keyに注意しつつ、家計簿アプリのDBデザインをしようと思います。迷った点や方針などを以下に記します。

  • 金額データは外貨の使用も想定してInt型ではなくReal型で保管。ただし円のみを使う当面の間、Python側で小数点以下がないことを確認してからDBに渡す
  • 金額データのマイナスを許容する。収入は正の、支出は負の金額として保管することでDB側で特殊な場合分けが不要になる。(ユーザ側でプラマイを入力するのは面倒なので、カテゴリによってPythonが自動的に正負を判断する)
  • 収入・支出を分けるフィールドをDBに作るか、Python側がカテゴリフィールドから自動で場合分けするか迷ったが、DBのシンプルさを優先して後者をとった
  • 収入支出と資産(フローとストック)を同一のDBに保管する必要は今のところないが、将来的につなげる可能性もなくもないので同一のDBに保管する
  • 詳細フィールドにはカテゴリよりも詳細な記述をするためのもので、必須でもよかったが、SQLite側では入力制限無しとした
  • タグフィールドは今のところ何に使うかわからないが、将来の分析機能が拡充した時に使うかもしれないので実装。使用想定:”#credit_card #electricity #auto-generated”等カテゴリではカバーできないデータをタグとしてテキスト化し、後に分析用の関数などが検索できるようにする

以上、何個か迷った点は大体プログラムの複雑化←→DBの複雑化のトレードオフ関係にあったので全て前者の方を選びました。

DB設計

前述の方針をもとに家計簿データを以下のように定義しました。

f:id:Nullius_in_verba:20220225145205p:plain

収入支出データ

f:id:Nullius_in_verba:20220225145237p:plain

資産データ

ここで(Autogenerated)とかいてある行は前回紹介した通り、SQLiteが自動で付け足してくれるのでこちらから定義する必要はありませんでした。収入と支出はDB側では分けないとの方針だったのですが、簡単のためにカテゴリテーブルのIDを1,2,3..を支出、101,102,103...を収入に当てて区分しました。今から考えるとIDにそんなカラクリを使うよりはカテゴリテーブルに新しく収入/出費フィールドを設けた方が他の人のReadabilityが向上しますね。

コードの実装

では実際にこのDBを作成、書き込み、読み出しする関数を書いていきます。収入支出と資産のデータ構造は一緒なので収入支出テーブルのみ表示します。まずはconnectionオブジェクトを生成します。

 c = sqlite3.connect('test.db')
c.execute('PRAGMA foreign_keys = 1;')

ここで前回紹介したForeign Key制約を有効化するためにpragmaコマンドを使います。Connectionオブジェクト毎に発行しなければいけないので注意しましょう。僕は忘れていました。

DB定義

次に先ほどの設計通りにテーブルを作成します。

# flow_cateogry table
sql = '''
CREATE TABLE flow_category
(
	category_code INTEGER PRIMARY KEY,
	category_name TEXT NOT NULL UNIQUE
);
'''
c.execute(sql)

# flow_data table
sql = '''
CREATE TABLE flow_data
( 
	date DATE NOT NULL,
	amount REAL,
	category_code INTEGER NOT NULL,
	detail TEXT,
	tags TEXT,
	FOREIGN KEY(category_code) REFERENCES flow_category(category_code)
);
'''
c.execute(sql)

SQLステートメントをstringとして保管し、これをc.execute()に渡すことで実行します。どうやらexecuteは一つの文(セミコロン一個分)づつしか実行できないみたいで、複数文をまとめて実行するためにはexecutescript()という関数があるみたいです。あとexecute()を実行してもdml(INSERTやDELETEなど)であれば仮書き込み状態となることに注意してください。本書き込みをするにはc.commit()でコミットすることが必要となります。今回はddl(CREATEなど)なのでどちらにせよauto-commitされるようです。

DB書き込み

テーブルの作成ができたので、カテゴリテーブルにカテゴリを書き込んでいきます。

flow_category_dict = {# 1,2-digits id reserved for expense
	1:'家賃関連',
	2:'食費',
	3:'日用品',
	# 3-digits id reserved for revenue
	101:'給料',
	102:'運用利益'}
	
# register flow_category 
for idx in flow_category_dict:
	sql = f''' 
	INSERT INTO flow_category VALUES({idx},'{flow_category_dict[idx]}');
	'''
	c.execute(sql)
c.commit()

ここまでは初期設定なのでUIを作る必要もなく、このままソースコードに実装しました。次に家計簿データの書き込みです。

date = '\'2022-02-01\''
amount = 0
category = 2
detail = 'ダミー'
tag = '#test'
sql = f'''
INSERT INTO flow_data VALUES({date},{amount},{category},'{detail}','{tag}');
'''
c.execute(sql)    

date = '2022-02-04'
amount = 0
category = 101
detail = 'ダミー2'
tag = '#test'
sql = f'''
INSERT INTO flow_data VALUES('{date}',{amount},{category},'{detail}','{tag}');
'''
c.execute(sql)
c.commit()

ここで若干の注意ですが、本来得るべきSQL構文は以下のようなもので、

sql='''
INSERT INTO flow_data VALUES('2022-02-04',0,2,'ダミー','#test');
'''

SQL内でstring型となるものには''が付いています。しかしsqlという変数はそれ自身がpythonのstr型なので、単にstr型を代入すると''が失われてしまいます。

var = 'apple'
f'this is an {var}'
>>'this is an apple'

そこで''記号を変数の方に埋め込めます。単に二重に入れるとstringをエスケープしてしまうのでバックスラッシュでリテラルであることを明示し、

var = '\'apple\''
f'this is an {var}'
>>"this is an 'apple'"

とする解決策と、もう一つは入れ込む方のstringに既に''をつけちゃう方法があります

var = 'apple
f"this is an '{var}'"
>>"this is an 'apple'"

pythonはstringを表す記号が""と''の二つあるからこそですね。いづれにせよ直接SQLステートメントをstring formattingするべきでないと公式に書いています。それは以下のような事例を防ぐためでもあると。

https://imgs.xkcd.com/comics/exploits_of_a_mom.png

一応訳しますと、

(学校)こんにちは、XX君の小学校ですがコンピュータ関連のトラブルがありまして…

(親)あらら、うちの息子が何か壊しました?

(学校)ある意味そうなのですが…本当に息子をRobert') DROP TABLE Students; --と名付けたのですか?

(親)あぁはいそうです、ボビーテーブルちゃんって呼んでいるんですよ。

(学校)どちらにせよ今年度の全生徒データを失いました、せめて貴方が楽しんだことを願いますよ。

(親)そして私は貴方がデータベース入力をしっかりフィルターするべきと学んだことを願います。

なかなキレのある面白い4コマですね、公式docに貼り付けてあるだけあります。

ではどうやるべきなのかについても公式は丁寧に書いてくれていますが、そもそもこれに気付いたのがこの記事を執筆している時だったので、その説明と自分のコードの改修はまたいつかやります。

DB読み出し

最後にDBにしっかり書き込めたのかの確認をします。SQL文がSELECTだった場合execute()関数はクエリ結果をイテレータとして返してくれるようです。このように1行づつFor文で取り出せます。

for row in c.execute('SELECT * FROM flow_data'):
	print(row)
>>('2022-02-01', 0.0, 2, 'ダミー', '#test')
>>('2022-02-04', 0.0, 101, 'ダミー2', '#test')

また”0”とint型っぽく渡した金額はちゃんと0.0とreal型で保管されていることも確認できます。実際のコードの方ではクエリ結果の取り出しはfor文ではなくpandasのread_sql_query()関数で行うのですが、それについてはまた後ほど。

全部合わせて

ここまでのDB作成、書き込み、読み出しまでをまとめたコードがこちらです。

from os import path
import sqlite3

def define_db(c):
	flow_category_dict = {# 1,2-digits id reserved for expense
		1:'家賃関連',
		2:'食費',
		3:'日用品',
		# 3-digits id reserved for revenue
		101:'給料',
		102:'運用利益'}

	# flow_cateogry table
	sql = '''
	CREATE TABLE flow_category
	(
		category_code INTEGER PRIMARY KEY,
		category_name TEXT NOT NULL UNIQUE
	);
	'''
	c.execute(sql)

	# flow_data table
	sql = '''
	CREATE TABLE flow_data
	( 
		date DATE NOT NULL,
		amount REAL,
		category_code INTEGER NOT NULL,
		detail TEXT,
		tags TEXT,
		FOREIGN KEY(category_code) REFERENCES flow_category(category_code)
	);
	'''
	c.execute(sql)
	# register flow_category 
	for idx in flow_category_dict:
		sql = f''' 
		INSERT INTO flow_category VALUES({idx},'{flow_category_dict[idx]}');
		'''
		c.execute(sql)
	c.commit()


def write_db(c):
	date = '\'2022-02-01\''
	amount = 0
	category = 2
	detail = 'ダミー'
	tag = '#test'
	sql = f'''
	INSERT INTO flow_data VALUES({date},{amount},{category},'{detail}','{tag}');
	'''
	c.execute(sql)    

	date = '2022-02-04'
	amount = 0
	category = 101
	detail = 'ダミー2'
	tag = '#test'
	sql = f'''
	INSERT INTO flow_data VALUES('{date}',{amount},{category},'{detail}','{tag}');
	'''
	c.execute(sql)
	c.commit()

def read_db(c):
	for row in c.execute('SELECT * FROM flow_data'):
		print(row)

if __name__=='__main__':
	assert not path.exists('test.db') # ensure .db is a new file each run
	c = sqlite3.connect('test.db')
	c.execute('PRAGMA foreign_keys = 1;')
	define_db(c)
	write_db(c)
	read_db(c)
	c.close()

今回はデータベース設計方針と設計、そしてテストコードへの実装を紹介しました。次回は何を書くか未定です、Pandasの使用法な気がします。

 

次回:TBD

前回:Pythonで家計簿アプリを作る その3の脇道、SQLとデータベースの勉強 - Thought Log

 

Pythonで家計簿アプリを作る その3の脇道、SQLとデータベースの勉強

今回はデータベース設計について書きたかったのですが、SQLiteSQLについて学んだまとめが肥大化したので外伝的な形で先に載せようと思います。

RDB(Relational Database)とSQL

SQLという名前は耳にしたことがあるものの、①DBを操作するための言語であること、②HTMLと同様に多くのエンジニアはそれができてもプログラマと見做さない傾向があること、位の認識でした。今回わざわざSQLiteを使った理由としてSQLやDBの理解も挙げているので、ここでその成果を書きたいと思います。英語版Wikiを参照しました。

  • SQLはStructured Query Languageの略(クエリとは問合せのこと)
  • 元々SEQUEL(Structured English Query Language)だったこともあり、エスキューエルではなくシークェルと発音する人も多い、というか本家本元?
  • Relational Database Management (RDBM)の言語で、基本的には構造が定まったデータを扱うのに適している
  • 具体的にはCREATE、INSERT、SELECT、DELETEなどのSyntaxを使ってデータに対して①定義②操作(追加/変更/削除)③制御(アクセス権)、④クエリが行える言語のこと
  • Relational Database (RDB)とはrelational modelという情報科学理論に基づいたデータ形式で、恐ろしくデフォルメすると二次元の表形式で表せるデータベースのこと
  • E. F. Coddさんという人が70年代にrelational modelとその応用性を提唱して、それをゆるーく原作として作られたのがSQLおよびSQLが対象とするDB
  • SQLという言語は一つではなく、その操作対象となるRDBMシステムのベンダがそれぞれのバージョンを持っていて、若干の違いがあるため相互に互換性はない。例えばOraclePL/SQLMySQLSQL/PSMなど

どうやらこのE. F. Coddさんは界隈では超有名人っぽいです。流体力学におけるL. Prandtlみたいなもんですかね。彼の元論文であるA Relational Model of Data for Large Data Banksはネットに落ちていました。数弱の工学徒として代数学は履修していませんが、なんとなく意味はわかる程度には例などを沢山盛り込んでくれています。先にSQLの説明などを読んでforeign keysなどでテーブルとテーブルが繋がってるのがRDBのrelationなのかと思いきや、彼がrelationと呼んでいるのはテーブルの各列間の関係です。例えば

Company Contact Country
Alfreds Futterkiste Maria Anders Germany
Centro comercial Moctezuma Francisco Chang Mexico
Ernst Handel Roland Mendel Austria
Island Trading Helen Bennett UK
Laughing Bacchus Winecellars Yoshi Tannamuri Canada
Magazzini Alimentari Riuniti Giovanni Rovelli Italy

のようなデータベースがあったとすると、Company集合における要素Alfreds Futterkisteと、Contact集合における要素Maria Anders、そしてCountry集合における要素Germanyが”繋がって”、あるいは関係を持って1つのタプル(データ、表の1行目)を構成している訳ですが、このタプルの集合(各行の集まり)をrelationと呼んでいるみたいです。そのように表データを考えたことがなかったので目から鱗でした。他にも一般的には木状に表すようなデータを表で表すdata normalizationなど、このようなコンセプトを全く知らない門外漢にはわからないなりに発見が沢山ある論文でした。

f:id:Nullius_in_verba:20220224170715p:plain

木状に表されているデータの正規化例。例では*historyとなっているデータが親に対して複数個ある1対多データとなっている。(Codd 1970)

同時に、論文に書かれてあることはMicrosoftがデータベースデザインの基礎を指南しているこのページに書かれてあることと似ていて、彼の理論の影響力を思い知らされます。またデータベースはRDBだけでなくネットワーク型やキャッシュ型など色々あるみたいです。色々なデータベースの形に関してはこのYoutube動画が面白かったです。

 

www.youtube.com

データベース設計におけるSQLiteの特性

さてごちゃごちゃとSQL自体についてまとめてみましたが、より実践的に今SQLiteの操作方法を見ていきます。公式のドキュメンテーションがわかりやすかったです。

SQLiteの位置付け

前述の通り大企業がOracleなどを通してDB管理をSQLで行ったりもするのですが、SQLiteはある意味その真逆を目指していて、ローカルで(サーバレス)データをRDBで管理するためのツールとして作られています。公式にも"SQLiteはクライエント/サーバDBと競合するものではありません、競合相手はfopen()です。"との記述があります。今回の家計簿アプリはスタンダロン形式なのでピッタリでした。

データの定義(参考)

エクセルで表を作成する時、①表の名前をつけて②列に名前をつけて③1行づつデータ入力とやっていくと思いますが、SQLiteでも似ていて、①テーブルを定義して②フィールド(行)を定義して③1行づつデータ入力という流れになります。注意点として

  • Primary Key
  • データの型
  • 制約
  • Foreign Key

を気にする必要があります。

Primary Key

Primary Keyとはそのデータを一意的に特定するためのIDで、1つまたは複数の列をPrimary Keyと指定することができます。言わずもがなですが、その列に入るデータが重複するようなものだとダメですね。家計簿の明細を例にするとカテゴリ、日付、金額等はそれぞれ重複する可能性があるので単一ではPrimary Keyの列となり得ません。新たにID列を設定して1、2、3と入力順に数字打った方が簡単そうですね。と思ったらSQLiteはデフォルトで新たなテーブルを定義するとrowidというまさにそれをするPrimary Key列を作ってくれるらしいです。

データの型(参考

SQLiteは他のSQLとは違い、dynamic typing対応のようです。でもSQLiteの勉強というよりはSQLの勉強をしたいので、静的な感じでTypeを使おうと思います。SQLiteの中ではStorage Classというものがあり、以下の5つの種類があるようです。

  • NULL
  • INTEGER
  • REAL
  • TEXT
  • BLOB

まぁほとんど見た通りなので説明は省くとして、BLOBは入力されたデータをそのまま保管するという意味らしいです。バイナリとかも入れれるのかな?家計簿に関連するものでは日付型がないですが、これはTEXTで”YYYY-MM-DD”と入れることでなんとかなるっぽいです。

制約

それぞれの列に入るデータに制約をかけることもできます。今回使いそうなのは空欄にしてはいけないというNOT NULL制約ですね。金額とかに使えそうです。他にUNIQUEという、Primary Key以外の列で重複を避けるための制約もありますが、これはカテゴリ名称などの列に使えそうです。foreign keysも制約の一種ですが、これは次に説明します。

Foreign Key

SQLではテーブル間でデータをつなげる時に、つなげる先の相手のPrimary Keyを使い、これをForeign Keyと呼びます。詳しくはこのページがわかりやすいです。家計簿の例でいうとカテゴリー欄はある特定の値、例えば[家賃、食費、日用品]だけを許容するとします。このときにDBにこれを強制させる方法として、

カテゴリID カテゴリ名
01 家賃
02 食費
03 日用品

というカテゴリID欄をprimary keyとする新たなテーブルを作ってしまい、メインのテーブルのカテゴリ欄にカテゴリID欄(メインから見るとForeign key)だけが登録できるように制約をかければ、メインのテーブルには[家賃、食費、日用品]しか入力できない状態が達成できます。ここで注意なのは、Foreign Keyしか入力できないという制約はデフォルトではOFFになっているので、

PRAGMA foreign_keys = 1;

によってコネクション毎に制約をONにしなければいけません。

以上、SQLについて、SQLiteとデータ定義についてでした。次回はこの知識を用いて家計簿DBの設計と実装を紹介したいと思います。

 

次回:Pythonで家計簿アプリを作る その3、データベース設計と実装 - Thought Log

前回:Pythonで家計簿アプリを作る その2、要件定義など - Thought Log

 

その他:Pythonで家計簿を作る、その3の備忘録 Hatenaブログでのコード表示 - Thought Log

Pythonで家計簿アプリを作る その2、要件定義など

今回は参考にしたブログに倣って、開発前に行った設計を紹介します。(開発終盤の現在振り返ると実際のプログラムと食い違った部分もあるので、その都度注をいれます。)自分はITエンジニアではなく、最初から大規模なプログラムを書くつもりで大規模なプログラムを書いたこともないのでこのような経験は初めてです。おかしなことを書いていたら申し訳ないです。

要件定義

”家計簿データ”を金額・日付・カテゴリ等、明細に関する情報をひとまとめにしたデータと定義する。また家計簿データを収入・支出に関する”フロー”のデータと資産に関する”ストック”のデータに分け、それぞれについて以下の機能を有すること。

  1. 家計簿データの入力機能
    • できるだけ簡単で直感的なGUIで家計簿データを入力できること
    • 既に入力したデータをユーザ側から書き換える変更機能もあること
    • 固定収入/支出の自動入力や外部データからの自動入力機能などの拡張余地を確保すること
  2. 家計簿データの表示機能
    • わかりやすいフォーマットで家計簿データを表示すること
      (最初は表のみでいいが、最終的にはカレンダー表示させたい)
  3. 家計簿データの分析機能
    • 家計簿データを解析し、グラフなどを出力するサブ関数群が入る拡張余地を確保すること
  4. その他システム要件

を要件とします。解析用のサブ関数に関しては開発が終わった後も必要に応じて解析パターンを追加する、というよりもデータを見つつ解析プログラムを考えようと思っています。Windows互換性に関しては共有したい人が大体Windowsユーザだからなのと、折角クロスプラットフォームフレームワーク(Qt)を使っているのだからやってみよう程度の意気込みです。

プログラム基本設計とGUI設計

まずソフトウェアとDBがどのように関わり合うかを概念図としてまとめました。

f:id:Nullius_in_verba:20220224141621p:plain

プログラムとDBの関係図

次にGUIのイメージをfig化しました。こういったForm的なものを書いたのは中学生の時にHTML・Javascriptをかじった以来なので、どれくらい労力がいるかなどは想像もつかない中設計しました。難しすぎる課題を自分に課して挫折するのを避けるために、初期目標と最終目標の二つの画面を作りました。

f:id:Nullius_in_verba:20220224141621p:plain

GUI設計初期目標

f:id:Nullius_in_verba:20220224143743p:plain

GUI設計最終目標1

f:id:Nullius_in_verba:20220224143826p:plain

GUI設計最終目標2

以上、要件定義・基本設計・GUI設計を紹介したところで今回は終わります。次回はデータベース設計について紹介します。

 

次回:Pythonで家計簿アプリを作る その3の脇道、SQLとデータベースの勉強 - Thought Log

前回:Pythonで家計簿アプリを作る その1 - Thought Log

Pythonで家計簿アプリを作る その1

題名の通り、Pythonを使ってGUI家計簿アプリを三週間程度で作った話を備忘録的に書きます。開発したアプリの要点だけいうと、巷によくある家計簿アプリとほぼ同等の機能を持つことを目標にし、明細データを入力、保管、表示、変更できるプログラムを作成しました。またその明細データをもとにグラフを出力して出費傾向を可視化できるようにしました。主な使用モジュールは、

  • PyQt5でGUIを作り
  • SQLiteを使って家計簿の記録データを.dbファイルに保管し
  • PandasがSQLiteが読み取ったデータをデータフレームとして扱い
  • Matplotlibで家計簿データを可視化しました

作成したアプリの概要はこんな感じで、今回は自己紹介・開発を思い立った経緯、目標、使用環境とツールを書きます。なおソースコードはいずれGithubのレポジトリを公開すると思います。

f:id:Nullius_in_verba:20220224122345p:plain

開発中の家計簿アプリ。データはダミーデータです。

 

自己紹介・経緯

航空宇宙工学の学生として数値計算用にはかれこれ5年ほどC,C++,Python,Juliaなどをいじってきましたが、数値計算以外にはほぼ接点がなかったのでアプリ開発GUIなどは初心者。春から無事就職できるということで、2月初旬の修士論文提出後に何か暇を潰せるものはないかと思い、家計簿開発に挑戦。あくまでプログラミングの勉強という位置づけなので、少々Overkillな機能をつけたり、不必要にモジュールを入れたりしているかもですが、ご了承ください。

目標

まず、自分が一人暮らしをしている2年間使っていた家計簿アプリをご紹介します、特に機能に不満はなかったのでこれをお手本としました。

f:id:Nullius_in_verba:20220224112602p:plain

以前使っていたiOS用家計簿アプリ。

シンプル家計簿(かけいぼ) 人気おこづかい帳かけいぼ on the App Store

もう一つの(より重要な)お手本として、”Python 家計簿アプリ”で検索して最初にヒットしたmemopyさんのブログ。

memopy.hatenadiary.jp

この方のブログを読んで、①そもそもPythonで初心者でもできそうなことを把握、②単に.csvファイルなどに記録するだけでなく、.dbファイルにデータベース(DB)として保管すればSQLやDB管理の練習にもなるという発想を得ました。ソースコードもそのまま動く状態で載せてくださっているのでとても参考になりました。memopyさんのアプリは表ベースの簡素なものだったので、バックエンド(と表現するのでしょうか?DB管理方法)を継承しつつ、スマホアプリ並みのGUIと機能(カレンダーベースの表示、グラフでの可視化、検索機能、等々)を追加したものを目標としました。

(なおアプリそのものはもちろん、このブログの形式もmemopyさんから参考にさせていただいています。)

 

使用環境とツール

  • 使用マシンはMacbook Pro 2020モデル(M1チップ)
  • Windows検証用に8年ほど前のLet's Noteも使用
  • Pythonは3系統でPython 3.9.2、Anacondaの仮想環境上で動かしています
  • エディタはVS Code
  • 主な使用モジュールは前述の通り、
    • PyQt5
    • SQLite
    • Pandas
    • Matplotlib
  • ほぼ終わりかけにGitとGithubを導入
エディタ

長年CとC++のコーディングにAtomを使っていたのですが、どうも使い心地がハマらずPythonだけはAnacondaのSpyderを使っていました。(もちろんAtomほどカスタマイズできるエディタにおいてハマらないのは自分の力量不足なのですが…)このアプリ開発を試みるにあたって試しにVS Codeも使ってみたらハマったという経緯があります。ですのでエディタも初心者。

GUI

GUIに関してはPythonでは大体TKinterPyQtの選択肢があるそうで、memopyさんはTKinterを使用していますが、カスタマイズできる幅や本格度が上らしいということ、あと単純にコードコピペは面白くないという理由でPyQt5を選びました。なお2021年時点でPyQt6がリリースされていましたが、初心者としてはStackOverflow等で検索しやすい前バージョンを選びました。

DB管理

SQLiteとデータベース管理に関してはSQLの勉強として追加しました。1日10件明細があったとしても3年で約1万件ですので、まだ余裕でcsv管理できそうな気はしますが、こんなところで触れておかないと機械系エンジニアとしては触れる機会もないのであえてSQLを使います。SQLiteの選択は単にPythonについてきたからです。

Pandas

開発の本当の初期にSQLiteのクエリ結果を二重のリスト型として処理していたのですが、for loopだらけで読みにくいなぁと思った時にPandasを思い出しました。Pandasもほぼ初心者だったのですが、家計簿データをPandasデータフレームとして処理することでコードの分量も読みやすさも格段と向上しました。なおPandasがあると余計SQLiteと.dbファイルの必要性が薄れる気がしますが、勉強と割り切ってSQLiteは使用します。

GitとGithub

これに関しては数値計算系で4年も5年もコード書いてて初心者とは何事ぞと情報系の方には笑われそうですね。言い訳として自分の周りに使っている人が誰もいなかったんです。機械系エンジニアのコーディングなんてそんなものです。適当な間隔を空けてソースコードをver1とか2とか名前変えてコピペして保管していたのですが、.pyファイルが複数になり、GUI用に色々な付属ファイルが増えてきた時点で”コピペ・改名バージョン管理”を断念し、Gitを勉強するいい機会と捉えて導入しました。

 

以上、開発概要、経緯、目標と使用環境・ツールでした。次回は使用要求や基本設計など、書きたいと思います。

 

次回:Pythonで家計簿アプリを作る その2、要件定義など - Thought Log

 

7/3

 オランダ語のテキストを読んでいて、Donder(雷)という単語に出会った。なんかどっかで見たことある単語やな…と思ったらDonderdag(木曜日)の前半や!Dagは日という意味なので木曜日は直訳で”雷の日”になる。

ところでオランダ語の曜日は英語とめちゃくちゃ似ている。日~土を並べるとZondag, Maandag, Dinsdag, Woensdag, Donderdag, Vrijdag, Zaterdag。オランダ語と英語の綴りの違い上Z⇒S、V⇒Fを考慮するとめちゃ似てる。ここまで似ているからこそ英語では明確ではない曜日の意味がオランダ語でははっきりと表れるのにびっくりした。

 

ん、ちょっと待てよ…英語のThursdayも言われてみればThunder Day と取れなくもない…けど若干違うな、と思ってググってみるとめちゃくちゃ面白い結果を見つけた。なんと英語のThursdayは古英語のThor's Dayから来ている!

 

Thorといえば某アメコミ映画でおなじみ、ゲルマン神話に登場する雷神を指す。これは実は、長年持っていた疑問の見事すぎる〆だった。

 

その疑問とは日本語の曜日とスペイン語の曜日がほとんど一致するのに何故か英語の曜日が日本語とほとんど一致しない理由だ。

 

スぺ語で日~土はdomingo, lunes, martes, miercoles, jueves, viernes, sabadoで土日を除く曜日はそれぞれLuna(月の擬人化), Mars(軍神), Mercurius(商い等の神), Iuppiter(神々の王、雷神), Venus(美神)とローマ神話の神々と対応している。ちなみに土日はキリスト教由来のものに変更されてローマ神話とのつながりは失われているけど、これは英語の曜日で残されている。Saturday, SundayがそれぞれSaturnus(農耕神), Sol(太陽の擬人化)そして西洋では星の名前もローマ神話の神々にちなんで名づけられている。上の神々の名前に対応した星は日本語でそれぞれ月、火星、水星、木星、金星、土星、太陽。

 

つまり日本語とスぺ語の曜日は完全に対応している。ここまではスぺ語を習い始めた時に気づいた訳やけど、一つ疑問が残る:日本語とスぺ語すら対応してるのに何故英語だけ全然違う名前付いてるんだ??

 

しかしThursday=Thor's dayでパズルが解けた。Thor=雷神≒Jupiter!ローマ神話の神をゲルマン神話の神を対応させて曜日にしてるんや!残念ながらゲルマン神話に関してそこまで詳しくないので残りの対応はググらんとわからなかったけど、日本語の曜日⇒星⇒ローマ神⇒ゲルマン神⇒英語の曜日のリンクが無事見つかった。

 

正直ここまですっきりしたのは久しぶり。語源って本当におもしろい。そしてオランダ語の勉強をすることでこのパズルが解けたのも本当に面白い。外国語を学ぶということは母国語についても学ぶことになるというのはまさにこのことなんやな。

 

 

 

 

日本語 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日 日曜日
対応する星 火星 水星 木星 金星 土星 太陽
それぞれ英語名 Moon Mars Mercury Jupiter Venus Saturn Sun
対応するローマ神 Luna Mars Mercurius Iuppiter Venus Saturnus Sol
対応するゲルマン神 (Moon) Tyr Odin(Woden) Thor Frigg Φ (Sun)
英語 Monday Tuesday Wednesday Thursday Friday Saturday Sunday

6/27

とあるヴルカヌス生の先輩のブログを読んでて、その文章が綺麗すぎて感動した。思えば最近は学術系しか本を読んでいない。綺麗な(修辞的な?)文章に触れていなさすぎてそれが存在すること自体を忘れていたような気がする。

 

こんな駄文を曝け出すのは少し恥ずかしいけど、それも含めてのブログということで続けます。

 

ところでオランダ語引き続き学習中なんやけど、意外と難しい。何が難しいって文構造が英語と違うから喋るにも聞くにも一旦脳内で並べ替えないといけない。例えば

I see the dog.

I see the dog.  (Emphasis on the dog)

Because I see the dog, ...

オランダ語でそれぞれ

Ik zie de hond.              (SVO)

De hond zie ik.              (OVS)

Omdat ik de hond zie,   (SOV)

過去形や助動詞が入る時も

I can see the dog.

I saw the dog.

と英語ではSVOを保つのに対して

Ik kan de hond zien.

Ik heb de hond gezien.

オランダ語では対応する言葉がどこか咄嗟には分からない。たった3文字からなる文ですらこれなのに、that節と副詞節と前置詞句を複数かました文なんてどこが何にかかってるのかさっぱりわからない。

 

ちなみにスペイン語だと

Yo veo el perro.

Porque yo veo el perro,

Yo puedo ver el perro.

Yo vi el perro.

まぁ、スペ語は目的格の代名詞がめんどいことになってるけど使わなければいいだけやし…。

 

考えてみると英語はSVOの狂信的信者なのかもしれない。スペ語もイタ語もドイ語もオランダ語も質問形で文を作るときはSVをひっくり返すのに対して、英語では形式述語doを入れることで名目上ひっくり返してはいるけど、実質的にはメインの動詞のSV順序は変わってない。

I eat.

Do I eat?

今まで疑問文では何故doを入れないといけないのか不思議だったが、SVOの形を保ちたかったのか。