2016/09/27

Python Tips:デコレータに引数を渡したい

Python のデコレータに引数を渡す方法について見てみます。

具体的には「引数を取ることができるデコレータの作り方」を見ていきます。

まずはかんたんにおさらいから。 Python では `@デコレータ` という形でデコレータを使うことができます。

# デコレータ関数 add_print() の定義
def add_print(func):
    def wrapper(*args):
        print("{} is invoked.".format(func.__name__))
        func(*args)

    # ラップした関数そのものを返す
    return wrapper

# デコレータ関数の利用
@add_print
def myfunc():
    print("Hello!")

# 動作確認
myfunc()
# =>
# myfunc is invoked.
# Hello!

myfunc() を add_print() でデコレートできていることが確認できます。

デコレータ機能をシンプルに使うだけの場合はこれでよいのですが、デコレータを使ってちょっと凝ったことをやろうとするとデコレータにパラメータを渡したくなってきます。今回はその方法を見ていきます。

作り方の前に使い方を先に見てみましょう。引数を受け取れるデコレータは次のように使います。

@デコレータ関数を生成する関数(引数)
def デコレート対象の関数(引数):
    関数の中身

引数を取らないデコレータの場合と見比べてみましょう。引数を取らないデコレータは次のように書きます。

@デコレータ関数
def デコレート対象の関数(引数):
    関数の中身

両者のちがいは、「 @ 」の後ろが「デコレータ関数を生成する関数(引数)」となっているか「デコレータ関数」となっているかです。

「デコレータ関数を生成する関数」と書いているのはまちがいではありません。引数を受け取れるデコレータというのは、実は「引数を受け取ってデコレータ関数を生成する関数」にほかなりません。

・・・概念的な説明だけだと何を言ってるのかわけわからないので具体例を見てみましょう。

sample.py:

# coding: utf-8

import os


def main():
    """main 関数
    """
    print("CSV generator.")
    names = None
    while not names:
        print("Enter file names with comma delimiter: ", end="")
        names = input().strip().split(",")

    create_files(*[name + ".csv" for name in names])


def add_print(pattern):
    """ファイルの作成前後に表示を行うデコレータ
    """
    def _add_print(func):
        def wrapper(*args):
            print("Creating {} {} ... ".format(pattern, ",".join(args)), end="")
            func(*args)
            print("Done!")
        return wrapper

    return _add_print


@add_print("files")
def create_files(*paths):
    """空のファイルを作成する
    """
    for p in paths:
        with open(p, "a"):
            os.utime(p, None)


if __name__ == "__main__":
    main()

動作イメージ:

python sample.py
CSV generator.
Enter file names with comma delimiter: a,b
Creating files a.csv,b.csv ... Done!

少し長いですが、ポイントになるのはごく一部です。

デコレータの使用場所は以下の部分です。

@add_print("files")
def create_files(*paths):
    """空のファイルを作成する
    """
    for p in paths:
        with open(p, "a"):
            os.utime(p, None)

この add_print() というのが上述の「引数を受け取ってデコレータ関数を生成する関数」になります。これはデコレータ関数ではなく、引数 files を受け取ってデコレータ関数 _add_print() を生成しています。そして、戻り値である _add_print() が create_files() を実際にデコレートし、 wrapper() という関数に置き換えてデコレート処理が完了するという流れです。

ということで、「引数を受け取れるデコレート関数」というものは実は見た目上のものであって、実際には「引数を受け取ってデコレート関数を生成する関数」を作成する必要があります。

元々デコレータ関数というもの自体が「関数を返す関数」なので、「デコレータを返す関数」となると自然「関数を返す関数を返す関数」という入れ子構造となります。見た目はとてもややこしいのですが、自分で実際に書いてみると構造がよくわかってすっきり見通せるようになるので、興味があってまだ書いたことのない方はよろしければぜひ自分の手でひとつ書いてみてください。

参考:

- Pythonのデコレータの使い方

2016/09/01

Python Tips: Mac で ValueError: unknown locale: UTF-8 のエラーを解決したい

Mac OS X のターミナルで Python を利用していると次のようなエラーが出ることがあります。

ValueError: unknown locale: UTF-8

エラーの長いバージョンは次のような形です。

Traceback (most recent call last):
  File "", line 1, in 
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/locale.py", line 560, in getdefaultlocale
    return _parse_localename(localename)
  File "/usr/local/Cellar/python3/3.5.1/Frameworks/Python.framework/Versions/3.5/lib/python3.5/locale.py", line 487, in _parse_localename
    raise ValueError('unknown locale: %s' % localename)
ValueError: unknown locale: UTF-8

今回はこの問題の解決方法を見ていきたいと思います。

この問題は私の場合は matplotlib を使っているときに遭遇しましたが、検索してみると結構多くの方が遭遇されていて、 matplotlib にかぎらずいろんな状況で発生するようです。 Python 以外でも多く発生しているようです。

まずは問題の原因から見ていきます(自分なりに調べて書いていますが、間違っていたらすみません・・・)。

原因


問題の原因は、ターミナルの環境変数の locale 周りの設定がまずいことのようです。

標準ライブラリ locale の中に getdefaultlocale() という関数があるのですが、これが呼び出されたときに、デフォルトのロケール情報を取得するために次の環境変数の値を見にいきます。

- LC_ALL
- LC_CTYPE
- LANG
- LANGUAGE

その際に、これらの変数の値として「 ja_JP.UTF-8 」といったフォーマットの文字列が入っていることが期待されているようです。

しかし、 Mac のターミナルで環境変数を確認してみると、次のようになっていたりします。

$ locale
LANG="ja_JP.UTF-8"
LC_COLLATE="ja_JP.UTF-8"
LC_CTYPE="UTF-8"
LC_MESSAGES="ja_JP.UTF-8"
LC_MONETARY="ja_JP.UTF-8"
LC_NUMERIC="ja_JP.UTF-8"
LC_TIME="ja_JP.UTF-8"
LC_ALL=

この場合に次のようなことが起こります。

上のリストの上から順に値がチェックされますが、 Python は LC_ALL は空なのでスルーします。次に LC_CTYPE をチェックするとこれには値が入っているのでこれをロケール情報として利用しようとします。ただ、格納されている値が「 UTF-8 」という予想しないフォーマットなので、うまく処理できず例外を上げる形になります。

対策


対策の方向性は 2 通りあるようです。

- 対策 A: 環境変数をセットする
- 対策 B: ターミナルの設定を変える

対策 A: 環境変数をセットする

上述のとおり、最初に LC_ALL がチェックされるので、これに変数をセットしておけば OK のようです。例えば Bash を使っている場合は .bashrc の中に次の 1 行を追加します。

export LC_ALL=ja_JP.UTF-8

エラーが発生するかどうかは次のコマンドでかんたんにチェックできます。

$ python -c 'import locale; print(locale.getdefaultlocale());'

エラーが出なくなれば OK です。

ちなみに上のとおりに LC_ALL をセットしてもよいですが、 LANG の値をそのまま流用するなら次のとおりとなるでしょうか。こちらの方がスマートなような気がします。

export LC_ALL=$LANG


対策 B: ターミナルの設定を変える

もうひとつの対策方法はターミナルの設定を変える方法です。

Terminal の場合と iTerm の場合について説明します。

Terminal の場合は、設定の「 Preferences → Profiles → Advanced 」を開きます。「 Set locale environment variables on startup 」というチェックボックスを見つけて、そのチェックを外します。


iTerm の場合も Terminal と同様です。設定の「 Preferences → Profiles → Terminal 」を開きます。「 Set locale variables automatically 」というチェックボックスを見つけて、そのチェックを外します。


いずれも、設定を変更した後新しいセッションを開くと設定が反映された状態になるので、次のコマンドなどで確認します。

$ python -c 'import locale; print(locale.getdefaultlocale());'

私の場合は例外が出なくなりました。

ちなみにこの場合の locale コマンドの出力結果は次のとおりでした。

$ locale
LANG="ja_JP.UTF-8"
LC_COLLATE="ja_JP.UTF-8"
LC_CTYPE="ja_JP.UTF-8"
LC_MESSAGES="ja_JP.UTF-8"
LC_MONETARY="ja_JP.UTF-8"
LC_NUMERIC="ja_JP.UTF-8"
LC_TIME="ja_JP.UTF-8"
LC_ALL=

LC_CTYPE が他と同じ「 ja_JP.UTF-8 」のフォーマットになっていることがわかりますね。

この「 locale の変数を自動セットする機能」については他の部分にも影響する可能性があるので副作用には注意が必要ですが、個人的には A の対策よりも本質的でシンプルでよい感じがします。

以上です。

ご参考になれば幸いです。

参考
- sphinx で ValueError: unknown locale: UTF-8 というエラーが出た - Please Sleep
- python - Pelican 3.3 pelican-quickstart error "ValueError: unknown locale: UTF-8" - Stack Overflow
- Fix "ValueError: unknown locale: UTF-8" under Mac OS X 10.7 - Lion
- encoding - In OSX Lion, LANG is not set to utf8, how fix? - Stack Overflow
- Setting locales correctly on Mac OSX Terminal application « Remi Bergsma's blog

2016/08/08

ライブラリ:csvtomd

Python のライブラリ「 csvtomd 」をご紹介します。

- mplewis/csvtomd: Convert your CSV files into Markdown tables.

$ pip3 install csvtomd

csvtomd は「 CSV を Markdown のテーブルに変換」するツールです。名前そのままですね。プログラムの中で import して使用するものではなく、ターミナル上で利用できるコマンドを提供してくれます。 Python のコードを書かなくても日頃 Markdown を使う方であれば利用することができます。

コマンド名はそのまま「 csvtomd 」です。

インストール


インストール方法は pip でそのままインストールします。 Python 3 にのみ対応しているとのことで、公式では `pip3` でのインストールコマンドが紹介されています。

$ pip3 install csvtomd

使い方


使い方はシンプルで、対象となる CSV のファイル名を引数に渡すだけです。 CSV を解釈し Markdown 化したテーブルを出力してくれます。日本語にもばっちり対応しています。

$ # 対象の CSV を確認
$ cat analysis.csv
名前,鼻,爪
犬,◯,出しっぱなし
猫,△,収納可
人間,×,出しっぱなし
$ # csvtomd で CSV を Markdown に変換
$ csvtomd analysis.csv
名前  |  鼻  |  爪
----|-----|--------
犬   |  ◯  |  出しっぱなし
猫   |  △  |  収納可
人間  |  ×  |  出しっぱなし

変換対象のファイルは複数受け取ることも可能です。

利用可能なオプションは次のとおりです。

- -h --help: ヘルプを表示する。
- -n --no-filenames: 複数の Markdown テーブルを出力する際にファイル名を表示しない。
- -p --padding: パディングの半角スペースの数を整数で指定する。デフォルトは 2 。
- -d --delimiter: 区切り文字を指定。デフォルトは , 。

以上です。

私はここ数年、仕事をしている日はほぼ毎日 Markdown を触りますが、 Markdown ではテーブルの編集が大変で・・・なかなかテーブルを使いたくありません。テーブルについてはスプレッドシートツールを使う方がずっとかんたんなので、テーブルについてはあえてひと手間入れて「スプレッドシートで編集 → csvtomd などで Markdown 化 → Markdown に貼り付け」の形がよいなぁと思います。