静かなる名辞

pythonとプログラミングのこと


【python】 immutableを参照渡ししたい

 pythonの参照渡しスタイル(を積極的に利用するコーディング)にケチ付ける記事を書いたんだけど、しばらく経ってから「逆にpythonでCみたいなバリバリの参照渡しするにはどうしたら良いんだ?」という疑問を持った。
 たとえば、Cで言うところのこういうもの。

#include<stdio.h>

void time_ptr(int *ptr, int n){
  *ptr = n*(*ptr);
}

int main(){
  int a;
  a = 3;

  printf("%d\n", a);
  time_ptr(&a, 3);
  printf("%d\n", a);

  return 0;
}

 有名な話だけど、pythonの世界にはmutable(変更可能)なオブジェクトとimmutable(変更不能)なオブジェクトがあり、前者にはリストや辞書、後者にはタプルや文字列、整数などが含まれる。mutableなオブジェクトに対する操作(list.append()とか)は元のオブジェクトを変化させるが、immutableなオブジェクトに対する操作は常に新しいオブジェクトを生成する。そしてpythonのルールとして、すべてのオブジェクトは参照渡しされる。

 こうして考えると、mutableなオブジェクトを参照渡しにするのは難しくない(むしろ容易だ。意図せず参照先をいじってしまい、ロジックエラーになって困るくらい)。一方、immutableなオブジェクトを参照渡しして、その値を書き換えたい(そんなシチュエーションがあるの? という疑問は黙殺するとして)場合、かなり困ったことになる。当たり前だ。なにせ、変更不能なのだから。

 この問題(というほどじゃないけど)の解決策は、以下の2通り考えられる。

  1. 任意のオブジェクトidを指定して新規オブジェクトを生成する
  2. mutableの中に入れて渡してやる

 ほんとうは1が実現できたら、面白いと思ったけど。残念ながらpythonで実現する方法は調べても出てこなかった(恐らくそんなものは存在しないと思う。あったら危なっかしくてしょうがないし)。なので素直に2を使うことにする。

 上のCのコードは、こう書ける。

def time_ref(ptr, n):
    ptr[0] *= n

def main():
    a = [3]
    print(a[0])
    time_ref(a, 3)
    print(a[0])

if __name__ == '__main__':
    main()

 使い道は特に思いつかないけど、参照渡しにするためだけにリストオブジェクトを生成するのもちょっと不条理な感じでクール? いや、そんなことはないですね。

2018/11/26 追記

 その後勉強し、そもそも参照渡しという発想で捉えるのが間違っていたという結論に達しました。

 こちらの記事を御覧ください。

共有渡しと参照の値渡しと - 静かなる名辞

【python】メモリ上のオブジェクトを是が非でも圧縮したい

 でかいデータをなにも考えずメモリ上に置いておくと、あっという間にメモリが埋まる。

 不要なデータはこまめに消して、必要なときに必要なものだけメモリに置くようにすれば大抵なんとかなるのだけど、そうやって整理していくと、ある水準を超えたところで処理時間とかコードの可読性が問題になってしまう。

 こういう問題の解決策として、データを生成したあとは圧縮してメモリ上に置いておき、使うときに解凍して呼び出すという方法が考えられる。もちろん普通にできることではない。ないのだが、pythonだとpickleを介することでできなくはない。当然オーバーヘッドは大きいけど。

 こういうのは説明するよりソースを見せた方が早い。

import pickle
import bz2
PROTOCOL = pickle.HIGHEST_PROTOCOL
class ZpkObj:
    def __init__(self, obj):
        self.zpk_object = bz2.compress(pickle.dumps(obj, PROTOCOL), 9)        

    def load(self):
        return pickle.loads(bz2.decompress(self.zpk_object))

 こういうファイルを作ってパスの通ったとこに置いておく。pickle化・圧縮のプロトコルは気分で変えても良い。

 使い方も特に難しくはない。

from zpkobj import ZpkObj  #zpkobjというファイル名にした場合
...
obj = ZpkObj(obj) #圧縮するとき
...
obj = obj.load() #解凍するとき

 留意点としては、オリジナルのオブジェクトの参照がどこかに残っているとGCが動かないので期待したようなメモリ節約効果が得られないことが挙げられる。そういうときは手動で消す。

...
zobj1 = ZpkObj(obj1)
del obj1 #違う名前に代入するなら必須

 せっかくなので、memory_profilerでベンチマークを取ってみる。

mtest.py(ベンチマーク用コード)

# coding: UTF-8
from zpkobj import ZpkObj

@profile
def main():
    lst = list(range(10000000))
    zlst = ZpkObj(lst)
    del lst
    lst = zlst.load()
    del zlst

if __name__ == '__main__':
    main()

結果

$ python -m memory_profiler mtest.py
Filename: mtest.py

Line #    Mem usage    Increment   Line Contents
================================================
     4   28.066 MiB    0.000 MiB   @profile
     5                             def main():
     6  415.867 MiB  387.801 MiB       lst = list(range(10000000))
     7  422.105 MiB    6.238 MiB       zlst = ZpkObj(lst)
     8   34.645 MiB -387.461 MiB       del lst
     9  428.691 MiB  394.047 MiB       lst = zlst.load()
    10  423.742 MiB   -4.949 MiB       del zlst

 7~8行目を見ればわかるように、意図した通りオブジェクトのサイズを1/50以下に圧縮できている(圧縮の効きやすさはデータ依存なんで、すべてのオブジェクトのかさを1/50以下にできる訳ではないけど)。

 ところで、6行目と10行目で若干メモリ消費量が増えてるのはなんで? memory_profilerの使用メモリが載っちゃってるか、それとも一千万個も生成した整数オブジェクトをぜんぶ回収しないうちに次の処理が進んでるんだろうか。そこに関しては詳しくないので謎。

2018/11/26 追記

 この記事を書いたときからほぼ2年の歳月が過ぎ、古い記事を見直していた私はこう思った。

「joblibでできそう。わざわざ自分で書く意味なくね」

joblib.dump — joblib 0.13.0 documentation

 近日中に検証して記事にする予定。

【python】random.shuffleについて

はじめに

 標準モジュールのrandom.shuffleは直感と違う挙動をするので、メモしておきます。

 参考:
9.6. random — 擬似乱数を生成する — Python 3.6.5 ドキュメント

問題のコード

 このようなコードです。

import random
lst = list(range(10))
shuffled = random.shuffle(lst)
for x in shuffled:
    print(x)

 いかにも人畜無害に見えるコードですが、実行すると「TypeError: 'NoneType' object is not iterable」を吐いてくれます。

 どうやらrandom.shuffle()はNoneを返すらしい。どういうことだろう? ああ、なんかなんとなく想像はできるけど・・・。

正しいやり方

import random
lst = list(range(10))
random.shuffle(lst)
for x in lst:
    print(x)
""" =>
1
9
5
3
7
0
4
2
8
6
"""

 random.shuffleはいわゆる「破壊的操作」で、listの参照を受け取って(pythonのオブジェクトはいわゆる参照で渡されるから)、その参照先をシャッフルします。Cかよ。

 ともかく、こいつは副作用のない、お行儀の良い関数じゃないのです。pythonは『マルチパラダイム言語』なので、そしてその『マルチパラダイム』の中にはオールドファッションな手続き型も入ってると思うので、こういうものが標準ライブラリの中にあっても怒ってはいけません。

この挙動が嫌なとき

 random.shuffleは引数のオブジェクトを直接変更するので、シャッフル前の情報は残してくれません。

 元のリストも取っておきたいときは自前でやる必要があります。

 これにはいろいろな方法があります。

  • list(lst)でオブジェクトを新しく生成する
  • lst[:]のように空のスライスを使う
  • lst.copy()のようにlist.copy()メソッドを使う

 どれが優れているとかは特にないと思いますが、lst.copy()で書いてみることにします。

import random

lst = list(range(10))
shuffled = lst.copy()
random.shuffle(shuffled)

for x, y in zip(lst, shuffled):
    print(x, y)
""" =>
0 0
1 3
2 8
3 6
4 4
5 9
6 1
7 7
8 5
9 2

"""

 shuffled = lst.copy()の時点ではシャッフルされていないという気持ち悪さがある。変数名には改善の余地があります。

 ちなみに、random.shuffle(lst.copy())としてもちゃんと動きますが、動き終わった後に結果のlistがゴミ集めに回収されていくだけなので無意味です。

まとめ

 知っていればハマりませんが、知らないと戸惑います。

【python】混同行列(Confusion matrix)をヒートマップにして描画

 pythonでラクして混同行列を描画したい(sklearnとかpandasとかseabornとか使って)という話。

 そもそもscikit-learnにはsklearn.metrics.confusion_matrixなるメソッドがあって、混同行列がほしいときはこれ使えば解決じゃん、と思う訳だが、このconfusion_matrixは2次元のnumpy配列を返すだけで「あとはユーザーが自分で描画してね♪」というメソッド。

 なので、とりあえずコンソールに結果を吐かせて、混同行列(の値が入った2次元配列)を確認したあと、ちょっとどう料理してやるか悩む羽目になる。

 表形式で出すのはダサいし見づらいので、ヒートマップにしようというところまではそんなに迷わないと思う。で、pythonのヒートマップの作り方はぶっちゃけよくわからない(日本語資料があまりない)。とりあえずseabornというライブラリを使えば良いらしいんだけど……。

 
 日本語で「python 混同行列 ヒートマップ」みたいな検索をすると、ぶっちゃけ楽そうな(5,6行くらいで書ける)方法がほとんど出てこない(皆無?)のだけど、Stack Overflowにはあった。

python - How can I plot a confusion matrix? - Stack Overflow


 とりあえずやり方はわかったので、使いやすいように正解ラベルと予測ラベルを受け取って描画する関数をメモしておく(骨子だけ)。

import pandas as pd
import seaborn as sns
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt

def print_cmx(y_true, y_pred):
    labels = sorted(list(set(y_true)))
    cmx_data = confusion_matrix(y_true, y_pred, labels=labels)
    
    df_cmx = pd.DataFrame(cmx_data, index=labels, columns=labels)

    plt.figure(figsize = (10,7))
    sns.heatmap(df_cmx, annot=True)
    plt.show()

 これをたとえば、

print_cmx(["a","b","c","d","e"],["a","b","c","d","e"])

 こうやって呼び出すと、
f:id:hayataka2049:20161215224000p:plain
 こんな結果が表示される。あとはmatplotlibなんで、好きにいじれば(文字大きくするとかタイトル/軸ラベル付けるとか)良いと思う。

 confusion_matrixを呼ぶとき、明示的にラベルを渡して(sklearnのドキュメントにもなんか正解ラベル+予測ラベルから重複取り除いてソートして使うみたいに書いてあるから、この場合は不要かもしれないけど。自分好みの順番で出力したいときは必要)、そのラベルをpandasデータフレームに渡すindex,columnsに使いまわすのがミソ。

2019/12/15 追記

 sklearn v0.22でもっといいのができた。

www.haya-programming.com