Python言語による実践プログラミング

この章では、Python言語を使用した実践的なプログラミングを取り上げます。

Pythonはスクリプト言語であり、コンパイル言語のCと違いPCで作成したプログラムを Armadillo用にクロスコンパイルする必要がないので、効率よく開発ができる可能性があります。

さらに、非常に多くのパッケージ(ライブラリ)が提供されており、 かつそれらを簡単に導入できます。

現在では、ディープラーニングなどにも利用されており、一般的なアプリケーションから 研究開発用アプリケーションまで幅広く利用されている言語です。

クラウドサービスを利用したアプリケーションを開発する場合などでも、 各クラウドサービスともにPython向けのSDKも提供していることが多いです。

実行速度が求められるアプリケーションやデバイスドライバの開発をするのでなければ、 開発言語としてPythonは選択肢の一つになります。

ここではPythonの文法に関する詳細な解説はしません。 詳細な解説は公式サイト[41] [42]を参照してください。

7.1. Python実行環境をインストールする

Pythonの実行環境はaptで簡単にインストールできます。 Pythonにはバージョン2系と3系があり、現在ではバージョン3系が主流と なっているので、ここでもバージョン3系をインストールします。

インストールはaptでできます。

[armadillo ~]# apt install python3

図7.1 pythonバージョン3のインストール


インストール後に動作を確認できます。

[armadillo ~]# python3
Python 3.5.3 (default, Sep 27 2018, 17:25:39)
[GCC 6.3.0 20170516] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("hello, world.")
hello, world.
>>> a = 10
>>> b = a * 2
>>> print(b)
20
>>> quit()
[armadillo ~]#

図7.2 pythonの動作確認


単にpython3として実行すると対話モードと呼ばれるモードでpythonが起動します。 このモードでは入力されたコードが随時実行されていきます。 対話モードを終了する場合はquit()関数を呼び出します。

次に、Pythonのパッケージ管理ツールであるpipをインストールします。 pipを使うことで必要なパッケージ(ライブラリ)を手軽にインストールできるようになります。

[armadillo ~]# apt install python3-pip

図7.3 pipのインストール


7.2. ファイルの取り扱い

まずは、「ファイルの取り扱い」と同様に ファイルの取扱について取り上げます。

7.2.1. テキストファイルを扱う

ここでは、「テキストファイルを扱う」と同じ仕様のサンプルプログラムを紹介します。 PythonではCSVを扱うパッケージが標準で用意されているのでこれを使います。

import csv
import sys
import os
import copy

# 表示する文字数
DISP_WIDTH = 60  # 幅
DISP_HEIGHT = 17 # 高さ

# CSVデータ1行の要素数
COLUMN_NUM = 11

line = 0   # 一画面に表示した行数
count = 0  # 表示したデータ総数


def printline(csvline):

    """ 行表示関数

    Args:
        csvline(stringの配列): 表示する1行分のCSVデータ

    """

    global line
    global count

    # CSVデータの各要素を表示
    if csvline is not None:
        # データがない場合表示しない
        if len(csvline) < 1:
            return

        #各要素を表示フォーマットに展開
        buf = "%6d %s %s %s %s %s %s %s %s %s %s %s" % \
                (count, csvline[0], csvline[1], csvline[2], csvline[3], csvline[4], \
                 csvline[5], csvline[6], csvline[7], csvline[8], csvline[9], csvline[10])
        count += 1
    else:
        # CSVデータの総数を表示
        # データ総数を表示フォーマットに展開
        buf = "Count: %6d" % (count)

    # 今回追加される表示行数を計算
    newline = int((len(buf) + DISP_WIDTH - 1) / DISP_WIDTH)
    # 1画面を超える場合、入力があるまで一時停止
    if line + newline >= DISP_HEIGHT:
        input()
        # 表示行数を初期化
        line = 0
    # 実際に表示する
    print(buf)
    # 表示行数を更新
    line += newline

if __name__ == "__main__":
    # 引数が指定されなかった場合、usage表示して終了
    if len(sys.argv) < 2:
        print("Usage: %s <csvfile>" % sys.argv[0])
        sys.exit(os.EX_OK)

    # CSVファイルオープン
    with open(sys.argv[1]) as csvfile:
        pzip = "00000000"
        csvline = [] # CSVデータ初期化
        # CSVファイルから1行読み込む
        csvreader = csv.reader(csvfile)
        for row in csvreader:
            # 要素数が不足している場合、次行にスキップ
            if len(row) < COLUMN_NUM:
                continue

            # 新しいデータの場合
            if pzip != row[1]:
                printline(csvline)
                csvline = copy.copy(row)
            else:
                # 既存データへの追加の場合
                # 町域名の続きを追加
                csvline[2] += row[2]
            pzip = row[1]

        # 保持済みのデータを表示
        printline(csvline)
        # データ総数を表示
        printline(None)

図7.4 CSVファイルの内容を表示するプログラム(dispcsv1.py)


一見してC言語のプログラムより短くなっていることがわかります。 特にCSVファイルを読み込んでカンマ区切りでトークンに分割する処理については、 Pythonでは2行で収まっています。

import csv
(中略)
        # CSVファイルから1行読み込む
        csvreader = csv.reader(csvfile)
        for row in csvreader:

変数rowにはすでに分割されたトークンが配列として代入されているので、 後の処理は内容を整形して表示するだけです。

このようにPythonでは適切なパッケージをimportすることによって、 煩雑な処理を簡潔に書くことができ、バグを作り込む可能性も低減できます。

実際に動作させるとC言語版と同じ結果が表示されます。

[armadillo ~]# python3 ./dispcsv1.py ./dispcsv1 ken_all_rome.csv
     0 01101 0600000 IKANIKEISAIGANAIBAAI CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 0 0 0 0
     1 01101 0640941 ASAHIGAOKA CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 1 0 0 0
     2 01101 0600041 ODORIHIGASHI CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 1 0 0 0
     3 01101 0600042 ODORINISHI(1-19-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0
     4 01101 0640820 ODORINISHI(20-28-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0
     5 01101 0600031 KITA1-JOHIGASHI CHUO-KU SAPPORO-SHI HOKKAIDO 0 0 1 0 0 0
     6 01101 0600001 KITA1-JONISHI(1-19-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0
     7 01101 0640821 KITA1-JONISHI(20-28-CHOME) CHUO-KU SAPPORO-SHI HOKKAIDO 1 0 1 0 0 0

図7.5 dispcsv1.pyの実行結果


リターンを入力すると次に進みます。途中で終了したいときは、Ctrl+Cを入力してください。

7.2.2. 設定ファイルに対応する

ここでも、「設定ファイルに対応する」と同じ仕様のサンプルプログラムを紹介します。 Pythonには設定ファイルを取り扱うconfigparserパッケージも標準で用意されているのでこれを使います。

import csv
import configparser
import sys
import os
import copy
import json

# 表示する文字数
DISP_WIDTH = 60  # 幅
DISP_HEIGHT = 17 # 高さ

# CSVデータ1行の要素数
COLUMN_NUM = 11

line = 0   # 一画面に表示した行数
count = 0  # 表示したデータ総数

conf = configparser.ConfigParser() # 設定ファイル管理オブジェクト

def printline(csvline):

    """ 行表示関数

    Args:
        csvline(stringの配列): 表示する1行分のCSVデータ

    """

    global line
    global count

    # CSVデータの各要素を表示
    if csvline is not None:
        # データがない場合表示しない
        if len(csvline) < 1:
            return

        # 全国地方公共団体コード条件設定があり、一致しなかったら表示しない
        if int(conf['data']['Code']) >= 0 and csvline[0] != conf['data']['Code']:
            return
        # 郵便番号条件設定があり、部分一致しなかったら表示しない
        if conf['data']['Zipcode'] != "" and conf['data']['Zipcode'] not in csvline[1]:
            return
        # 町域名条件設定があり、部分一致しなかったら表示しない
        if conf['data']['Street'] != "" and conf['data']['Street'].lower() not in csvline[2].lower():
            return
        # 市区町村名条件設定があり、部分一致しなかったら表示しない
        if conf['data']['City'] != "" and conf['data']['City'].lower() not in csvline[3].lower():
            return
        # 都道府県条件設定があり、部分一致しなかったら表示しない */
        if conf['data']['Pref'] != "" and conf['data']['Pref'].lower() not in csvline[4].lower():
            return

        for idx, flag in enumerate(json.loads(conf['data']['Flag'])):
            if int(flag) >= 0 and flag != csvline[5 + idx]:
                return

        #各要素を表示フォーマットに展開
        buf = "%6d %s %s %s %s %s %s %s %s %s %s %s" % \
                (count, csvline[0], csvline[1], csvline[2], csvline[3], csvline[4], \
                 csvline[5], csvline[6], csvline[7], csvline[8], csvline[9], csvline[10])
        count += 1
    else:
        # CSVデータの総数を表示
        # 総数表示無効なら表示しない
        if not conf['control']['Count']:
            return
        # データ総数を表示フォーマットに展開
        buf = "Count: %6d" % (count)

    # 今回追加される表示行数を計算
    disp_width = int(conf['display']['Width'])
    disp_height = int(conf['display']['Height'])
    newline = int((len(buf) + disp_width - 1) / disp_width)
    # 1画面を超える場合、入力があるまで一時停止
    if line + newline >= disp_height:
        # 一時停止有効なら
        if conf['control']['Pause']:
            input()
        # 表示行数を初期化
        line = 0
    # 実際に表示する
    print(buf)
    # 表示行数を更新
    line += newline


def readconf(conf_file_name):

    """ confファイル読み込み関数

    Args:
        conf_file_name(string): confファイルのファイル名

    """

    global conf
    if not os.path.exists("./" + conf_file_name):
        # ファイルが存在していない場合は新規作成
        # デフォルト設定
        conf['display'] = {
            'Width': 60,
            'Height': 17
        }
        conf['control'] = {
            'Pause': True,
            'Count': True
        }
        conf['data'] = {
            'Code': -1,
            'Zipcode': "",
            'Street': "",
            'City': "",
            'Pref': "",
            'Flag': [-1, -1, -1, -1, -1, -1]
        }
        with open(conf_file_name, 'w') as conf_file:
            conf.write(conf_file)
    else :
        conf.read(conf_file_name)

if __name__ == "__main__":
    # 引数が指定されなかった場合、usage表示して終了
    if len(sys.argv) < 2:
        print("Usage: %s <csvfile>" % sys.argv[0])
        sys.exit(os.EX_OK)

    # confファイルを読み込み
    readconf(os.path.splitext(sys.argv[0])[0] + ".conf")

    # CSVファイルオープン
    with open(sys.argv[1]) as csvfile:
        csvreader = csv.reader(csvfile)
        pzip = "00000000"
        csvline = [] # CSVデータ初期化
        # CSVファイルから1行読み込む
        for row in csvreader:
            # 要素数が不足している場合、次行にスキップ
            if len(row) < COLUMN_NUM:
                continue

            # 新しいデータの場合
            if pzip != row[1]:
                printline(csvline)
                csvline = copy.copy(row)
            else:
                # 既存データへの追加の場合
                # 町域名の続きを追加
                csvline[2] += row[2]
            pzip = row[1]

        # 保持済みのデータを表示
        printline(csvline)
        # データ総数を表示
        printline(None)

図7.6 CSVファイルの内容を表示するプログラムのconfファイル対応版 (dispcsv2.py)


設定ファイルの作成・読み込みを行うreadconf関数を追加しています。この関数の中で configparserパッケージを使って処理を行っています。

設定ファイルの新規作成時のデフォルト値が連想配列のように書くことができたり、 読み込みは1行で済んだりと、こちらもC言語版と比べて簡潔に書くことができています。

注意点として、読み込んだ設定値はすべて文字列として保持されるため、数値として扱う場合や 配列として扱う場合は、適宜変換する必要があります。

初回実行した後に、dispcsv2.confファイルができています。

[display]
width = 60
height = 17

[control]
pause = True
count = True

[data]
code = -1
zipcode =
street =
city =
pref =
flag = [-1, -1, -1, -1, -1, -1]

C言語版と同様に、以下のように動作を変更させてみます。

  1. 画面サイズは80x24
  2. 一時停止しない
  3. 町域名にKOKUBUNJIを含んだもののみ出力する
[display]
width = 80
height = 24

[control]
pause = False
count = True

[data]
code = -1
zipcode =
street = kokubunji
city =
pref =
flag = [-1, -1, -1, -1, -1, -1]

テキストエディタでこのようにdispcsv2.confを 変更して実行すると、以下のように動作します。

[armadillo ~]# python3 ./dispcsv2.py ./ken_all_rome.csv
     0 09216 3290417 KOKUBUNJI SHIMOTSUKE-SHI TOCHIGI 0 0 0 0 0 0
     1 12219 2900071 KITAKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     2 12219 2900073 KOKUBUNJIDAICHUO ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     3 12219 2900072 NISHIKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     4 12219 2900074 HIGASHIKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     5 12219 2900075 MINAMIKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     6 14215 2430413 KOKUBUNJIDAI EBINA-SHI KANAGAWA 0 0 1 0 0 0
     7 15222 9420088 BISHAMONKOKUBUNJI JOETSU-SHI NIIGATA 0 0 0 0 0 0
     8 15224 9520304 KOKUBUNJI SADO-SHI NIIGATA 0 0 0 0 0 0
     9 27127 5310064 KOKUBUNJI KITA-KU OSAKA-SHI OSAKA 0 0 1 0 0 0
    10 28201 6710234 MIKUNINOCHO KOKUBUNJI HIMEJI-SHI HYOGO 0 0 0 0 0 0
    11 28209 6695341 HIDAKACHO KOKUBUNJI TOYOKA-SHI HYOGO 0 0 0 0 0 0
    12 31201 6800155 KOKUFUCHO KOKUBUNJI TOTTORI-SHI TOTTORI 0 0 0 0 0 0
    13 31203 6820943 KOKUBUNJI KURAYOSHI-SHI TOTTORI 0 0 0 0 0 0
    14 33203 7080843 KOKUBUNJI TSUYAMA-SHI OKAYAMA 0 0 0 0 0 0
    15 35206 7470021 KOKUBUNJICHO HOFU-SHI YAMAGUCHI 0 0 0 0 0 0
    16 37201 7690105 KOKUBUNJICHO KASHIHARA TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    17 37201 7690102 KOKUBUNJICHO KOKUBU TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    18 37201 7690104 KOKUBUNJICHO SHIMMYO TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    19 37201 7690101 KOKUBUNJICHO NII TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    20 37201 7690103 KOKUBUNJICHO FUKE TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    21 46215 8950073 KOKUBUNJICHO SATSUMASENDAI-SHI KAGOSHIMA 0 0 0 0 0 0
Count:     22

図7.7 dispcsv2.confを編集したdispcsv2.pyの実行結果


7.2.3. JSONファイルを扱う

PythonでのJSONファイルを扱いについて説明します。現在ほとんどの クラウドサービスでは、通信のレスポンスとしてJSON形式を採用しており、 クラウドサービスと連携したアプリケーションを開発するには、JSONファイルを 扱うことがほぼ必須となっています。

当然、C言語でもJSONファイルを扱うプログラムを書くことはできますが、C言語では 書きづらい文字列処理を多く実装しなくてはならないため、簡潔に書くのは難しくなります。

ここでは、「設定ファイルに対応する」で取り上げた設定ファイルを、JSON形式として 保存・読み込みを行うサンプルプログラムを示します。

import csv
import sys
import os
import copy
import json

# 表示する文字数
DISP_WIDTH = 60  # 幅
DISP_HEIGHT = 17 # 高さ

# CSVデータ1行の要素数
COLUMN_NUM = 11

line = 0   # 一画面に表示した行数
count = 0  # 表示したデータ総数

conf = {} # 設定ファイル管理オブジェクト

def printline(csvline):

    """ 行表示関数

    Args:
        csvline(stringの配列): 表示する1行分のCSVデータ

    """

    global line
    global count

    # CSVデータの各要素を表示
    if csvline is not None:
        # データがない場合表示しない
        if len(csvline) < 1:
            return

        # 全国地方公共団体コード条件設定があり、一致しなかったら表示しない
        if int(conf['data']['Code']) >= 0 and csvline[0] != conf['data']['Code']:
            return
        # 郵便番号条件設定があり、部分一致しなかったら表示しない
        if conf['data']['Zipcode'] != "" and conf['data']['Zipcode'] not in csvline[1]:
            return
        # 町域名条件設定があり、部分一致しなかったら表示しない
        if conf['data']['Street'] != "" and conf['data']['Street'].lower() not in csvline[2].lower():
            return
        # 市区町村名条件設定があり、部分一致しなかったら表示しない
        if conf['data']['City'] != "" and conf['data']['City'].lower() not in csvline[3].lower():
            return
        # 都道府県条件設定があり、部分一致しなかったら表示しない */
        if conf['data']['Pref'] != "" and conf['data']['Pref'].lower() not in csvline[4].lower():
            return

        for idx, flag in enumerate(conf['data']['Flag']):
            if flag >= 0 and flag != csvline[5 + idx]:
                return

        #各要素を表示フォーマットに展開
        buf = "%6d %s %s %s %s %s %s %s %s %s %s %s" % \
                (count, csvline[0], csvline[1], csvline[2], csvline[3], csvline[4], \
                 csvline[5], csvline[6], csvline[7], csvline[8], csvline[9], csvline[10])
        count += 1
    else:
        # CSVデータの総数を表示
        # 総数表示無効なら表示しない
        if not conf['control']['Count']:
            return
        # データ総数を表示フォーマットに展開
        buf = "Count: %6d" % (count)

    # 今回追加される表示行数を計算
    disp_width = conf['display']['Width']
    disp_height = conf['display']['Height']
    newline = int((len(buf) + disp_width - 1) / disp_width)
    # 1画面を超える場合、入力があるまで一時停止
    if line + newline >= disp_height:
        # 一時停止有効なら
        if conf['control']['Pause']:
            input()
        # 表示行数を初期化
        line = 0
    # 実際に表示する
    print(buf)
    # 表示行数を更新
    line += newline


def readconf(conf_file_name):

    """ confファイル読み込み関数

    Args:
        conf_file_name(string): confファイルのファイル名

    """

    global conf
    if not os.path.exists("./" + conf_file_name):
        # ファイルが存在していない場合は新規作成
        # デフォルト設定
        conf['display'] = {
            'Width': 60,
            'Height': 17
        }
        conf['control'] = {
            'Pause': True,
            'Count': True
        }
        conf['data'] = {
            'Code': -1,
            'Zipcode': "",
            'Street': "",
            'City': "",
            'Pref': "",
            'Flag': [-1, -1, -1, -1, -1, -1]
        }
        # JSON形式のデータとして書き込む
        with open(conf_file_name, 'w') as conf_file:
            json.dump(conf, conf_file, ensure_ascii=False, indent=4, sort_keys=False, separators=(",", ": "))
    else :
        # JSON形式のデータとして読み込む
        with open(conf_file_name, 'r') as conf_file:
            conf = json.load(conf_file)

if __name__ == "__main__":
    # 引数が指定されなかった場合、usage表示して終了
    if len(sys.argv) < 2:
        print("Usage: %s <csvfile>" % sys.argv[0])
        sys.exit(os.EX_OK)

    # confファイルを読み込み
    readconf(os.path.splitext(sys.argv[0])[0] + ".json")

    # CSVファイルオープン
    with open(sys.argv[1]) as csvfile:
        csvreader = csv.reader(csvfile)
        pzip = "00000000"
        csvline = [] # CSVデータ初期化
        # CSVファイルから1行読み込む
        for row in csvreader:
            # 要素数が不足している場合、次行にスキップ
            if len(row) < COLUMN_NUM:
                continue

            # 新しいデータの場合
            if pzip != row[1]:
                printline(csvline)
                csvline = copy.copy(row)
            else:
                # 既存データへの追加の場合
                # 町域名の続きを追加
                csvline[2] += row[2]
            pzip = row[1]

        # 保持済みのデータを表示
        printline(csvline)
        # データ総数を表示
        printline(None)

図7.8 CSVファイルの内容を表示するプログラムのJSON形式のconfファイル対応版 (dispcsv3.py)


内容としては、図7.6「CSVファイルの内容を表示するプログラムのconfファイル対応版 (dispcsv2.py)」のreadconf関数でconfigparserパッケージを使っている箇所を JSONパッケージを使うように置き換えただけです。加えて、configparserとは違い、設定ファイルを読み込んだ際に、 数値は数値として読み込まれるので、文字列を数値に変換する必要はありません。

初回実行した後に、dispcsv3.jsonファイルができています。

{
    "display": {
        "Width": 60,
        "Height": 17
    },
    "control": {
        "Pause": true,
        "Count": true
    },
    "data": {
        "Code": -1,
        "Zipcode": "",
        "Street": "",
        "City": "",
        "Pref": "",
        "Flag": [
            -1,
            -1,
            -1,
            -1,
            -1,
            -1
        ]
    }
}

ここでも、以下のように動作を変更させてみます。

  1. 画面サイズは80x24
  2. 一時停止しない
  3. 町域名にKOKUBUNJIを含んだもののみ出力する
{
    "display": {
        "Width": 80,
        "Height": 24
    },
    "control": {
        "Pause": false,
        "Count": true
    },
    "data": {
        "Code": -1,
        "Zipcode": "",
        "Street": "kokubunji",
        "City": "",
        "Pref": "",
        "Flag": [
            -1,
            -1,
            -1,
            -1,
            -1,
            -1
        ]
    }
}

テキストエディタでこのようにdispcsv3.jsonを 変更して実行すると、図7.7「dispcsv2.confを編集したdispcsv2.pyの実行結果」と同じ結果となります。

[armadillo ~]# python3 ./dispcsv3.py ./ken_all_rome.csv
     0 09216 3290417 KOKUBUNJI SHIMOTSUKE-SHI TOCHIGI 0 0 0 0 0 0
     1 12219 2900071 KITAKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     2 12219 2900073 KOKUBUNJIDAICHUO ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     3 12219 2900072 NISHIKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     4 12219 2900074 HIGASHIKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     5 12219 2900075 MINAMIKOKUBUNJIDAI ICHIHARA-SHI CHIBA 0 0 1 0 0 0
     6 14215 2430413 KOKUBUNJIDAI EBINA-SHI KANAGAWA 0 0 1 0 0 0
     7 15222 9420088 BISHAMONKOKUBUNJI JOETSU-SHI NIIGATA 0 0 0 0 0 0
     8 15224 9520304 KOKUBUNJI SADO-SHI NIIGATA 0 0 0 0 0 0
     9 27127 5310064 KOKUBUNJI KITA-KU OSAKA-SHI OSAKA 0 0 1 0 0 0
    10 28201 6710234 MIKUNINOCHO KOKUBUNJI HIMEJI-SHI HYOGO 0 0 0 0 0 0
    11 28209 6695341 HIDAKACHO KOKUBUNJI TOYOKA-SHI HYOGO 0 0 0 0 0 0
    12 31201 6800155 KOKUFUCHO KOKUBUNJI TOTTORI-SHI TOTTORI 0 0 0 0 0 0
    13 31203 6820943 KOKUBUNJI KURAYOSHI-SHI TOTTORI 0 0 0 0 0 0
    14 33203 7080843 KOKUBUNJI TSUYAMA-SHI OKAYAMA 0 0 0 0 0 0
    15 35206 7470021 KOKUBUNJICHO HOFU-SHI YAMAGUCHI 0 0 0 0 0 0
    16 37201 7690105 KOKUBUNJICHO KASHIHARA TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    17 37201 7690102 KOKUBUNJICHO KOKUBU TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    18 37201 7690104 KOKUBUNJICHO SHIMMYO TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    19 37201 7690101 KOKUBUNJICHO NII TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    20 37201 7690103 KOKUBUNJICHO FUKE TAKAMATSU-SHI KAGAWA 0 0 0 0 0 0
    21 46215 8950073 KOKUBUNJICHO SATSUMASENDAI-SHI KAGOSHIMA 0 0 0 0 0 0
Count:     22

図7.9 dispcsv3.jsonを編集したdispcsv3.pyの実行結果


7.2.4. XMLファイルを扱う

XMLファイルはJSONと同様に広く使われているファイルフォーマットです。

PythonではXMLを扱うパッケージも標準で用意されているので、 C言語で実装する場合と比較すると簡単に扱うことができます。

ここでも、「設定ファイルに対応する」で取り上げた設定ファイルを、XML形式として 保存・読み込みを行うサンプルプログラムを示します。

import csv
import sys
import os
import copy
from xml.etree import ElementTree # XML操作用パッケージのインポート
import xml.dom.minidom            # XML操作用パッケージのインポート

# 表示する文字数
DISP_WIDTH = 60  # 幅
DISP_HEIGHT = 17 # 高さ

# CSVデータ1行の要素数
COLUMN_NUM = 11

line = 0   # 一画面に表示した行数
count = 0  # 表示したデータ総数

conf = {} # 設定ファイル管理オブジェクト

def printline(csvline):

    """ 行表示関数

    Args:
        csvline(stringの配列): 表示する1行分のCSVデータ

    """

    global line
    global count

    # CSVデータの各要素を表示
    if csvline is not None:
        # データがない場合表示しない
        if len(csvline) < 1:
            return

        # 全国地方公共団体コード条件設定があり、一致しなかったら表示しない
        if int(conf['data']['Code']) >= 0 and csvline[0] != conf['data']['Code']:
            return
        # 郵便番号条件設定があり、部分一致しなかったら表示しない
        if conf['data']['Zipcode'] != None and conf['data']['Zipcode'] not in csvline[1]:
            return
        # 町域名条件設定があり、部分一致しなかったら表示しない
        if conf['data']['Street'] != None and conf['data']['Street'].lower() not in csvline[2].lower():
            return
        # 市区町村名条件設定があり、部分一致しなかったら表示しない
        if conf['data']['City'] != None and conf['data']['City'].lower() not in csvline[3].lower():
            return
        # 都道府県条件設定があり、部分一致しなかったら表示しない */
        if conf['data']['Pref'] != None and conf['data']['Pref'].lower() not in csvline[4].lower():
            return

        for idx, flag in enumerate(conf['data']['Flag']):
            if flag >= 0 and flag != csvline[5 + idx]:
                return

        #各要素を表示フォーマットに展開
        buf = "%6d %s %s %s %s %s %s %s %s %s %s %s" % \
                (count, csvline[0], csvline[1], csvline[2], csvline[3], csvline[4], \
                 csvline[5], csvline[6], csvline[7], csvline[8], csvline[9], csvline[10])
        count += 1
    else:
        # CSVデータの総数を表示
        # 総数表示無効なら表示しない
        if not conf['control']['Count']:
            return
        # データ総数を表示フォーマットに展開
        buf = "Count: %6d" % (count)

    # 今回追加される表示行数を計算
    disp_width = conf['display']['Width']
    disp_height = conf['display']['Height']
    newline = int((len(buf) + disp_width - 1) / disp_width)
    # 1画面を超える場合、入力があるまで一時停止
    if line + newline >= disp_height:
        # 一時停止有効なら
        if conf['control']['Pause']:
            input()
        # 表示行数を初期化
        line = 0
    # 実際に表示する
    print(buf)
    # 表示行数を更新
    line += newline


def readconf(conf_file_name):

    """ confファイル読み込み関数

    Args:
        conf_file_name(string): confファイルのファイル名

    """

    global conf
    if not os.path.exists("./" + conf_file_name):
        # ファイルが存在していない場合は新規作成
        # デフォルト設定
        conf['display'] = {
            'Width': 60,
            'Height': 17
        }
        conf['control'] = {
            'Pause': True,
            'Count': True
        }
        conf['data'] = {
            'Code': -1,
            'Zipcode': "",
            'Street': "",
            'City': "",
            'Pref': "",
            'Flag': [-1, -1, -1, -1, -1, -1]
        }

        # conf要素(ルート)の作成
        conf_elem = ElementTree.Element("conf")

        # display要素の作成とその下にある子要素の作成
        display_elem = ElementTree.SubElement(conf_elem, "display")
        width_elem = ElementTree.SubElement(display_elem, "width")
        width_elem.text = str(conf['display']['Width'])
        height_elem = ElementTree.SubElement(display_elem, "height")
        height_elem.text = str(conf['display']['Height'])

        # control要素の作成とその下にある子要素の作成
        control_elem = ElementTree.SubElement(conf_elem, "control")
        pause_elem = ElementTree.SubElement(control_elem, "pause")
        pause_elem.text = str(conf['control']['Pause'])
        count_elem = ElementTree.SubElement(control_elem, "count")
        count_elem.text = str(conf['control']['Count'])

        # data要素の作成とその下にある子要素の作成
        data_elem = ElementTree.SubElement(conf_elem, "data")
        code_elem = ElementTree.SubElement(data_elem, "code")
        code_elem.text = str(conf['data']['Code'])
        zipcode_elem = ElementTree.SubElement(data_elem, "zipcode")
        zipcode_elem.text = ""
        street_elem = ElementTree.SubElement(data_elem, "street")
        street_elem.text = ""
        city_elem = ElementTree.SubElement(data_elem, "city")
        city_elem.text = ""
        pref_elem = ElementTree.SubElement(data_elem, "pref")
        pref_elem.text = ""
        flag_elem = ElementTree.SubElement(data_elem, "flag")
        flag_elem.text = "-1,-1,-1,-1,-1,-1"

        # ファイルに書き出したときに見やすくするため整形する
        doc = xml.dom.minidom.parseString(ElementTree.tostring(conf_elem, "utf-8"))
        with open(conf_file_name, "w") as conf_file:
            doc.writexml(conf_file, newl="\n", indent="", addindent="    ")
    else :
        # XML形式のデータとして読み込む
        conf_elem = ElementTree.parse(conf_file_name)
        conf['display'] = {}
        conf['display']['Width'] = int(conf_elem.find(".//width").text)
        conf['display']['Height'] = int(conf_elem.find(".//height").text)
        conf['control'] = {}
        conf['control']['Pause'] = bool(conf_elem.find(".//pause").text)
        conf['control']['Count'] = bool(conf_elem.find(".//count").text)
        conf['data'] = {}
        conf['data']['Code'] = int(conf_elem.find(".//code").text)
        conf['data']['Zipcode'] = conf_elem.find(".//zipcode").text
        conf['data']['Street'] = conf_elem.find(".//street").text
        conf['data']['City'] = conf_elem.find(".//city").text
        conf['data']['Pref'] = conf_elem.find(".//pref").text

        flag = []
        for val in conf_elem.find(".//flag").text.split(","):
            flag.append(int(val))
        conf['data']['Flag'] = flag

if __name__ == "__main__":
    # 引数が指定されなかった場合、usage表示して終了
    if len(sys.argv) < 2:
        print("Usage: %s <csvfile>" % sys.argv[0])
        sys.exit(os.EX_OK)

    # confファイルを読み込み
    readconf(os.path.splitext(sys.argv[0])[0] + ".xml")

    # CSVファイルオープン
    with open(sys.argv[1]) as csvfile:
        csvreader = csv.reader(csvfile)
        pzip = "00000000"
        csvline = [] # CSVデータ初期化
        # CSVファイルから1行読み込む
        for row in csvreader:
            # 要素数が不足している場合、次行にスキップ
            if len(row) < COLUMN_NUM:
                continue

            # 新しいデータの場合
            if pzip != row[1]:
                printline(csvline)
                csvline = copy.copy(row)
            else:
                # 既存データへの追加の場合
                # 町域名の続きを追加
                csvline[2] += row[2]
            pzip = row[1]

        # 保持済みのデータを表示
        printline(csvline)
        # データ総数を表示
        printline(None)

図7.10 CSVファイルの内容を表示するプログラムのXML形式のconfファイル対応版 (dispcsv4.py)


実行方法や実行結果に関してはこれまでのサンプルプログラムと同じなのでここでは省略し、 初回実行時に生成されるXMLファイルのみ示します。

<?xml version="1.0" ?>
<conf>
    <display>
        <width>60</width>
        <height>17</height>
    </display>
    <control>
        <pause>True</pause>
        <count>True</count>
    </control>
    <data>
        <code>-1</code>
        <zipcode/>
        <street/>
        <city/>
        <pref/>
        <flag>-1,-1,-1,-1,-1,-1</flag>
    </data>
</conf>

XMLファイル内の設定値を変更してプログラムを実行すると、JSONファイル版の プログラムと同じように動作することを確認できます。

ここで取り上げたものだけではなく、Pythonには様々なファイル形式に対応した パッケージがあるので、何かファイルを処理したい場合はまずパッケージがないか 探してみると見つかる可能性があります。

7.3. ネットワークを使う

Pythonでもソケットを使ったネットワークプログラムを書くことができます。

TCP/IPプログラムの流れは「TCP/IP」を参照してください。 Pythonでもこの流れと同じです。

7.3.1. Python版TCP/IPでHello!

「TCP/IPでHello!」のPython版を作ってみます。

import socket
import sys
import os
import traceback

if __name__ == "__main__":
    # 引数が指定されなかった場合、usage表示して終了
    if len(sys.argv) < 2:
        print("Usage: %s <port>" % sys.argv[0])
        sys.exit(os.EX_OK)

    port = int(sys.argv[1])
    if port < 49152 or 65535 < port:
        print("Specify the port 49152-65535\n")
        sys.exit(os.EX_OK)

    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
        # アドレスとポートを割り当て
        server.bind(("", port))

        # クライアントからの接続を待つ
        server.listen(socket.SOMAXCONN)

        # クライアントからの接続を受け付けて、入出力用のソケットを作成
        client, client_addr = server.accept()

        try:
            # メッセージを送信
            client.sendall(b'Hello!\r\n')
        except:
            print(traceback.format_exc())

図7.11 ネットワークでHello!を返すサーバー Python版(network_hello_server.py)


ソケット作成から、メッセージの送信までの流れはC言語版と同じです。

実際に動作させてみます。

[armadillo ~]# python3 ./network_hello_server.py 65432

図7.12 network_hello_server.pyの実行結果


PCからtelnetで接続してみます。

[ATDE ~]$ telnet 192.168.1.100 65432
Trying 192.168.1.100...
Connected to 192.168.1.100.
Escape character is '^]'.
Hello!
Connection closed by foreign host.

図7.13 network_hello_serverへのtelnet


このようにHello!と表示されることが確認できました。

7.3.2. HTTP通信を行う

Pythonで通信系アプリケーションを開発しようとする場合は、Socketのような 低位層のインターフェースを使ったものより、HTTPのような 上位層の通信を要求されることのほうが多いかもしれません。

ここではPythonにおけるHTTP通信の例を紹介します。

7.3.2.1. HTTPサーバーを準備する

ATDE7では、標準状態でlighttpdというHTTPサーバが可動していますので、 これを利用します。

ATDEを起動している状態で、他のPCなどからウェブブラウザでATDEのIPアドレスに アクセスするとlighttpdの初期画面が表示されます。

この画面を簡単なhtmlに置き換えます。

<html>
    <body>
        hello, Python.
    </body>
</html>

図7.14 取得するindex.htmlファイル(index.html)


作成したhtmlファイルを、wwwディレクトリに配置してパーミッションを変更します。

[ATDE ~]$ mv index.html /var/www/html
[ATDE ~]$ sudo chown www-data:www-data /var/www/html/index.html

ここまでで、再度ブラウザからATDEのIPアドレスにアクセスすると、「hello, Python.」 と表示されることが確認できます。

7.3.2.2. HTTPサーバーに接続する

HTTPサーバーにアクセスするPythonプログラムは、urllibパッケージを使うと 簡単に書けます。

import sys
import os
import traceback
import urllib.request

if __name__ == "__main__":
    # 引数が指定されなかった場合、usage表示して終了
    if len(sys.argv) < 2:
        print("Usage: %s <url>" % sys.argv[0])
        sys.exit(os.EX_OK)

    url = sys.argv[1]
    # 接続先URLと送信パラメータからリクエストを作成
    request = urllib.request.Request(url)
    try:
        # 接続してレスポンスを取得
        with urllib.request.urlopen(request) as response:
            # レスポンスを読み込む
            body = response.read()
            print(body.decode())
    except:
        print(traceback.format_exc())

図7.15 HTTPサーバーにアクセスするプログラム(http_access.py)


実行すると、index.htmlの内容が表示されます。

[armadillo ~]# python3 ./http_access.py http://(ATDEのIPアドレス)
<html>
    <body>
        hello, Python.
    </body>
</html>

図7.16 http_access.pyの実行結果


URLにパラメータをつけてリクエストすることもできます。

import sys
import os
import traceback
import urllib.request

if __name__ == "__main__":
    # 引数が指定されなかった場合、usage表示して終了
    if len(sys.argv) < 2:
        print("Usage: %s <url>" % sys.argv[0])
        sys.exit(os.EX_OK)

    url = sys.argv[1]
    # 送信するパラメータを準備
    params = {
        "pref": "hokkaido",
        "city": "sapporo"
    }

    # 接続先URLと送信パラメータからリクエストを作成
    request = urllib.request.Request("%s?%s" % (url, urllib.parse.urlencode(params)))
    try:
        # 接続してレスポンスを取得
        with urllib.request.urlopen(request) as response:
            # レスポンスを読み込む
            body = response.read()
            print(body.decode())
    except:
        print(traceback.format_exc())

図7.17 パラメータ付きでHTTPサーバーにアクセスするプログラム(http_access_param.py)


ここまではGETリクエストでしたが、POSTでリクエストすることもできます。

POSTでJSONデータを送信するプログラム例です。

import sys
import os
import traceback
import urllib.request
import json

if __name__ == "__main__":
    # 引数が指定されなかった場合、usage表示して終了
    if len(sys.argv) < 2:
        print("Usage: %s <url>" % sys.argv[0])
        sys.exit(os.EX_OK)

    url = sys.argv[1]
    # 送信するJSONデータを準備
    data = {
        "pref": "hokkaido",
        "city": "sapporo"
    }
    # 送信ヘッダを準備
    headers = {
        'Content-Type': 'application/json'
    }

    # 接続先URLと送信パラメータからリクエストを作成
    request = urllib.request.Request(url, json.dumps(data).encode(), headers)
    try:
        # 接続してレスポンスを取得
        with urllib.request.urlopen(request) as response:
            # レスポンスを読み込む
            body = response.read()
            print(body.decode())
    except:
        print(traceback.format_exc())

図7.18 パラメータ付きでHTTPサーバーにアクセスするプログラム(http_access_post.py)


7.4. データベースを使う

センサーなどから取得したデータやサーバーから受信したデータを保存しておく場合、 ファイルとして保存しておくのも手ですが、データ同士に関連があったりまとまったデータに対して、 なにか条件をつけて目的のデータを検索したいとなった場合、データベースを構築して そこに保存しておく方法もあります。

ここではPythonでデータベースを扱う例について取り上げます。

Python向けにpeeweeというデータベースを扱うパッケージがあるので、 それを使います。

peeweeはSQLite、MySQL、PostgreSQL、CockroachDBといったデータベースシステムを扱えますが、 ここでは組み込み向けに利用されることが多いSQLiteを選択します。

まずは、pipでpeeweeをインストールします。

[armadillo ~]# pip3 install peewee

例として「テキストファイルを扱う」で使用した、住所のCSVファイルのデータを データベースに保存するプログラムを示します。

仕様は以下のとおりです。

  1. データベースファイルの名前はaddress.dbとする
  2. CSVファイルの先頭から10件分の住所をデータベースに保存する
  3. 保存するデータは、郵便番号、都道府県、市区町村、住所とする
  4. サブコマンドとしてadd、show、update、deleteの4つを用意する
  5. add -F <CSVファイル名> で読み込むCSVファイルを指定する
  6. show で保存しているデータを表示する
  7. update -I <id> -C <column名> -V <値> で指定したidのデータの<column名>を<値>に変更する
  8. delete -I <id> で指定したidのデータを削除する
import sys
import os
import csv
import traceback
import argparse
from peewee import *

# CSVデータ1行の要素数
COLUMN_NUM = 11

# データベースファイルのオープン
db = SqliteDatabase("address.db")

# 住所クラスの定義
class Address(Model):
    zipcode = CharField()
    pref = CharField()
    city = CharField()
    street = CharField()

    class Meta:
        database = db

# Addressテーブルの作成
db.create_tables([Address])

def add(args):

    """ データベースへのデータの追加

    Args:
        args: コマンド引数
            args.file: CSVファイル名

    """

    with open(args.file) as csvfile:
        csvreader = csv.reader(csvfile)
        count = 0
        try:
            # トランザクションの開始
            with db.transaction():
                # CSVファイルから1行読み込む
                # 10行目まで読み込む
                for row in csvreader:
                    # 要素数が不足している場合、次行にスキップ
                    if len(row) < COLUMN_NUM:
                        continue
                    if count >= 10:
                        break

                    # データベースへのデータの追加(INSERT)
                    Address.create(zipcode=row[1], pref=row[4], city=row[3], street=row[2])

                    count += 1

                # トランザクション終了 データをコミット
                db.commit()
        except:
            # エラー発生の場合データベースをロールバック
            db.rollback()
            print(traceback.format_exc())

def show(args):

    """ データベースに保存されているデータの表示

    Args:
        args: コマンド引数

    """

    for address in Address.select():
        print("%s %s %s %s %s" \
                % (address.id, address.zipcode, address.pref, address.city, address.street))

def update(args):

    """ データベースに保存されているデータを変更する

    Args:
        args: コマンド引数
            args.id: 変更したいデータのid
            args.column: 変更したいデータのカラム名
            args.val: 変更後の値

    """

    try:
        # トランザクションの開始
        with db.transaction():
            # idを指定してデータを取得
            address = Address.get(id=args.id)

            # 指定されたカラム名毎に値を変更
            if args.column == "zipcode":
                address.zipcode = args.val
            elif args.column == "pref":
                address.pref = args.val
            elif args.column == "city":
                address.city = args.val
            elif args.column == "street":
                address.street = args.val

            # 変更を保存してトランザクション終了
            address.save()
            db.commit()
    except:
        # エラー発生の場合データベースをロールバック
        db.rollback()
        print(traceback.format_exc())

def delete(args):

    """ データベースに保存されているデータを削除

    Args:
        args: コマンド引数
            args.id: 削除したいデータのid

    """

    try:
        # トランザクションの開始
        with db.transaction():
            # idを指定してデータを取得
            address = Address.get(id=args.id)

            # データを削除
            address.delete_instance()

            # トランザクション終了
            db.commit()
    except:
        # エラー発生の場合データベースをロールバック
        db.rollback()
        print(traceback.format_exc())

def commandline_parser():

    """ コマンドオプションとハンドラを設定する

    """

    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers()

    # addコマンド
    add_command = subparsers.add_parser("add")
    add_command.add_argument("-F", "--file", required=True, help="csv file")
    add_command.set_defaults(handler=add)

    # showコマンド
    show_command = subparsers.add_parser("show")
    show_command.set_defaults(handler=show)

    # updateコマンド
    update_command = subparsers.add_parser("update")
    update_command.add_argument("-I", "--id", required=True, help="id number")
    update_command.add_argument("-C", "--column", required=True, help="column name")
    update_command.add_argument("-V", "--val", required=True, help="new value")
    update_command.set_defaults(handler=update)

    # deleteコマンド
    delete_command = subparsers.add_parser("delete")
    delete_command.add_argument("-I", "--id", required=True, help="id number")
    delete_command.set_defaults(handler=delete)

    return parser

if __name__ == "__main__":
    parser = commandline_parser()
    args = parser.parse_args()
    if hasattr(args, 'handler'):
        # ハンドラが登録されていれば実行
        args.handler(args)
    else:
        # 未知のサブコマンドの場合はヘルプを表示
        parser.print_help()

図7.19 データベースを操作するプログラム(handling_database.py)


実際に動作させてみます。まず、CSVファイルからデータベースに保存します。

サブコマンドaddにオプション-FでCSVファイルを指定して実行します。

[armadillo ~]# python3 ./handling_database.py add -F ./ken_all_rome.csv

図7.20 handling_database.pyの実行結果(addサブコマンド)


サブコマンドshowでデータが保存されていることが確認できます。

[armadillo ~]# python3 ./handling_database.py show
1 0600000 HOKKAIDO CHUO-KU SAPPORO-SHI IKANIKEISAIGANAIBAAI
2 0640941 HOKKAIDO CHUO-KU SAPPORO-SHI ASAHIGAOKA
3 0600041 HOKKAIDO CHUO-KU SAPPORO-SHI ODORIHIGASHI
4 0600042 HOKKAIDO CHUO-KU SAPPORO-SHI ODORINISHI(1-19-CHOME)
5 0640820 HOKKAIDO CHUO-KU SAPPORO-SHI ODORINISHI(20-28-CHOME)
6 0600031 HOKKAIDO CHUO-KU SAPPORO-SHI KITA1-JOHIGASHI
7 0600001 HOKKAIDO CHUO-KU SAPPORO-SHI KITA1-JONISHI(1-19-CHOME)
8 0640821 HOKKAIDO CHUO-KU SAPPORO-SHI KITA1-JONISHI(20-28-CHOME)
9 0600032 HOKKAIDO CHUO-KU SAPPORO-SHI KITA2-JOHIGASHI
10 0600002 HOKKAIDO CHUO-KU SAPPORO-SHI KITA2-JONISHI(1-19-CHOME)

図7.21 handling_database.pyの実行結果(showサブコマンド)


次に、サブコマンドupdateで5番目のデータを変更してみます。

[armadillo ~]# python3 ./handling_database.py update -I 5 -C zipcode -V 9000002
[armadillo ~]# python3 ./handling_database.py update -I 5 -C pref -V OKINAWA
[armadillo ~]# python3 ./handling_database.py update -I 5 -C city -V NAHA-SHI
[armadillo ~]# python3 ./handling_database.py update -I 5 -C street -V AKEBONO
[armadillo ~]# python3 ./handling_database.py show
1 0600000 HOKKAIDO CHUO-KU SAPPORO-SHI IKANIKEISAIGANAIBAAI
2 0640941 HOKKAIDO CHUO-KU SAPPORO-SHI ASAHIGAOKA
3 0600041 HOKKAIDO CHUO-KU SAPPORO-SHI ODORIHIGASHI
4 0600042 HOKKAIDO CHUO-KU SAPPORO-SHI ODORINISHI(1-19-CHOME)
5 9000002 OKINAWA NAHA-SHI AKEBONO
6 0600031 HOKKAIDO CHUO-KU SAPPORO-SHI KITA1-JOHIGASHI
7 0600001 HOKKAIDO CHUO-KU SAPPORO-SHI KITA1-JONISHI(1-19-CHOME)
8 0640821 HOKKAIDO CHUO-KU SAPPORO-SHI KITA1-JONISHI(20-28-CHOME)
9 0600032 HOKKAIDO CHUO-KU SAPPORO-SHI KITA2-JOHIGASHI
10 0600002 HOKKAIDO CHUO-KU SAPPORO-SHI KITA2-JONISHI(1-19-CHOME)

図7.22 handling_database.pyの実行結果(updateサブコマンド)


5番目のデータが変更されていることが確認できます。

最後に、サブコマンドdeleteで6番目のデータを削除してみます。

[armadillo ~]# python3 ./handling_database.py delete -I 6
[armadillo ~]# python3 ./handling_database.py show
1 0600000 HOKKAIDO CHUO-KU SAPPORO-SHI IKANIKEISAIGANAIBAAI
2 0640941 HOKKAIDO CHUO-KU SAPPORO-SHI ASAHIGAOKA
3 0600041 HOKKAIDO CHUO-KU SAPPORO-SHI ODORIHIGASHI
4 0600042 HOKKAIDO CHUO-KU SAPPORO-SHI ODORINISHI(1-19-CHOME)
5 9000002 OKINAWA NAHA-SHI AKEBONO
7 0600001 HOKKAIDO CHUO-KU SAPPORO-SHI KITA1-JONISHI(1-19-CHOME)
8 0640821 HOKKAIDO CHUO-KU SAPPORO-SHI KITA1-JONISHI(20-28-CHOME)
9 0600032 HOKKAIDO CHUO-KU SAPPORO-SHI KITA2-JOHIGASHI
10 0600002 HOKKAIDO CHUO-KU SAPPORO-SHI KITA2-JONISHI(1-19-CHOME)

図7.23 handling_database.pyの実行結果(deleteサブコマンド)


6番目のデータが削除されていることが確認できます。

このように、PythonからSQLデータベースを複雑なSQL文を意識することなく、 操作できます。

peeweeには他にも様々な機能がありますので詳細は公式サイト[43]を 参照してください。

ここで作成したデータベースファイルaddress.dbはWindowsなどにあるSQLデータベースブラウザ のようなアプリケーションで中身を直接確認することもできます。

7.5. プログラムをデバッグする

Pythonでアプリケーションを開発しても、C言語に比べてコード量が少ないとはいえ バグは存在します。 Pythonはスクリプト言語なので、怪しいと思った箇所にprint文を 埋め込んでコンパイル不要ですぐに動作を確認することができますが、 それでも数が多くなるとprint文を埋め込むのもデバッグ後に削除するのも大変です。

ここではPythonプログラムのデバッグ手法について紹介します。

7.5.1. トレースバックを読む

Pythonでは、プログラムがクラッシュした場合、その理由やプログラムのどこで 発生したのかなどの情報を含んだトレースバックを出力します。 ほとんどの場合は、このトレースバックを読んで原因箇所を修正すれば解決します。

以下は0除算を行う可能性のあるプログラムです。

import os
import sys

def divide(a, b):
    """ aをbで割った商を返す
    """
    c = a / b
    return c

if __name__ == "__main__":
    n = [5, 2, 4, 0, 1, 3]
    for i in n:
        d = divide(10, i)
        print(d)

図7.24 0除算を行う可能性のあるプログラム (dividezero.py)


実行すると以下のようになります。

[armadillo ~]# python3 ./dividezero.py
2.0
5.0
2.5
Traceback (most recent call last):
  File "dividezero.py", line 13, in <module>
    d = divide(10, i)
  File "dividezero.py", line 7, in divide
    c = a / b
ZeroDivisionError: division by zero

図7.25 dividezero.pyの実行結果


トレースバックが出力されています。 下から上に向かって読むと、まず

ZeroDivisionError: division by zero

このメッセージで0除算が発生していることが分かります。 次に

  File "dividezero.py", line 7, in divide
    c = a / b

ここで、divide関数の中、プログラム7行目で発生していることが分かります。

別な例として、配列の範囲外へのアクセスが発生した場合です。

import os
import sys

def printarray(a):
    """ 配列を表示する
    """
    for i in range(0, 10):
        print(a[i])

if __name__ == "__main__":
    n = [5, 2, 4, 0, 1, 3]
    printarray(n)

図7.26 配列の範囲外へアクセスする可能性のあるプログラム (outofrange.py)


実行すると以下のようになります。

[armadillo ~]# python3 ./outofrange.py
5
2
4
0
1
3
Traceback (most recent call last):
  File "outofrange.py", line 12, in <module>
    printarray(n)
  File "outofrange.py", line 8, in printarray
    print(a[i])
IndexError: list index out of range

図7.27 outofrange.pyの実行結果


トレースバックの内容から、プログラム8行目で配列の範囲外へのアクセスが 発生していることが分かります。

このように、クラッシュが発生するようなバグの場合はトレースバックに表示される 情報ですぐに発生原因と発生箇所を特定できるので、まずはトレースバックに目を通すのが よいです。

7.5.2. デバッガを使う

クラッシュはしないが期待していた動作と違うといった場合は、デバッガを使うのが有効です。

Pythonでは標準でpdbというデバッガが用意されていますので、これを使います。

デバッグ対象の簡単なプログラムです。

import os
import sys

def sum(a):
    """ 1からaまでの和を計算する
    """
    s = 0
    for i in range(1, a):
        s += i

    return s

if __name__ == "__main__":
    s = sum(100)
    print(s)

図7.28 1から100までの和を計算するプログラム (sum.py)


このプログラムを実行すると以下のように表示されます。

[armadillo ~]# python3 ./sum.py
4950

図7.29 sum.pyの実行結果


結果として5050と表示されるはずですが期待と違っていました。 pdbを使ってデバッグしてみます。

pdbを使うためにプログラム実行時にオプション引数を渡します。

[armadillo ~]# python3 -m pdb sum.py
> /home/atmark/workspace/source/sum.py(1)<module>()
-> import os
(Pdb)

1行目で止まり、(Pdb)プロンプトが表示されました。今後はこのプロンプトに コマンドを入力してデバッグを進めます。 nextと入力すると次の行へ進んで実行が止まります。

[armadillo ~]# python3 -m pdb sum.py
> /root/work/developers_guide/python/debug/sum.py(1)<module>()
-> import os
(Pdb) next
> /root/work/developers_guide/python/debug/sum.py(2)<module>()
-> import sys
(Pdb) next
> /root/work/developers_guide/python/debug/sum.py(4)<module>()
-> def sum(a):
(Pdb) next
> /root/work/developers_guide/python/debug/sum.py(11)<module>()
-> if __name__ == "__main__":
(Pdb) next
> /root/work/developers_guide/python/debug/sum.py(12)<module>()
-> s = sum(100)
(Pdb) next
> /root/work/developers_guide/python/debug/sum.py(13)<module>()
-> print(s)

ここでpコマンドを使うと、変数sに入っている値を表示することができます。

(Pdb) p s
4950

continueコマンドを使うとプログラムを最後まで実行します。最後まで実行された後は、 再び最初から実行が開始されます。

(Pdb) continue
4950
The program finished and will be restarted
> /root/work/developers_guide/python/debug/sum.py(1)<module>()
-> import os

pdbを終了するにはquitと入力します。

(Pdb) quit
[armadillo ~]#

再び、pdbを開始しsum関数の呼び出し箇所まで処理を進めます。

# python3 -m pdb sum.py
> /root/work/developers_guide/python/debug/sum.py(1)<module>()
-> import os
(Pdb) next
> /root/work/developers_guide/python/debug/sum.py(2)<module>()
-> import sys
(Pdb) next
> /root/work/developers_guide/python/debug/sum.py(4)<module>()
-> def sum(a):
(Pdb) next
> /root/work/developers_guide/python/debug/sum.py(11)<module>()
-> if __name__ == "__main__":
(Pdb) next
> /root/work/developers_guide/python/debug/sum.py(12)<module>()
-> s = sum(100)

ここで、nextではなくstepと入力するとsum関数の中へ入ることができます。nextも stepも1行ずつ実行するコマンドですが、nextは関数の中には入らず、stepは関数の中に入ります。

(Pdb) step
--Call--
> /root/work/developers_guide/python/debug/sum.py(4)sum()
-> def sum(a):

sum関数の中へ入り、処理が止まりました。

ここまで、nextやstepで一行ずつ実行してきましたが、目的の行に たどり着くまで何度も入力するのは大変です。

そこで、ブレークポイントを設定しそこまで一気に処理を実行してみます。

まず、ブレークポイントを設定する行を確認するために、listコマンドで ソースコードを表示します。

[armadillo ~]# python3 -m pdb sum.py
> /home/atmark/work/developer_guide/python/sum.py(1)<module>()
-> import os
(Pdb) list
  1  -> import os
  2     import sys
  3
  4     def sum(a):
  5         s = 0
  6         for i in range(1, a):
  7             s += i
  8
  9         return s
 10
 11     if __name__ == "__main__":
(Pdb)
[警告]

ソースコード内にコメントがある場合はコメントも表示されます。 この時、日本語のコメントがあった場合、環境によっては 文字エンコードに関する例外が発生することがありますが、 デバッグは可能です。

breakコマンドで7行目にブレークポイントを設定し、continueコマンドで 設定した行まで一気に実行します。

(Pdb) break 7
Breakpoint 1 at /root/work/developers_guide/python/debug/sum.py:7
(Pdb) continue
> /root/work/developers_guide/python/debug/sum.py(7)sum()
-> s += i
(Pdb) p s
0
(Pdb) p i
1

ブレークポイントで実行が停止しました。そこでの変数sとiの値も表示しています。

displayコマンドを使うと値が変化した変数が自動的に表示されるようになります。

(Pdb) display s
display s: 0
(Pdb) display i
display i: 1
(Pdb) continue
> /root/work/developers_guide/python/debug/sum.py(7)sum()
-> s += i
display s: 1  [old: 0]
display i: 2  [old: 1]
(Pdb) continue
> /root/work/developers_guide/python/debug/sum.py(7)sum()
-> s += i
display s: 3  [old: 1]
display i: 3  [old: 2]
(Pdb) continue
> /root/work/developers_guide/python/debug/sum.py(7)sum()
-> s += i
display s: 6  [old: 3]
display i: 4  [old: 3]

for文を抜けるまでcontinueし続けるのは大変なので、clearコマンドでブレークポイントを 削除し、returnコマンドで関数の最後まで処理を実行します。

clearコマンドでブレークポイントを削除する時は、ソースコードの行数ではなく ブレークポイント番号を指定する必要があります。ブレークポイント番号は、 breakと入力することで確認できます。

(Pdb) break
Num Type         Disp Enb   Where
1   breakpoint   keep yes   at /root/work/developers_guide/python/debug/sum.py:7
        breakpoint already hit 6 times
(Pdb) clear 1
Deleted breakpoint 1 at /root/work/developers_guide/python/debug/sum.py:7
(Pdb) return
--Return--
> /root/work/developers_guide/python/debug/sum.py(9)sum()->4950
-> return s
display s: 4950  [old: 15]
display i: 99  [old: 6]

ブレークポイントを削除し、関数の最後まで処理を進めたところ 変数iの値が99となっており、forループが99回しか実行されておらず、 forループの終了条件に問題があることがわかりました。

このように、デバッガを使うとデバッグのためのprint文を埋め込むことなく 処理の流れや変数の内容を確認できるので、効率的にデバッグができるようになります。

最後に、ここでの説明で使用したpdbのコマンド一覧を示します。 またほとんどのコマンドは、省略形でも使うことができます。

表7.1 使用したコマンド

コマンド 省略形 動作

step

s

1行ずつ実行し、関数の場合は中に入る

next

n

1行ずつ実行し、関数には入らない

p

-

変数の値を表示する

continue

c

実行を再開する

quit

q

pdbを終了する

list

l

ソースコードを表示する

break n

b n

n行目にブレークポイントを設定する

clear n

d n

n番目のブレークポイントを削除する

return

r

関数を最後まで実行する

display

-

変化のあった変数の値を表示する


他にもコマンドがありますので詳細は公式サイト[44]を参照してください。