###### tags: `Programming`
# Python Programming Basics
# 概要
## この記事について
これはプログラミングの経験が無い初心者がpythonで簡単な業務スクリプトを作成できるようにするためのトレーニングコンテンツです。pythonの基礎的な制御構文を押さえたあとアルゴリズムの言語化とプログラムコードへの落とし込み方を学びます。
このコースは限られた時間で基礎的な実技を学ぶことを目的としており、厳密な定義や深い内容には触れません。興味がある方は各位で調査してください。
## 参加準備
ハンズオン型のトレーニングのため手元にpythonの実行環境と事前配布されたscript.zipを用意してください。
- pythohn 最新版
https://www.python.org/downloads/
# 前提知識
## プログラムの基本
### 変数
```
a = 2
print(a)
```
`2` が表示されます。
条件に応じた様々な挙動をさせるために変数を利用します。変数は名前の通り変化させることが出来る値です。上の例ではありがたみが感じられませんが、この後出てくる while の説明で変数のありがたみが分かるようになります。
### 配列変数
```
a = [1, 2, 3, 4]
b = ["apple", "orange", "banana"]
print(a[0])
print(b[1])
```
`1` と `orange` が表示されます。
複数の変数をグループ化したものが配列変数です。上の例では4つの数字をグループ化した a という配列と、3つの文字列をグループ化した b という配列を使っています。配列の内容を利用する際は`[1]`のように配列の何番目の内容を使うか指定する必要があります。なおpythonでは配列の番号は0番から始まります。
### 変数同士の演算
```
message = "Hello " + "World"
print(message)
a = 2
b = 3
number = a + b
print(number)
```
`Hello World` と `5` が表示されます。
pythonでは + 演算子を使って2つの変数を加算することが出来ます。変数に数値が格納されている場合は算術加算になり、文字列が格納されている場合は文字列の結合になります。加算された結果は別の変数へ代入することが可能です。
### 制御構文
- if文
```
a = 2
b = 3
if a + b > 4:
print("4より大きい")
else:
print("4以下")
```
`4より大きい` が表示されます。
if文は条件によって処理の内容を分岐させるための構文です。上記の例では a に 2 が代入されており、b に 3 が代入されているので a + b の結果は 5 です。したがって4より大きいが表示されています。もし a + b の結果が 4 以下の場合は else 以下に記述された命令が実行されます。
pythonでは他のプログラミング言語のような `{ }` ではなくインデントによってブロックを区切ります。インデントに意味がある言語であることを覚えておいてください。
$$
f(x) =
\begin{cases}
4より大きい & (a+b > 4) \\
4以下 & (a+b \leq 4)
\end{cases}
$$
- while文
```
a = 0
while a < 4:
print(a)
a = a + 1
```
`0`から`3` までの数字が1行ずつ表示されます。
while文はある条件を満たしている限り同じ処理を繰り返すための構文です。上記の例では a < 4 が継続条件になっています。a は最初 0 から始まり、繰り返し処理の中で 1 が加算されているので、4回繰り返されたら継続条件が満たせなくなり繰り返しが終了します。
この例文で変数のありがたみが分かります。もし変数を使わなかったらprint命令を4行書かなければなりません。0 から 1000 まで表示するなら 1001 行です。
ここでもインデントが重要です。もし a = a + 1 の行にインデントが無ければ while文の中で a が加算されないため、継続条件 a < 4 が破れず無限ループに陥ってしまいます。
- for文
```
a = [1, 2, 3, 4]
for i in a:
print(i)
```
for文もwhile文とよく似た繰り返しの構文です。pythonでは複数の値を一つずつ取り出す際にfor文がよく使われます。上記の例では a から値を 1 つずつ取り出して i に保存し、取り出せる値が無くなったら繰り返しを終了するようプログラミングされています。
for文は何回繰り返せばよいか事前に分からない場合に威力を発揮します。例えば処理すべきファイルが無数に存在しており、しかも自動的にファイルの数が増減する場合を想像してください。ファイル名一覧を配列にしてからfor文を使って読み込めば全てのファイルを処理することが出来ます。
## その他Python構文
### ファイル読み書き
```
filename = "c:\\work\\test.txt"
with open(filename, mode="w") as f:
f.write("Hello World")
with open(filename) as f:
message = f.read()
print(message)
```
`with open as` 構文を使ってファイルシステムにアクセスにアクセスできます。上記の例では `c:\\work\\test.txt` というファイルをWriteモードで開いて変数 `f` に格納しています。その後 with ブロックの中で `write` 命令を使って `Hello World` という文字列を書き込んでいます。
この `f` の中にはファイルシステムを概念的に表したもの(ファイルオブジェクトと呼びます)が代入されていると考えてください。ファイルオブジェクトには読み書きや削除などの機能が用意されています。用意された機能を使うには変数に `.` を付けて命令を呼び出す必要があります。
pythonにはファイルオブジェクト以外にも様々な種類のオブジェクトがあり、オブジェクトによって使える命令は異なります。実際のプログラミングではリファレンスを参照しながら必要なオブジェクトや命令を調べてください。
### ディレクトリ作成
```
import os
dirname = "c:\\work"
os.makedirs(dirname, exist_ok=True)
```
osオブジェクトにはディレクトリを作成する命令が備わっています。makedirsを使えばCドライブ直下にworkという名前のディレクトリを作ることが出来ます。`exist_ok=True`という記述は同名のフォルダがあってもエラーにさせないためのオプションです。
なおosオブジェクトを使うためにはプログラムの冒頭で import 文を使って使用を宣言しなければなりません。import が必要なオブジェクトと必要ないオブジェクトがあるので、エラーメッセージを読みながら覚えていってください。
### 実行されたファイルの場所を調べる
```
import os
dirname = os.path.dirname(__file__)
print(dirname)
```
pythonスクリプトファイルが存在するディレクトリ名が表示されます。
`__file__`はpythonによって定義された特殊な変数で、実行されたスクリプトファイルの絶対パスが格納されています。これを os オブジェクトの path.dirname 命令に渡すことで、スクリプトファイルが存在するディレクトリのパスを取得できます。
スクリプトファイルと同じディレクトリにある別のファイルを読み込む場合にこの手法を用いることがあります。
### ファイル一覧取得
```
from pathlib import Path
dirname = "c:\\work"
p = Path(dirname)
filelist = p.glob("*.csv")
for filename in filelist
print(filename)
```
c:\work 配下にあるcsvファイルの一覧が表示されます。
pathlibパッケージに含まれるPathオブジェクトを使えばファイル検索のような高度なディレクトリ操作が可能です。上記の例では c:\work ディレクトリを表す Path オブジェクトを p に代入し、Pathオブジェクトが持つ glob 命令を使って c:\work 配下にあるファイル名が .csv で終わるファイルの一覧を取得しています。取得した一覧をfor文で1つずつ取り出して画面に表示します。
なおパッケージというのは複数のオブジェクトをグループ化したものです。パッケージに含まれるオブジェクトを使うには from import 文を使って宣言する必要があります。どのオブジェクトがパッケージ化されているのかは使いながら覚えていってください。
### 配列の長さを調べる
```
a = [1, 2, 3, 4]
nagasa = len(a)
print(nagasa)
```
`4` が表示されます。
lenは与えられた配列変数の中に要素が何個入っているか調べることが出来ます。
lenはオブジェクトを必要としない命令で 組み込み命令 と呼ばれます。これまでの例で何度も出てきた print も組み込み命令の一つです。
### 配列に要素を追加する
```
a = [1, 2, 3, 4]
nagasa = len(a)
print(nagasa)
a.append(5)
nagasa = len(a)
print(nagasa)
```
`4` と `5` が表示されます。
配列に値を代入するのではなく追加したい場合には配列オブジェクトが持つ append 命令を使います。
### zipファイルを解凍する
```
from zipfile import ZipFile
filename = "c:\\work\\file.zip"
dirname = "c:\\work"
with ZipFile(filename) as z:
z.extractall(dirname)
```
c:\work ディレクトリに file.zip という圧縮ファイルを置いてから上記プログラムを実行すると、c:\work ディレクトリに圧縮ファイルの中身が展開されます。
プログラムコードの構造は前述した File オブジェクトと似ています。with構文を使って c:\work\file.zip ファイルをZIPファイルオブジェクトとして z に代入し、extractall 命令を使ってZIPファイルに含まれる全てのファイルを c:\work 配下に展開しています。
# プログラミングの手順
プログラミングの大まかな手順は以下の通りです。
1. 目的を決める
1. 目的の達成方法を決める
1. 詳細な手順を決める
1. 手順通りに実施する
文脈によって定義には幅がありますが、今回は1~2が基本設計で3が詳細設計、4が実際にプログラムコードを書く作業として進めます。
プログラミングをする際にPCに向かってキーボードを打つ作業はそれほど重要ではありません。プログラミングの品質はコーディング前の作業によって左右されます。設計なんてやったことが無いという人も頭の中で考えながらプログラムコードを書いています。そのとき手を動かす時間と考える時間を天秤にかければ考える時間の方が多いはずです。
コーダーが不要と言いたいわけでは無く、優れたコーダーは計算量やモジュール間の構造などを考慮しながらプログラミングをしているという事です。
## 基本設計
基本設計ではプログラムを作成する目的とそのプログラムに持たせる機能を決めます。目的を決める際は抱えている課題からブレークダウンしていくのが良いと思います。あまりに壮大な(漠然としたと言い換えても良いでしょう)目的を設定してしまうと完成にたどり着けません。ここで適切な目的を設定するには対象業務の知識が必要になります。
事前配布した `extract.py` の目的は「旧システムから新システムへの移行に際して劣化した機能が無いか調べたい」です。これでは漠然としているので「劣化した機能を調べる」を「旧システムと新システムに設定された分析レポート毎の出力件数を調べる」に置き換えて、これをとりあえずの目的とします。出力件数が機能の良し悪しに影響するというのは対象業務の知識です。
目的が決まったら実現方法を検討します。目的は必ずしもプログラミングだけで実現できるわけではありません。例えば今回の題材である「旧システムと新システムの分析レポート」はそれぞれのシステムの出力機能を使ってzipファイルとして入手しました。事前配布したzipファイルに含まれる new ディレクトリと old ディレクトリの中にはそれぞれ同名のzipファイルが格納されています。一つ一つを解凍して中身のcsvファイルを確認してみてください。このcsvファイル一つ一つが分析レポートです。全部で300個以上のファイルが存在しますが、片方には存在するファイルがもう片方には存在しなかったりします。これは新システムが既に運用されており旧システムに無い分析ルールが追加されているためです。
以上を踏まえると、先ほど設定した目的は「二つの異なるディレクトリに存在するzipファイルを解凍し、その中に含まれる同名のcsvファイルの内容を比較し、旧レポートに対して新レポートの方が出力件数が劣っているものを明らかにすること」と言い換えられそうです。この目的を実現するために必要な機能を書き出してみましょう。
- zipファイルを解凍する機能
- 解凍したファイル一覧を取得する機能
- ファイル一覧で指定されたファイルを順番に読み込む機能
- ファイルの行数を調べる機能
- レポート名ごとに新旧のファイル行数を出力する機能
もれなく機能を書き出せたと思うなら基本設計は終了です。
## 詳細設計
基本設計で検討した機能を実現するための詳細な手順を検討します。ここでは実際のプログラミングに近いレベルで手順を検討しています。[python標準ライブラリ](https://docs.python.org/ja/3/library/index.html) などを調べながら具体的な手順を検討しましょう。
- zipファイルを解凍する機能
- csvが読めるところまで解凍したい
- 2重圧縮されているのでzip解凍が2回必要
- 解凍されたファイルが混ざらないように新旧を区別したい
- extractディレクトリ配下にoldとnewの異なるディレクトリを作る
- newディレクトリ配下にあるzipファイルは/extract/newディレクトリ以下に展開する
- oldディレクトリ配下にあるzipファイルは/extract/oldディレクトリ以下に展開する
- どうやって解凍先を/extract/newと/extract/oldに振り分ける?
- 解凍対象の存在するディレクトリ名を切り出して出力先に当てはめれば良い
- 解凍したファイル一覧を取得する機能
- ファイルを解凍してみるとディレクトリ構造は `./RSA-RES/**/**.csv` となっている
- glob命令で `./RSA-RES/**` のファイル一覧を取る
- ファイル一覧で指定されたファイルを順番に読み込む機能
- globの結果をfor文でループさせれば良い
- oldとnewでそれぞれファイルを読み込む必要がある
- ディレクトリパスがほぼ同じだからパスの一部を結合して変更すれば良い
- ファイルの行数を調べる機能
- ファイルの中身を1行単位で配列に読み込む
- 配列の要素数を数える
- newのみに存在するファイルは運用によって作られた物だから比較する必要無し
- oldを先に読み込んでnewに同名ファイルが存在する時だけ比較すれば良い
- レポート名ごとに新旧のファイル行数を出力する機能
- 1行毎にレポート名、旧ファイル行数、新ファイル行数を表示する
- 1行を表す配列を親の配列にどんどん追加して2次元配列を作る
- 2次元配列をcsvとしてファイルに書き出す
安心してください。いきなりこの手順が全て思い浮かんだわけではありません。まずはzipファイルを解凍する手順を実際にコーディングしながら検討し、徐々に手順を進めていったらこうなったのです。小さい規模のプログラムなら、机上の検討だけでなく実際にコーディングしながら進めたほうが考えがまとまる場合もあります。
# コーディング
実際に手を動かしてzipファイルを解凍する機能を実装してみましょう。この記事の最初の方で必要な構文は明示してありますので、それを見ながら試行錯誤してみてください。詳細設計で書き出した手順を全て実現することが難しければ単純に一つのzipファイルを解凍するだけでも構いません。自分でロジックを考えてプログラムを書き上げたという事実が重要です。
# サンプルプログラム
この手順をプログラムに書き下したものが下記のコードです。事前配布したファイルにはこのスクリプトと一緒に処理対象のzipファイルも含まれています。実際にスクリプトを実行しながら、どのコードが何をやっているのかを追いかけてみましょう。
:::spoiler サンプルコードを開く
{%gist ox0xo/7e790afce32c55135c301448141ac684 %}
:::
# まとめ
あなたはpythonの基礎的な構文に触れ、実際に手を動かしてプログラムを1から作成する経験を積みました。プログラミングにおいて言語の理解はさほど重要ではなく、問題設定と手順の検討が重要であることを感じ取ってもらえたなら幸いです。
プログラムにおける動作手順のことをアルゴリズムと呼びます。良いアルゴリズムを実装するコツは普段から様々なアルゴリズムに慣れ親しむことです。手を動かせばそれだけ強くなれる世界なので、普段使っているツールのソースコードを読んだり[AtCoder](https://atcoder.jp/?lang=ja)のようなプログラミングコンテストに参加して楽しんでください。