今日も見に来てくださって、ありがとうございます。なぜか着々と読者数が増えています。
先日、Pythonで16進数ダンプするプログラムをつくる、ということで記事をアップしました。でも、しばらくして、そういえば、あれはスクリプトをつくっただけで、プログラムとは言えないよねぇ、という気持ちになってまいりました。再利用もできないし、拡張性にも乏しいですからね。と、いうことで今回は、先日のスクリプトをモジュールにしてみる、ということをテーマにしようと思います。
Pythonでモジュールをつくると、import
文で読み込んで利用することができるようになります。先日は、「python hexdump.py ファイル名
」で実行するようにしましたが、この機能を残したまま、プログラム中で「h=hexdump(filename)
」のように文字列を返すようにしてみたいと思います。
今回も、時間のない方に、完成したソースは以下の通りです。
from os.path import exists def hexdump(filename, separator=" ", enterper=20, separateper=10, start=None, end=None): '''処理:ファイルの内容を16進数でダンプする 引数:separator 区切り文字 初期値空白 バイトごとに挿入する enterper 何バイトごとに改行するか separateper 何バイトごとに追加で区切り文字を入れるか start 何バイト目からスライスの開始値 end 何バイト目までスライスの終了値 ''' if not exists(filename): raise Exception(f"指定されたファイルは存在しません。[{filename}]") with open(filename, 'rb') as f: data = f.read() output = "" data = data[start:end] for i, x in enumerate(data, 1): output += f'{x:02x}' + separator if enterper > 0 and i % enterper == 0: output += "\n" elif separateper > 0 and i % separateper == 0: output += separator return output if __name__ == '__main__': from sys import argv howtouse = f'使い方:python {argv[0]} ダンプしたいファイル名' if len(argv) != 2: print(howtouse) exit() print(hexdump(argv[1]))
取り急ぎ、まずは、関数を作ってみることにしましょう。関数は、def
を使って定義します。前回のスクリプトは以下のように変更されます。
from os.path import exists def hexdump(filename): '''処理:ファイルの内容を16進数でダンプする''' if not exists(filename): raise Exception(f"指定されたファイルは存在しません。[{filename}]") with open(filename, 'rb') as f: data = f.read() output = "" for i, x in enumerate(data, 1): output += f'{x:02x} ' if i % 20 == 0: output += "\n" elif i % 10 == 0: output += ' ' return output if __name__ == '__main__': from sys import argv howtouse = f'使い方:python {argv[0]} ダンプしたいファイル名' if len(argv) != 2: print(howtouse) exit() print(hexdump(argv[1]))
上から順番に見ていきましょう。まずdef
を使って関数を定義していますが、その次の行です。'''
で始まって、'''
で終了していますが、これは三連引用符といいまして、間の改行を許して文字列を生成してくれます。このdef
の直後の文字列はドキュメンテーション文字列として扱われ、大規模開発など、たくさんの人と協業してプログラムをつくるときにとても役立つ説明になります。モジュールをインポートすると、以下のようにhelp
関数を使って確認することができます。
>>> from hexdump import hexdump >>> help(hexdump) Help on function hexdump in module hexdump: hexdump(filename) 処理:ファイルの内容を16進数でダンプする >>>
このドキュメンテーション文字列、実際には、関数オブジェクトの属性__doc__
に格納されています。help
関数はこの属性値を整形して出力しているのでしょうね。
>>> hexdump.__doc__ '処理:ファイルの内容を16進数でダンプする' >>>
さて、次にさらりと登場したのがraise Exception
ですね。これは例外(Exception)というPythonの機構で、実行時のエラーに対処するために用意されているものです。ファイルが存在しない場合を考慮せず実行すれば、open
でFileNotFoundError: [Errno 2] No such file or directory: '指定したファイル名'
という例外が発生します。でも、今回のプログラムでは、「指定されたファイルは存在しません。[ファイル名]」と丁寧に教えてあげることにしました。大規模なモジュールを開発するときは独自の例外を定義するようですが、今回は規模も小さいので、これでよいでしょう。
このファイルが開けなかった時のようなエラーの一般的な処置として、関数の戻り値を使ってNone
を返すことでエラーを教えることもできますが、予期せぬ不具合になる可能性があるため、このような場合には例外を選ぶ、というのがパイソニックな考え方だそうです(笑)。
ちなみに、どういうときに予期せぬ不具合になるかというと、ファイルサイズが0の時の戻り値判定などですね。if not data:
のように書いてしまうと、エラーかデータなしかわからずに処理続行してしドツボにハマる可能性がありそうです。
さて、のこりもうひとつ注目点は、最後の方のif __name__ == '__main__':
です。この記述により、モジュールとしてimport
したときにこの行以降は実行されなくなります。ただし、python hexdump.py ...
と実行されたときには、このif
以降の処理が有効になります。これもPythonでモジュールをスタンドアロンとしてスクリプトを実行する準標準イディオムなので、覚えておくとよいでしょう。最後にこのスクリプトを書くことで、スタンドアロンとして実行できるだけでなく、モジュールの使い方の具体例を示すことができるので、積極的に書いた方がいいのかな、と思います。
いい感じにできました、ということで実行してみましたところ、スタンドアロンの実行はこれまで通りで問題ありませんでした。しかし、関数として実行した結果はいまいちです。こんな感じです。
>>> import hexdump >>> hexdump.hexdump('hexdump.py') '66 72 6f 6d 20 6f 73 2e 70 61 74 68 20 69 6d 70 6f 72 74 20 \n65 78 69 73 74 73 0d 0a 0d 0a 64 65 66 20 68 65 78 64 75 6d \n70 28 ・・・(中略)・・・ \n20 20 65 78 69 74 28 29 0d 0a 20 20 20 20 70 72 69 6e 74 28 \n68 65 78 64 75 6d 70 28 61 72 67 76 5b 31 5d 29 29 0d 0a 0d \n0a ' >>>
間の空白や改行コード、自由に指定したいですよね。それに、開始と取得バイト数などがあるといいかも。と、いうことで、指定可能に変更してみます。
def hexdump(filename, separator=" ", enterper=20, separateper=10, start=None, end=None): '''処理:ファイルの内容を16進数でダンプする 引数:separator 区切り文字 初期値空白 バイトごとに挿入する enterper 何バイトごとに改行するか(0は改行なし) separateper 何バイトごとに追加で区切り文字を入れるか(0は追加なし) start 何バイト目からスライスの開始値 end 何バイト目までスライスの終了値 '''
パラメータがありすぎて、ちょっと使い勝手がよくないですけど、その他の変更箇所はわりと限定されていて、以下のとおりです。
data = data[start:end] for i, x in enumerate(data, 1): output += f'{x:02x}' + separator if enterper > 0 and i % enterper == 0: output += "\n" elif separateper > 0 and i % separateper == 0: output += separator
ここまで完全にスルーしてましたけど、「%」演算子は余りを求める演算子でモジュロといいます。0で割り算しようとするとエラーになってしまいますので、enterper > 0
を入れることで意味のないマイナス値とエラーを回避しています。ちなみにPythonの論理演算は、左から順番に評価していきます。enterper > 0
を評価してからi % enterper == 0
を評価する、ということです。また、enterper > 0
がFalseになる場合はその時点で式全体がFalseであるということが確定するので、次の式の評価は必要ないため行いません。このため、評価してほしい順番に式を書くよう気をつけましょう。
改良点はまだまだありますが、ここまででいったんモジュールへの置き換えは完了とします。