Anaconda3 Python実行時にエラーが出始めました。

今日も見に来て下さって、ありがとうございます。

Anaconda3ディストリビューションのpythonを使っていると、なぜか起動時にエラーが出るようになってしまいました。

(base) C:\work>python
Python 3.7.3 (default, Mar 27 2019, 17:13:21) [MSC v.1915 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
Failed calling sys.__interactivehook__
Traceback (most recent call last):
  File "C:\ProgramData\Anaconda3\lib\site.py", line 439, in register_readline
    readline.read_history_file(history)
  File "C:\ProgramData\Anaconda3\lib\site-packages\pyreadline\rlmain.py", line 165, in read_history_file
    self.mode._history.read_history_file(filename)
  File "C:\ProgramData\Anaconda3\lib\site-packages\pyreadline\lineeditor\history.py", line 82, in read_history_file
    for line in open(filename, 'r'):
UnicodeDecodeError: 'cp932' codec can't decode byte 0xef in position 1549: illegal multibyte sequence
>>> 

エラーはUnicodeDecodeErrorということで、'cp932'(Windowsのデフォルトで概ねShift JISのコードのことです)に変換しようとがんばってみたんだけど、できなかったよ~、ごめんね、という意味です。何にもしていないはずの起動時にどうして出るのでしょうね。エラーメッセージに「history.py」とありますので、きっとインタプリタの履歴の中に変換できない文字コードが使われているということでしょう。エラーになっても実行できますが、履歴は保存されないので不便です。ちなみに、履歴は「C:\Users\username\.python_history」というファイルの中に格納されていました。中身を確認すると、ありました「髮サ隧ア逡ェ蜿キ = 10」というよくわからない行が。UTF-8→SJISに変換してみると「電話番号 = 10」という行になりました。確かに、なんかそんなことを実行した気がする。電話番号(笑)を保存してpythonを実行し直したところ、スムーズに開始しました。ただ、再度終了して起動したところ同様のエラーが出始めます。日本語の行を履歴ファイルからすべて削除すれば出なくなりますが、もっと根本的に解決したいですね。

そこで、histroy.pyファイルをよく読んでみました。どうやら、保存するときに文字列はちゃんとUnicodeになるようにしておいて、バイナリファイルとして保存するようにしているけど、開くときはデフォルトのテキストファイルとして読み込んでおり、デフォルトのエンコーディングが'cp932'になっているのが原因のようです。そこで、このファイルの中のread_history_file関数のファイルを開くところにencodingにutf8を指定するように変更してみます。そう、ちょうどエラーが発生しているhistory.pyファイルの82行目です。

【修正前】
            for line in open(filename, 'r'):
【修正後】
            for line in open(filename, 'r', encoding='utf8'):

はい、うまいこと動くようになりました。これで解決ですね!
ちなみにぼくの環境ではこのhistory.pyファイルは書き込み不可になっているものもありました。その場合は、ファイルのプロパティからセキュリティタブを選んでアクセス許可を編集して書き込みを許可すれば編集できます。

でも、なんでテキストで書いて、バイナリで読んでるのかなぁ。せっかくUnicodeを保証してるのに、最初からUnicodeで読んでほしいですよね。ソースファイル調べてみると、GitHubに登録されていました。issueにも同じエラーについて登録されていました。最近gitを使い始めたので、ちょっと修正依頼をお願いしてみましょう。フォークして、編集をして、プルリクエスト、終了。これであってるかな。ま、たぶん大丈夫でしょう。みんな初心者には優しいはずだし。リクエストが受け付けられるのを待ちましょう。あ、、、別のissueでもnone-latin charで同じようなエラーが出ていて、そちらも同様の修正でプルリクエストがでてる、というのを見つけちゃいました。結構プルリクエストは放置されているようですねぇ。。。ま、いっか。

Pythonで16進数ダンプするプログラムをつくる その2

今日も見に来てくださって、ありがとうございます。なぜか着々と読者数が増えています。

先日、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の機構で、実行時のエラーに対処するために用意されているものです。ファイルが存在しない場合を考慮せず実行すれば、openFileNotFoundError: [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であるということが確定するので、次の式の評価は必要ないため行いません。このため、評価してほしい順番に式を書くよう気をつけましょう。

改良点はまだまだありますが、ここまででいったんモジュールへの置き換えは完了とします。