CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: line_profiler でボトルネックを調べる

前回は Python の標準ライブラリに用意されているプロファイラの profile/cProfile モジュールについて書いた。

blog.amedama.jp

今回は、同じ決定論的プロファイリングを採用したプロファイラの中でも、サードパーティ製の line_profiler を使ってみることにする。 line_profiler は profile/cProfile モジュールに比べるとコード単位でプロファイリングが取れるところが魅力的なパッケージ。 また、インターフェースについても profile/cProfile に近いものとなっている。

今回使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G31
$ python --version
Python 3.5.2

インストール

まずは line_profiler をインストールする。 サードパーティ製のパッケージなので pip を使う。

$ pip install line_profiler

題材とするプログラム

ひとまず、前回も使った FizzBuzz を表示するソースコードをプロファイリングの対象にしよう。

次のようなソースコードを用意する。

$ cat << 'EOF' > fizzbuzz.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def fizzbuzz(n):
    if n % 3 == 0 and n % 5 == 0:
        return 'FizzBuzz'

    if n % 3 == 0:
        return 'Fizz'

    if n % 5 == 0:
        return 'Buzz'

    return str(n)


def main():
    for i in range(1, 100):
        message = fizzbuzz(i)
        print(message)


if __name__ == '__main__':
    main()
EOF

このプログラムは実行すると 1 から 99 までの FizzBuzz を表示する。

$ python fizzbuzz.py
1
2
Fizz
...(省略)...
97
98
Fizz

プロファイリングしてみる

さて、それでは用意したソースコードを早速プロファイリングしてみよう。

ここでは Python の REPL を使って動作を確認していく。 先ほどのソースコードを用意したのと同じディレクトリで REPL を起動しよう。

$ python

REPL が起動したら、まずはプロファイリング対象の fizzbuzz モジュールをインポートしておく。 補足しておくと Python ではソースコード (*.py) が、そのままモジュールに対応している。 例えば先ほどの fizzbuzz.py であれば fizzbuzz モジュールとして使えるわけだ。

>>> import fizzbuzz

続いて line_prpfiler モジュールをインポートして LineProfiler クラスのインスタンスを生成しよう。 このインスタンスを使ってプロファイリングをする。

>>> import line_profiler
>>> pr = line_profiler.LineProfiler()

プロファイリング対象の関数を LineProfiler#add_function() メソッドを使って登録しよう。 ここでは、前述の fizzbuzz モジュールの中にある fizzbuzz 関数を登録した。

>>> pr.add_function(fizzbuzz.fizzbuzz)

そして、続いて LineProfiler#runcall() メソッドでプロファイリングを実行する。 このメソッドで実行した処理の中で、先ほど LineProfiler#add_function() メソッドで登録した関数が呼び出されることを期待する。 もし呼びだされた場合には、それが処理される過程が line_profiler によって逐一記録されるという寸法だ。

>>> pr.runcall(fizzbuzz.main)
1
2
Fizz
...(省略)...
97
98
Fizz

ちなみに、上記の LineProfiler#runcall() メソッドは、代わりに次のようにしても構わない。 こちらのやり方では LineProfiler#enable() メソッドと LineProfiler#disable() メソッドによってプロファイリングのオン・オフを切り替えている。 LineProfiler#enable() メソッドと LineProfiler#disable() メソッドの間に実行された処理がプロファイリングの対象ということになる。

>>> pr.enable()
>>> fizzbuzz.main()
1
2
Fizz
...(省略)...
97
98
Fizz
>>> pr.disable()

プロファイリングが終わったら LineProfiler#print_stats() メソッドで結果を表示しよう。 すると、次のようにプロファイリング結果が得られる。

>>> pr.print_stats()
Timer unit: 1e-06 s

Total time: 0.000292 s
File: /Users/amedama/Documents/temporary/fizzbuzz.py
Function: fizzbuzz at line 5

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     5                                           def fizzbuzz(n):
     6        99           95      1.0     32.5      if n % 3 == 0 and n % 5 == 0:
     7         6            0      0.0      0.0          return 'FizzBuzz'
     8
     9        93           57      0.6     19.5      if n % 3 == 0:
    10        27           13      0.5      4.5          return 'Fizz'
    11
    12        66           46      0.7     15.8      if n % 5 == 0:
    13        13           29      2.2      9.9          return 'Buzz'
    14
    15        53           52      1.0     17.8      return str(n)

上記の表のカラムは次のような意味がある。

  • Line

    • ソースコードの行番号
  • Hits

    • その行が実行された回数
  • Time

    • その行の実行に費やした時間の合計
    • 単位は先頭行の Timer unit で示されている
  • Per Hit

    • その行を 1 回実行するのに費やした時間
  • % Time

    • その行の実行に費やした時間の全体に対する割合
  • Line Contents

    • 行番号に対応するソースコード

とても分かりやすい。

コマンドラインから調べる

line_profiler には、もうひとつコマンドラインからプロファイリングを実行するインターフェースがある。 むしろ、公式の説明を見るとこちらを推している雰囲気がある。 ただ、個人的には先ほどのやり方が便利なので、こちらをあまり使うことはないかな…という感じ。

コマンドラインからの処理では、まずプロファイリングを実行したい関数に一工夫が必要になる。 具体的には、プロファイリングを実行したい関数やメソッドを @profile デコレータで修飾する。

以下は fizzbuzz() 関数に @profile デコレータをつけたサンプルコード。

$ cat << 'EOF' > fizzbuzz.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


@profile
def fizzbuzz(n):
    if n % 3 == 0 and n % 5 == 0:
        return 'FizzBuzz'

    if n % 3 == 0:
        return 'Fizz'

    if n % 5 == 0:
        return 'Buzz'

    return str(n)


def main():
    for i in range(1, 100):
        message = fizzbuzz(i)
        print(message)


if __name__ == '__main__':
    main()
EOF

ちなみに、するどい人は上記が @profile デコレータがインポートされていないのでエラーになると気づくかもしれない。

その通りで、実はこのソースコードは通常の Python インタプリタでは実行できなくなる。

$ python fizzbuzz.py 
Traceback (most recent call last):
  File "fizzbuzz.py", line 5, in <module>
    @profile
NameError: name 'profile' is not defined

ただ、line_profiler を使ってコマンドラインでプロファイリングするときは専用のコマンドを使うことになる。 具体的には kernprof というコマンドを使う。 このコマンドではソースコードの中の @profile デコレータがちゃんと動作するように細工して実行するので問題ない。

早速 kernprof コマンドで fizzbuzz.py をプロファイリングしてみよう。

$ kernprof -l fizzbuzz.py
1
2
Fizz
...(省略)...
97
98
Fizz
Wrote profile results to fizzbuzz.py.lprof

実行すると、プロファイリング対象のファイル名に .lprof という拡張子のついたファイルができる。

そして、そのできたファイルを line_profiler モジュールをコマンドライン経由で読み込ませる。 すると、解析結果が表示される。

$ python -m line_profiler fizzbuzz.py.lprof
Timer unit: 1e-06 s

Total time: 0.000275 s
File: fizzbuzz.py
Function: fizzbuzz at line 5

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     5                                           @profile
     6                                           def fizzbuzz(n):
     7        99          116      1.2     42.2      if n % 3 == 0 and n % 5 == 0:
     8         6            3      0.5      1.1          return 'FizzBuzz'
     9
    10        93           53      0.6     19.3      if n % 3 == 0:
    11        27           15      0.6      5.5          return 'Fizz'
    12
    13        66           33      0.5     12.0      if n % 5 == 0:
    14        13            5      0.4      1.8          return 'Buzz'
    15
    16        53           50      0.9     18.2      return str(n)

ただ、上記のやり方だとソースコードに手を加えないといけないから面倒くさい。 それに、モジュールが実行可能になっていないとプロファイリングできないところも汎用性が低いと感じた。

まとめ

  • 標準ライブラリには profile/cProfile というプロファイラがある
  • それ以外にもサードパーティ製で line_profiler というパッケージがある
  • line_profiler はコード一行単位でプロファイリング結果が見えるので分かりやすい

Python: profile/cProfile モジュールでボトルネックを調べる

プログラムを作ってみたは良いけれど、想定していたよりもパフォーマンスが出ないという事態はよくある。 そんなときはパフォーマンス・チューニングをするわけだけど、それにはまず測定が必要になる。 具体的に何を測定するかというと、プログラムの中のどこがボトルネックになっているかという点だ。 この測定の作業は一般にプロファイリングと呼ばれる。

プロファイリングにはふたつの種類がある。 ひとつ目は今回紹介する profile/cProfile モジュールが採用している決定論的プロファイリング。 そして、もうひとつが統計的プロファイリングだ。

決定論的プロファイリングでは、プログラムが実行されていく過程を逐一記録していく。 そのため、時間のかかっている処理を正確に把握することができる。 ただし、測定自体がプログラムに与える影響 (プローブ・エフェクト) も大きくなる。

それに対し統計的プロファイリングでは、プログラムが実行されていく過程をランダムに記録していく。 これは、統計的にいえばプログラムが実行されていく過程の全体を母集団と捉えて、そこから標本を抽出している。 そして、抽出した標本から母集団を推定する形で、呼び出しの多い処理や時間のかかっている処理を推定することになる。 この場合、結果はあくまで推定 (一定の割合で誤りを許容すること) になるものの、測定自体が与える影響は比較的小さくなる。

profile/cProfile モジュール

前置きが長くなったけど、今回は Python の標準ライブラリに用意されているプロファイリング用のモジュールを紹介する。 具体的には profile と cProfile というふたつの実装で、どちらも前述した通り決定論的プロファイリングを採用している。 ふたつある理由は profile が Pure Python の実装で、cProfile が C 拡張モジュールを使った実装になっている。 それぞれの特徴は、profile に比べると cProfile は測定にかかるオーバーヘッドが少ない代わりに、ユーザの拡張性が低くなっている。 使い分けについては、一般的なユースケースでは cProfile を常に使っていれば良さそうだ。

今回使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.6
BuildVersion:   15G31
$ python --version
Python 3.5.1

まずは使ってみる

何はともあれ profile/cProfile を使ってみることにしよう。 まずは Python の REPL を使って動作を確認していくことにする。

$ python

起動したら cProfile をインポートする。

>>> import cProfile

もちろん profile を使っても構わない。

>>> import profile

動作確認には time モジュールを使うことにしよう。 このモジュールを使うとスレッドを一定時間停止させることができる。

>>> import time

最もシンプルな使い方としてはモジュールの run() 関数に呼び出したい Python のコードを渡す形になる。 こうすると、渡した Python のコードが cProfile 経由で実行されて、プロファイリング結果が得られる。

>>> cProfile.run('time.sleep(1)')
         4 function calls in 1.001 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    1.001    1.001 <string>:1(<module>)
        1    0.000    0.000    1.001    1.001 {built-in method builtins.exec}
        1    1.001    1.001    1.001    1.001 {built-in method time.sleep}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

結果として得られる表の読み方は次の通り。

  • ncalls

    • 呼び出し回数
  • tottime

    • その関数の実行にかかった時間の合計
    • (別の関数呼び出しにかかった時間を除く)
  • percall

    • かかった時間を呼び出し回数で割ったもの
    • (つまり 1 回の呼び出しにかかった平均時間)
  • cumtime

    • その関数の実行にかかった時間の合計
    • (別の関数呼び出しにかかった時間を含む)
  • filename

    • lineno(function): 関数のファイル名、行番号、関数名

再帰的な処理が含まれる場合

上記の ncalls (呼び出し回数) は、再帰呼び出しがあるときとないときで表示が異なる。 例えば、次のような再帰を使ってフィボナッチ数を計算する関数を用意する。

>>> def fib(n):
...     if n == 0:
...         return 0
...     if n == 1:
...         return 1
...     return fib(n - 2) + fib(n - 1)
...

これをプロファイリングにかけると 177/1 といったように分数のような表示が得られる。

>>> cProfile.run('fib(10)')
         180 function calls (4 primitive calls) in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    177/1    0.000    0.000    0.000    0.000 <stdin>:1(fib)
        1    0.000    0.000    0.000    0.000 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

これは、元々は 1 回の呼び出しから関数が再帰的に 176 回呼びだされたことを意味している。 右側が再帰によるものでない呼び出し回数、左側は再帰を含む呼び出し回数になっているため。

より実践的な使い方

先ほどの例では Python のコードを文字列で渡すことでプロファイリングした。 とはいえ、より柔軟に一連の Python のコードをプロファイリングしたいという場合も多いはず。 次は、そんなときはどうしたら良いかについて。

まずは Profile クラスのインスタンスを用意する。

>>> pr = cProfile.Profile()

そして、プロファイリングを開始するために enable() メソッドを呼びだそう。

>>> pr.enable()

その上で、プロファイリングしたいコードを実行する。

>>> fib(10)
55

そして、プロファイリングを終了するために disable() メソッドを呼び出す。

>>> pr.disable()

ちなみに、これは代わりに runcall() メソッドを使っても構わない。

>>> pr.runcall(fib, 10)
55

プロファイリングが終わったら print_stats() メソッドで結果を出力する。

>>> pr.print_stats()
         179 function calls (3 primitive calls) in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 <stdin>:1(<module>)
    177/1    0.000    0.000    0.000    0.000 <stdin>:1(fib)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

結果を詳しく見る

これまでの例では、プロファイリングの対象となるコードがシンプルだったので結果もシンプルだった。 とはいえ、実際にプロファイリングしたいようなコードは色々な処理が呼び出しあうような複雑なものになるはず。 結果も自ずと重厚長大なものになるはずだ。 そんなときは、プロファイリングの結果を pstats モジュールで詳しく調べることができる。

まずは pstats モジュールをインポートする。

>>> import pstats

そして、先ほどの Profile クラスのインスタンスを渡して Stats クラスをインスタンス化しよう。

>>> stats = pstats.Stats(pr)

Stats クラスのインスタンスでは、例えば特定のカラムで結果をソートできる。 ここでは ncalls (呼び出し回数) で降順ソートしてみよう。

>>> stats.sort_stats('ncalls')
<pstats.Stats object at 0x102518278>

>>> stats.print_stats()
         178 function calls (2 primitive calls) in 0.000 seconds

   Ordered by: call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    177/1    0.000    0.000    0.000    0.000 <stdin>:1(fib)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


<pstats.Stats object at 0x102518278>

これで、呼び出し回数の多い fib() 関数が上にくる。

プロファイリング結果をファイルに残す

プロファイリングの結果はデータとして残せるようになっていると色々と便利なはず。

そんなときは Profile クラスの dump_stats() というメソッドが使える。 このメソッドは、ファイル名を指定してプロファイリング結果をファイルに保存できる。

>>> pr.dump_stats('fib.profile')

上記であれば Python の REPL を起動したディレクトリに fib.profile という名前のファイルができる。

$ ls | grep ^fib
fib.profile

上記のファイルは pstats モジュールから読み込んで使うことができる。 Stats クラスをインスタンス化するときにファイル名を文字列で指定すれば結果を読み込むことができる。

>>> import pstats
>>> stats = pstats.Stats('fib.profile')
>>> stats.print_stats()
Tue Aug 30 19:30:07 2016    fib.profile

         178 function calls (2 primitive calls) in 0.000 seconds

   Random listing order was used

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
    177/1    0.000    0.000    0.000    0.000 <stdin>:1(fib)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


<pstats.Stats object at 0x10ec9acf8>

コマンドラインでプロファイリングする

次は、そんなに使う場面はないかもしれないけどコマンドラインでプロファイリングする方法について。

例えば、次のような FizzBuzz を実行するソースコードを用意しておこう。

$ cat << 'EOF' > fizzbuzz.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def fizzbuzz(n):
    if n % 3 == 0 and n % 5 == 0:
        return 'FizzBuzz'

    if n % 3 == 0:
        return 'Fizz'

    if n % 5 == 0:
        return 'Buzz'

    return str(n)


def main():
    for i in range(1, 100):
        message = fizzbuzz(i)
        print(message)


if __name__ == '__main__':
    main()
EOF

このソースコードは実行すると 1 から 99 までの FizzBuzz を出力する。

$ python fizzbuzz.py 
1
2
Fizz
...(省略)...
97
98
Fizz

このとき -m cProfile というオプションをつけて cProfile モジュールが実行されるようにしてみよう。 これで、自動的にプロファイリングが実行される。

$ python -m cProfile fizzbuzz.py
1
2
Fizz
...(省略)...
97
98
Fizz
         202 function calls in 0.001 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.001    0.001 fizzbuzz.py:18(main)
        1    0.000    0.000    0.001    0.001 fizzbuzz.py:5(<module>)
       99    0.000    0.000    0.000    0.000 fizzbuzz.py:5(fizzbuzz)
        1    0.000    0.000    0.001    0.001 {built-in method builtins.exec}
       99    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

この使い方では -s オプションを使って結果のソートができる。

$ python -m cProfile -s ncalls fizzbuzz.py
1
2
Fizz
...(省略)...
97
98
Fizz
         202 function calls in 0.001 seconds

   Ordered by: call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       99    0.000    0.000    0.000    0.000 {built-in method builtins.print}
       99    0.000    0.000    0.000    0.000 fizzbuzz.py:5(fizzbuzz)
        1    0.000    0.000    0.001    0.001 fizzbuzz.py:5(<module>)
        1    0.000    0.000    0.001    0.001 fizzbuzz.py:18(main)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        1    0.000    0.000    0.001    0.001 {built-in method builtins.exec}

同じように -o オプションを使えばプロファイリング結果をファイルに保存できる。

$ python -m cProfile -o fizzbuzz.profile fizzbuzz.py
$ ls | grep .profile$
fizzbuzz.profile

呼び出し関係を調べる

profile/cProfile は処理の呼び出し回数や時間などの他に、呼び出し関係も調べることができる。 例えば、先ほどの FizzBuzz のプロファイリング結果を例に使ってみよう。

まずは改めて Python の REPL を起動する。

$ python

そしてプロファイリング結果をファイル経由で読み込む。

>>> import pstats
>>> p = pstats.Stats('fizzbuzz.profile')

そして print_callers() メソッドで、呼び出し元が分かる。 左が呼び出された関数で、右が呼び出し元の関数になっている。

>>> p.print_callers()
   Random listing order was used

Function                                          was called by...
                                                      ncalls  tottime  cumtime
{built-in method builtins.print}                  <-      99    0.000    0.000  fizzbuzz.py:18(main)
fizzbuzz.py:5(<module>)                           <-       1    0.000    0.001  {built-in method builtins.exec}
{built-in method builtins.exec}                   <-
{method 'disable' of '_lsprof.Profiler' objects}  <-
fizzbuzz.py:18(main)                              <-       1    0.000    0.001  fizzbuzz.py:5(<module>)
fizzbuzz.py:5(fizzbuzz)                           <-      99    0.000    0.000  fizzbuzz.py:18(main)


<pstats.Stats object at 0x100d24a58>

その逆が print_callees() メソッドで得られる。 今度は左が呼び出し元で、右が呼び出し先になっている。

>>> p.print_callees()
   Random listing order was used

Function                                          called...
                                                      ncalls  tottime  cumtime
{built-in method builtins.print}                  ->
fizzbuzz.py:5(<module>)                           ->       1    0.000    0.001  fizzbuzz.py:18(main)
{built-in method builtins.exec}                   ->       1    0.000    0.001  fizzbuzz.py:5(<module>)
{method 'disable' of '_lsprof.Profiler' objects}  ->
fizzbuzz.py:18(main)                              ->      99    0.000    0.000  fizzbuzz.py:5(fizzbuzz)
                                                          99    0.000    0.000  {built-in method builtins.print}
fizzbuzz.py:5(fizzbuzz)                           ->


<pstats.Stats object at 0x100d24a58>

まとめ

  • パフォーマンスチューニングには、まずプロファイリングが必要になる
  • プロファイリングには決定論的プロファイリングと統計的プロファイリングのふたつがある
  • Python には profile/cProfile という決定論的プロファイリングができるモジュールがある
  • このモジュールを使うと特定の処理における関数の呼び出し回数や、かかった時間を調べることができる

参考

27.4. Python プロファイラ — Python 3.6.5 ドキュメント

Python: データセットの標準化について

今回は機械学習とか統計で扱うデータセットの標準化について。

まずは、標準化されていない生のデータセットについて考えてみよう。 それらの多くは、次元によって数値の単位がバラバラだったり、あるいは大きさが極端に異なったりする。 これをそのまま扱ってしまうと、各次元を見比べたときにそれぞれの関係が分かりにくい。 また、機械学習においては特定の次元の影響が強く (または反対に弱く) 出てしまったりすることもあるらしい。 そこで、それぞれの次元のスケールを同じに揃えてやりたい。 これを標準化というようだ。

今回は「Zスコア」という標準化のやり方を扱う。 これは、一言で言ってしまえばデータセットの各要素から平均を引いて、標準偏差で割ったもの。 これをすると、データセットは平均が 0 で標準偏差・分散が 1 になる。

使った環境は次の通り。

$ python --version
Python 3.5.1

NumPy を使った標準化

まずは一番単純な NumPy の配列を直接使ったやり方から。

ひとまず pip を使って NumPy をインストールしておこう。

$ pip install numpy

Python の REPL を起動しよう。

$ python

NumPy をインポートしたら、次にZスコア化する対象となる配列を変数 l に代入する。 ようするに、これがデータセットを模したものということ。

>>> import numpy as np
>>> l = np.array([10, 20, 30, 40, 50])

この時点でデータセットの算術平均は 30 になっている。

>>> l.mean()
30.0

Zスコアによる標準化の第一段階では、まずデータセットの各要素から、データセットの平均値を引く。

>>> l2 = l - l.mean()

データセットの平均値は、これで 0 になった。

>>> l2.mean()
0.0

各要素から平均値を引いたので、中身はこんなことになる。

>>> l2
array([-20., -10.,   0.,  10.,  20.])

次にZスコア化の第二段階では標準偏差を扱う。 今のデータセットの標準偏差は約 14.14 だ。

>>> l2.std()
14.142135623730951

この標準偏差で、同じくデータセットの各要素を割る。

>>> l3 = l2 / l2.std()

これでデータベースの標準偏差は 1 になった。 きれいに 1 になっていないのは浮動小数点の計算誤差だろうね。

>>> l3.std()
0.99999999999999989

中身はこんなことになる。

>>> l3
array([-1.41421356, -0.70710678,  0.        ,  0.70710678,  1.41421356])

そして、これこそが標準化されたデータセットだ。 各要素の単位は、もはや元々のデータセットで扱われていたそれではなくなっている。 代わりに、単に各要素の相対的な位置関係を表したZスコアになっている。

SciPy を使った標準化

さっきは NumPy の配列を自分で標準化してみたけど、実際にはこの作業はライブラリが代わりにやってくれる。 ここでは SciPy を使った例を挙げておこう。

先ほどと同じように、今度は SciPy を pip コマンドでインストールする。

$ pip install scipy

インストールできたら Python の REPL を起動する。

$ python

さっきと同じようにデータセットを模した NumPy 配列を変数 l として用意しておく。

>>> import numpy as np
>>> l = np.array([10, 20, 30, 40, 50])

SciPy では、そのものずばり zscore() という名前の関数が用意されている。

>>> from scipy.stats import zscore

これに NumPy の配列を渡せば一発でZスコアに標準化できる。

>>> zscore(l)
array([-1.41421356, -0.70710678,  0.        ,  0.70710678,  1.41421356])

あっけない。

scikit-learn を使った標準化

同じように scikit-learn にも標準化のユーティリティが用意されている。

まずは scikit-learn を pip でインストールしておく。

$ pip install scikit-learn

Python の REPL を起動したら…

$ python

データセットを模した NumPy 配列を用意して…

>>> import numpy as np
>>> l = np.array([10, 20, 30, 40, 50])

scikit-learn では StandardScaler というクラスを使って標準化する。

>>> from sklearn.preprocessing import StandardScaler

インスタンス化したら、データセットを模した配列を渡す。 これでデータセットの情報が StandardScaler のインスタンスにセットされる。

>>> sc = StandardScaler()
>>> sc.fit(l)

そして StandardScaler#transform() メソッドを使ってデータセットを標準化する。 返り値として標準化されたデータセットが返る。

>>> sc.transform(l)
array([-1.41421356, -0.70710678,  0.        ,  0.70710678,  1.41421356])

ばっちり。 ちなみに、上記を実行すると NumPy 配列の型が int から float に変換されたよ!っていう警告が出る。 けど、意図したものなので無視しておっけー。

まとめ

  • データセットは標準化してから扱ったほうが良いらしい
  • 標準化は「Zスコア」というものを使うのがメジャーっぽい
  • 標準化のやり方には NumPy / SciPy / scikit-learn それぞれにやり方がある

Python: シーケンスアンパックについて

Python では、右辺がタプルなどのとき、左辺に複数の変数を置くことで、その中身を展開できる。 これをシーケンスアンパック (sequence unpack) という。

シーケンスアンパックを試してみる

まずは python コマンドを実行して REPL を起動しておこう。

$ python

例えば example という変数にタプルで 3 つの要素を代入しておく。

>>> example = (1, 2, 3)

これを右辺に置いて、左辺には同じ数の変数 a b c をカンマ区切りで置く。

>>> a, b, c = example

するとタプルの中身が、各変数に代入される。 これがシーケンスアンパックの挙動だ。

>>> a
1
>>> b
2
>>> c
3

シーケンスアンパックできるオブジェクト・できないオブジェクト

シーケンスアンパックは、例えばリストでもできる。

>>> example = [1, 2, 3]
>>> a, b, c = example
>>> a
1
>>> b
2
>>> c
3

シーケンスアンパックできるか否かは、そのオブジェクトがイテラブル (Iterable) か否かによる。 例えば object クラスのインスタンスはイテラブルでないので、シーケンスアンパックできない。

>>> a, b, c = object()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'object' object is not iterable

ユーザ定義クラスでシーケンスアンパックできるようにする

次は、ユーザ定義クラスでシーケンスアンパックできるようにしてみよう。 これは、ようするにそのユーザ定義クラスのインスタンスをイテラブルにするということ。

ユーザ定義クラスをイテラブルにするには、イテレータプロトコルという特殊メソッドを実装する必要がある。 これは具体的には __iter__() と __next__() という、ふたつのメソッドだ。 ただし、イテレータのコンテナオブジェクトであれば、このうち __iter__() さえあればいい。 コンテナオブジェクトというのは、自身のメンバなどを元にイテレータオブジェクトを生成して返すオブジェクトのことをいう。

次のサンプルコードでは User クラスをイテラブルにすることでシーケンスアンパックできるようにしている。 具体的には __iter__() メソッドを実装することで、クラスをイテレータのコンテナオブジェクトにしている。 このメソッドの中では、自身のメンバである name と age をリストにラップした上で iter() 組み込み関数でイテレータオブジェクトを取り出して返している。

>>> class User(object):
...     def __init__(self, name, age):
...         self.name = name
...         self.age = age
...     def __iter__(self):
...         return iter([self.name, self.age])
...

それでは、上記のクラスをインスタンス化しよう。

>>> u = User('Spam', 20)

そして、右辺に上記のインスタンスを、左辺は name と age という変数でそれを受ける。

>>> name, age = u

すると、シーケンスアンパックされて各変数に内容が代入された。

>>> name
'Spam'
>>> age
20

ばっちり。

まとめ

  • タプルやリストの内容を複数の変数に一度に代入することをシーケンスアンパックという
  • シーケンスアンパックできるオブジェクトはイテラブルなオブジェクト
  • ユーザ定義クラスをイテラブルにするにはイテレータプロトコルを実装する

参考

5. データ構造 — Python 3.5.2 ドキュメント

Python: Ellipsis について

今回は Python の特殊な定数 Ellipsis について調べてみた。

Ellipsis ってなんだ

Ellipsis というのは、主に拡張スライス文と共に使われる特殊な定数のこと。 これを使うと、例えば配列などのスライスで「...」を指定できるようになる。

3. 組み込み定数 — Python 3.5.2 ドキュメント

具体的な使用例

例えば、高速な数値計算のための配列ライブラリである NumPy の配列には、この Ellipsis を使うことができる。

ひとまず NumPy をインストールしよう。

$ pip install numpy

そして Python の REPL を起動する。

$ python

array という名前で NumPy の配列を作る。

>>> import numpy as np
>>> array = np.array([1, 2, 3])

この配列のスライスに「...」を指定してみよう。 これが Ellipsis だ。

>>> array[...]
array([1, 2, 3])

NumPy では (その次元の) すべての要素を返すことを表すために使われている。

ユーザ定義クラスで Ellipsis を使ってみる

ユーザ定義クラスで Ellipsis を指定できるようにしてみよう。 まず、ユーザ定義クラスでは __getitem__() という特殊メソッドを実装すると、そのインスタンスにスライス ([]) が使えるようになる。 そのメソッドに渡されるキーとして Ellipsis が指定されたら「...(snip)...」という値を返すようにしてみる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class MyClass(object):

    def __getitem__(self, key):
        if key is Ellipsis:
            return '...(snip)...'

        return None


def main():
    obj = MyClass()
    print(obj[...])


if __name__ == '__main__':
    main()

上記を ellipsis.py という名前で保存して実行してみよう。

$ python ellipsis.py
...(snip)...

ばっちり。

Python 2 と 3 における Ellipsis の振る舞い

実は Ellipsis は Python 2.x と 3.x で振る舞いが結構違っている。 それぞれのインタプリタで挙動の違いを確かめてみよう。

まずは Python 3.x から。

$ python --version
Python 3.5.1
$ python

Python 3.x では「...」が単独で Ellipsis オブジェクトとして使える。

>>> ...
Ellipsis

それ対し Python 2.x ではどうなるだろうか。

$ python --version
Python 2.7.10
$ python

なんと「...」だけでは文法エラーになってしまった。

>>> ...
  File "<stdin>", line 1
    ...
    ^
SyntaxError: invalid syntax

Python 2.x では Ellipsis の使えるシチュエーションが、とても限られていることがわかる。

例えば、次のように通常のメソッドの引数として Ellipsis を受け取るようにしてみよう。

#!/usr/bin/env python
# -*- coding: utf-8 -*-


class MyClass(object):

    def mymethod(self, value):
        if value is Ellipsis:
            return '...(snip)...'

        return None


def main():
    obj = MyClass()
    print(obj.mymethod(...))


if __name__ == '__main__':
    main()

これを、先ほどと同じように ellipsis.py という名前で保存する。 まずは Python 3.x で動かしてみよう。

$ python --version
Python 3.5.1
$ python ellipsis.py
...(snip)...

ちゃんと動いた。

それに対し Python 2.x だと、どうなるだろうか。

$ python --version
Python 2.7.10
$ python ellipsis.py
  File "ellipsis.py", line 16
    print(obj.greeting(...))
                       ^
SyntaxError: invalid syntax

こちらは文法エラーになってしまった。 通常のメソッドの呼び出しに Ellipsis は渡すことができないらしい。

まとめ

  • Ellipsis は「...」を表す特殊な定数
  • Python 2 と 3 では挙動が異なる
  • Python 3 では「...」が Ellipsis オブジェクトになっている
  • そのため、通常のメソッドの引数などにも Ellipsis が使える
  • それに対し Python 2 では拡張スライス文でしか使えない

Python: 環境ごとの依存ライブラリをセットアップスクリプトの extras_require で管理する

Python のパッケージを作っていると、特定の環境だけで必要となるパッケージが大抵はでてくる。 例えばデータベースを扱うアプリケーションなら、使う RDBMS によってデータベースドライバのパッケージが異なる。 あるいは、インストール先の Python のバージョンによっては標準ライブラリに用意されていないパッケージのバックポート版をインストールしなきゃいけない。 今回は、そんなときに便利なセットアップスクリプト (setup.py) の extras_require 引数を使ってみる。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.5
BuildVersion:   15F34
$ python --version
Python 3.5.1

下準備

まず最初に題材とするのはデータベースを扱うアプリケーションにしよう。 前述した通り、この状況では使う RDBMS によって異なるデータベースドライバをインストールしなきゃいけない。 今回は RDBMS に MySQL と Postgresql を使い分ける状況を想定しよう。

データベースドライバをビルドするために MySQL と Postgresql をインストールしておく。

$ brew install mysql postgresql

次に、題材とするアプリケーション本体のソースコード。 これには SQLAlchemy を使ってモデルを定義したモジュールを mydbapp という名前で保存しておく。 ただし、今回これはあくまで単なる例に過ぎないので実際に動かしたりすることはない。

$ cat < 'EOF' > mydbapp.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sqlalchemy.ext.declarative.api import declarative_base
from sqlalchemy.sql.schema import Column
from sqlalchemy.sql.sqltypes import BigInteger
from sqlalchemy.sql.sqltypes import Text


Base = declarative_base()


class User(Base):
    """データベースのスキーマの元になるモデル"""
    __tablename__ = 'users'

    id = Column(BigInteger, primary_key=True)
    name = Column(Text, nullable=False)
EOF

サンプルコードにセットアップスクリプトを書く

それでは、今回の本題となるセットアップスクリプト (setup.py) を書いてみることにする。

アプリケーションが共通で必要とするパッケージについては通常どおり install_requires に記述しよう。 今回においては SQLAlchemy がこれに当たる。 そして、環境に依存するデータベースドライバは extras_require に辞書の形で渡す。 辞書のキーは環境の名前で、バリューにはパッケージの入ったリストを指定することになる。 今回であれば mysql には mysqlclient を、そして postgresql には psycopg2 を指定している。

$ cat << 'EOF' > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup


def main():
    setup(
        name='mydbapp',
        version='0.0.1',
        zip_safe=False,
        py_modules=['mydbapp'],
        install_requires=[
            # どのような環境でも SQLAlchemy は必要になる
            'SQLAlchemy',
        ],
        extras_require={
            # 使う RDBMS ごとに、それ専用のドライバが必要になる
            'mysql': ['mysqlclient'],
            'postgresql': ['psycopg2'],
        },
    )


if __name__ == '__main__':
    main()
EOF

これで mydbapp モジュールをインストールできるようになった。

環境を指定してインストールする

pip install サブコマンドでは setup.py のあるディレクトリを指定することで、そのパッケージ (モジュール) をインストールできる。 このとき extras_require を使ったものであれば、角括弧で環境を指定する。

それでは、環境として mysql を指定してインストールしてみよう。

$ pip install .[mysql]
...(省略)...
Running setup.py install for mydbapp ... done
Successfully installed SQLAlchemy-1.0.13 mydbapp-0.0.1 mysqlclient-1.3.7

インストールされたパッケージを確認すると SQLAlchemy や mydbapp に混じって mysqlclient が見つかる。

$ pip list
mydbapp (0.0.1)
mysqlclient (1.3.7)
pip (8.1.2)
setuptools (23.0.0)
SQLAlchemy (1.0.13)
wheel (0.29.0)

同じように postgresql を指定したときはどうなるだろうか。

$ pip install .[postgresql]
...(省略)...
Running setup.py install for mydbapp ... done
Successfully installed SQLAlchemy-1.0.13 mydbapp-0.0.1 psycopg2-2.6.1

先ほどとは異なり psycopg2 がインストールされている。 ちなみに Python の仮想環境は作りなおしている。

$ pip list
mydbapp (0.0.1)
pip (8.1.2)
psycopg2 (2.6.1)
setuptools (23.0.0)
SQLAlchemy (1.0.13)
wheel (0.29.0)

もちろん、環境の指定は pip install 以外のサブコマンドにも有効になっている。 例えば Wheel をビルドするときも指定すれば環境ごとの内容になる。

$ pip wheel .[mysql]
$ ls | grep whl$
SQLAlchemy-1.0.13-cp35-cp35m-macosx_10_11_x86_64.whl
mydbapp-0.0.1-py3-none-any.whl
mysqlclient-1.3.7-cp35-cp35m-macosx_10_11_x86_64.whl

Python のバージョンごとに依存ライブラリを切り替える

extras_require には、環境の名前を指定してインストールする以外にも便利な使い方がある。 例えば Python のバージョンごとにインストールする依存ライブラリを切り替えることができる。

それでは、例としてアプリケーションが ipaddress モジュールに依存している場合を考えてみよう。 ipaddress モジュールは Python 3.3 で新たに標準ライブラリの仲間入りを果たしたモジュールだ。 つまり、それ以前のバージョンでは使うことができない。 ただし、バックポート版を PyPI からダウンロードしてインストールすることはできる。

次のセットアップスクリプトでは Python のバージョンが 3.3 未満のときだけ ipaddress モジュールをインストールするようにしよう。 これには「:python_version<"3.3"」といった書式で extras_require のキーを指定する。

$ cat << 'EOF' > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup


def main():
    setup(
        name='mydbapp',
        version='0.0.1',
        zip_safe=False,
        py_modules=['mydbapp'],
        install_requires=[
            # どのような環境でも SQLAlchemy は必要になる
            'SQLAlchemy',
        ],
        extras_require={
            # 使う RDBMS ごとに、それ専用のドライバが必要になる
            'mysql': ['mysqlclient'],
            'postgresql': ['psycopg2'],
            # Python 3.3 未満には ipaddress が標準ライブラリにない
            ':python_version<"3.3"': [
                'ipaddress',
            ],
        },
    )


if __name__ == '__main__':
    main()
EOF

それでは、上記を Python 2.7 の環境にインストールしてみよう。

$ python --version
Python 2.7.10
$ pip install .
...(省略)...
Running setup.py install for mydbapp ... done
Successfully installed SQLAlchemy-1.0.13 ipaddress-1.0.16 mydbapp-0.0.1

ipaddress モジュールがインストールされていることがわかる。

$ pip list
ipaddress (1.0.16)
mydbapp (0.0.1)
pip (8.1.2)
setuptools (23.0.0)
SQLAlchemy (1.0.13)
wheel (0.29.0)

次に Python 3.5 にもインストールしてみる。

$ python --version
Python 3.5.1
$ pip install .
...(省略)...
Running setup.py install for mydbapp ... done
Successfully installed SQLAlchemy-1.0.13 mydbapp-0.0.1

今度は ipaddress モジュールはインストールされていない!

$ pip list
mydbapp (0.0.1)
pip (8.1.2)
setuptools (23.0.0)
SQLAlchemy (1.0.13)
wheel (0.29.0)

ちなみに、上記で登場したバージョンの指定方法は PEP 426 という仕様で規定されているらしい。 具体的には、その中の Environment Markers だ。

Environment Markers については、次のブログ記事が詳しかった。 ちなみに、システムのプラットフォーム (Linux だとか Windows だとか) やアーキテクチャ (i386 や x86_64) まで判定できるようだ。

2014/07/10 PEP-0426 Environment Markers の調査 - 清水川Web

Python: (今のところ) Flask で Request#get_data(as_text=True) は使わない方が良い

今回は最近見つけた Flask (正確には、その中で使われている WSGI ツールキットの Werkzeug) のバグについて。 先にざっくりと概要を説明しておくと Flask の Request#get_data() の引数として as_text=True を渡したときの挙動に問題がある。 このメソッドは Content-Type に含まれる charset 指定にもとづいてマルチバイト文字をデコードできない。 デコードに使われる文字コードが UTF-8 に固定されてしまっているため、それ以外の文字コードを扱うことができない。

このエントリでは、上記の問題について詳しく見ていくことにする。

今回使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.5
BuildVersion:   15F34
$ python --version
Python 3.5.1
$ echo $LANG
ja_JP.UTF-8

下準備

まずは Flask をインストールしておく。

$ pip install Flask
$ pip list | grep -i flask
Flask (0.11.1)

Request#charset メンバについて

Flask (正確には Werkzeug) の Request オブジェクトには charset というメンバがある。 これは、おそらくは Content-Body をエンコードした文字コードを格納するためのものだろう。 ただし、「おそらく」と言ったように、実際にはそのようには動作しない。 このメンバの値は、今のところ UTF-8 に固定されてしまっているためだ。

動作を確認するために、次のようなサンプルコードを用意しよう。 このサンプルコードでは Request#charset の内容をレスポンスとして返す。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
from flask import request

app = Flask(__name__)


@app.route('/', methods=['POST'])
def post():
   return request.charset

上記のサンプルコードを実行する。 Flask v0.11 からは Flask のテストサーバの推奨される起動方法が少し変わった。

$ export FLASK_APP=charset.py
$ flask run
 * Serving Flask app "charset"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

まずは Content-Type に charset の指定がないとき。 これは「utf-8」になる。 まあ、猫も杓子も UTF-8 の昨今、これは特に違和感のない挙動だと思う。

$ curl -X POST -H "Content-Type: text/plain" -d "こんにちは" http://localhost:5000
utf-8

次に UTF-8 以外の文字コードを扱ってみることにしよう。 今回は EUC-JP を使うことにして、それでエンコードされたテキストファイルを用意する。 先ほどのサンプルコードでは Content-Body の内容を読みこんだりはしないけど、一応ね。

$ cat << 'EOF' > greeting.txt
こんにちは
EOF
$ nkf -e greeting.txt > greeting.euc.txt
$ nkf --guess greeting.euc.txt
EUC-JP (LF)

ちなみに Mac OS X に nkf はデフォルトではインストールされていないので Homebrew でインストールしよう。

$ brew install nkf

次は Content-Type に charset をつけてリクエストする。 Content-Body も、それに合わせて EUC-JP でエンコードされたテキストファイルを使って送る。

$ curl -X POST -H "Content-Type: text/plain; charset=EUC-JP" -d @greeting.euc.txt http://localhost:5000
utf-8

Content-Type の charset で EUC-JP と指定しているんだけど utf-8 になってしまっている。

このように Flask (正確には Werkzeug) の Request#charset は、今のところ正しく動作しない。

MIMETYPE の文字コードは何処に格納されるのか

じゃあ Flask では Content-Type で指定された文字コードを正しく扱うことはできないのか?というと、そうではない。 実は Request#mimetype_params という辞書の中に入っている。

これもサンプルコードを用意して、動作を確認してみよう。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
from flask import request

app = Flask(__name__)


@app.route('/', methods=['POST'])
def post():
   return str(request.mimetype_params.get('charset'))

そしてアプリケーションを起動する。

$ export FLASK_APP=mimetype.py
$ flask run
 * Serving Flask app "mimetype"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

先ほどと同じように EUC-JP を指定したリクエストを送ってみよう。

$ curl -X POST -H "Content-Type: text/plain; charset=EUC-JP" -d @greeting.euc.txt http://localhost:5000
EUC-JP

今度はちゃんと EUC-JP になっている!

ちなみに、この値にはデフォルト値が入るわけではない。 指定がないときは空になっている。

$ curl -X POST -H "Content-Type: text/plain" -d @greeting.euc.txt http://localhost:5000
None

Request#get_data(as_text=True) への副作用

先ほどの Request#charset が UTF-8 で固定される問題は、別のメソッドにも影響を与えている。 それが、今回のエントリのタイトルにもなっている Request#get_data() メソッドだ。 このメソッドはリクエストから Content-Body として送られたデータを取り出すためのメソッドになっている。

この Request#get_data() というメソッドには as_text という引数がある。 これは Content-Body をデコードした内容を受け取るためのオプションで、デフォルトでは False になっている。 つまり、デフォルトでは Request#get_data() を実行したとき得られるものはバイト列 (bytes) ということになる。 そして、この引数を True にすると、バイト列をデコードしたユニコード文字列 (Python3: str, Python2: unicode) になる。

問題は、この as_text オプションを True にしたとき使われる文字コードだ。 ここまで語ってきたように Request#charset は utf-8 に固定されてしまっている。 だから、このメンバにもとづいてデコードしているとアウトなんだけど、今 (v0.11.10) の Wekzeug は見事にそれをやってしまっている。

github.com

この挙動を確認するため、次のようなサンプルコードを用意しよう。 このサンプルコードでは Request#get_data(as_text=True) で取得した内容を、そのままレスポンスとして返す。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
from flask import request

app = Flask(__name__)


@app.route('/', methods=['POST'])
def post():
    return request.get_data(as_text=True)

上記を実行する。

$ export FLASK_APP=getdata1.py
$ flask run
 * Serving Flask app "getdata1"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

先ほどと同じように EUC-JP を含むリクエストを送ってみよう。

$ curl -X POST -H "Content-Type: text/plain; charset=EUC-JP" -d @greeting.euc.txt http://localhost:5000
����ˤ���

文字化けしてしまった…。

何が起こったのか

これはつまり、以下のようなことが起こっている。

[EUC-JP 文字列] -> (UTF-8 デコード) -> [Python ユニコード文字列] -> (UTF-8 エンコード) -> [UTF-8 文字列 (文字化け)]

HTTP クライアントから送られてきた EUC-JP のバイト列を UTF-8 でデコードしてしまっているのが間違い。 結果的にめちゃくちゃなユニコード文字列が生成されて、それをエンコードしたところで文字化けしてしまう、という寸法だ。

じゃあ、どうすればいいのか

Wekzeug のバグが修正されるまではワークアラウドでしのぐしかない。 Werkzeug 任せにすると、リクエストに含まれるバイト列が UTF-8 固定でデコードされてしまうのが根本的な原因だ。 つまり、デコードを自分でやれば問題は起きなくなる。

次のサンプルコードを見てほしい。 このコードではリクエストからバイト列でデータを取り出した上で、それを自分でデコードしている。 デコードに使う文字コードは Request#mimetype_params に入っている値で、それがなければ UTF-8 を使うようにした。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
from flask import request

app = Flask(__name__)


@app.route('/', methods=['POST'])
def post():
    data = request.get_data()
    charset = request.mimetype_params.get('charset') or 'UTF-8'
    return data.decode(charset, 'replace')

実行してみる。

$ export FLASK_APP=getdata2.py
$ flask run
 * Serving Flask app "getdata2"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

今度も同じように EUC-JP を含むリクエストを送ってみよう。

$ curl -X POST -H "Content-Type: text/plain; charset=EUC-JP" -d @greeting.euc.txt http://localhost:5000
こんにちは

今度は文字化けしていない!

今度は、次のようなことが起こっている。

[EUC-JP 文字列] -> (EUC-JP デコード) -> [Python ユニコード文字列] -> (UTF-8 エンコード) -> [UTF-8 文字列]

EUC-JP 文字列が正しい文字コードでデコードされて、本来のユニコード文字列になっている。 それを UTF-8 でエンコードしてレスポンスとして返した。 そして、ターミナルの文字コードも UTF-8 なので文字化けは起きない。

実は、このやり方は Flask の Request#get_json() を真似している。 このメソッドでも、同じように Request#mimetype_params に入っている charset にもとづいてデコードしているからだ。 つまり Request#get_json() はバグっていない。 github.com

最近は猫も杓子も JSON だし、UTF-8 以外の文字コードを使う機会も少ないから今回のバグを踏む人は少ないのかもしれない。 とはいえ、こういう問題があるので Flask のアプリケーションでマルチバイト文字を扱うときは注意しよう。

ちなみに

この不具合については Wekzeug にバグレポートした。 将来的には、いつか直るかもしれない。

Request#get_data(as_text=True) does not work with Content-Type/charset · Issue #947 · pallets/werkzeug · GitHub