【python】pythonでもprintf/scanfしたい!
最終更新:2018-04-10
※注意! この記事はネタ記事です。この記事に書いてある方法でpythonからprintf/scanfを使うことはできますが、実用性は保証しません。
実用的な方法を知りたい方は、こちらの記事にアクセスしてください。
【python】pythonでprintf的なことをする - 静かなる名辞
【python】pythonでscanf的なことをする - 静かなる名辞
C言語でプログラミングの基礎を勉強した人がpythonにやってきて初めに抱く不満は「文字列処理面倒くさ」ではないでしょうか。pythonにはpythonの文字列フォーマットがありますし、正規表現も標準で使えるので慣れれば快適な環境という考え方もありますが、Cの書記指定に郷愁を抱く方もいるでしょう(今時そんな奴いるのか? というツッコミはなしで)。
幸い、pythonには標準でctypesというモジュールがあり、これを使えばDLLを読み出せます。なので、いつもCで使っていたprintf/scanfをpythonから呼び出してやることにしましょう。
とりあえず練習としてputsを使ってみます。
>>> from ctypes import * >>> libc = CDLL("libc.so.6") >>> libc.puts("hello world".encode()) #よくわからないけどエンコードしないと駄目でした hello world 12
死ぬほど簡単ですね。hello worldの後に表示されている12はいつもの関数返り値がREPLに出てるだけです。hello worldはDLLが直接標準出力に書いてます(るはず)。「libc.so.6ってなに?」って人は諦めてpythonの文字列フォーマットを覚えましょう。
ところで、日本語は大丈夫なんでしょうか。
>>> libc.puts("ほげほげ".encode()) ほげほげ 13
とりあえず動いてるんだと思いますが、この辺は環境によってシビアな場合もあるので、なんとも言えません。とりあえず私のlinuxは大丈夫みたいです(たぶん。特定文字だけ化けるみたいな嫌過ぎるアレがなければ)。
いけそうなので、printfしてみましょう。
>>> libc.printf("%s%d\n".encode(),"ほげほげ".encode(), 1234) ほげほげ1234 17
死ぬほど簡単ですね。これなら特に悩むこともなく使えそうです。ところで、小数を与えたら?
>>> libc.printf("%s%f\n".encode(),"ほげほげ".encode(), 12.34) Traceback (most recent call last): File "<stdin>", line 1, in <module> ctypes.ArgumentError: argument 3: <class 'TypeError'>: Don't know how to convert parameter 3
こうなりました。実はctypesでは基本的に、pythonの型を「Cの型をラップしたオブジェクト」に変換してからCの関数に渡す必要があります。ただし「文字列と整数(とNone)は頑張って自動的キャストする」という仕様になっているので、これまでエラーが出てこなかっただけです。
こういう場合は、大人しく型を書きましょう。簡単です。
>>> libc.printf("%s%f\n".encode(),"ほげほげ".encode(), c_double(12.34)) ほげほげ12.340000 22
使える型の一覧とかは公式ドキュメント見てください。
16.16. ctypes — Pythonのための外部関数ライブラリ — Python 3.5.4 ドキュメント
printfはこれで何とか使えそうですが、次はscanfを使わないといけませんね。scanfは周知の通り、標準入力から読み込んだ文字列を書式に従って解釈し、結果をポインタに格納します。なんかポインタとかやばそうです。そもそも使えるんでしょうか。
結論から言うと、使えます。つーか、ドキュメントに使えるって書いてあります。
どれどれ
>>> i = c_int() >>> libc.scanf("%d".encode(), byref(i)) 0 >>> i c_int(0)
これを実行すると、入力待ちにならないで速攻終わってくれます。そして値が入っていてほしかったiの中身は、初期化された状態のままの0です。駄目じゃん。駄目な理由はわからないようななんとなくわかるような雰囲気ですが、とりあえずちゃんと調べて対処するのは面倒くさいのでパス。動いてないのは標準入力だと思うので、sscanfを使ってみます(ドキュメントにもそっちが書いてあるし)。
>>> i = c_int() >>> libc.sscanf("1234".encode(), "%d".encode(), byref(i)) 1 >>> i c_int(1234)
これは期待通りですね。ちなみに、c_intからpythonの型の値を取り出すには、
>>> i.value
1234
で良いようです。
これができるなら後は簡単で、
>>> s = input() 1234 >>> libc.sscanf(s.encode(), "%d".encode(), byref(i)) 1 >>> i.value 1234
と動かすことができ、scanfと同じことができます。やったねたえちゃん、書式指定が使えるよ!
まとめ
こんなの使うより正規表現とか覚えるべき。あとはsscanf風(というかpythonのformatの逆みたいな感じ)に文字列を切り出せるparseってパッケージもあるらしいです。pipで入ります。
parse · PyPI
【python】multiprocessingはアホみたいにメモリ食うよって話
それなりに大きい(それでも数GBとかそんなもん)データをmultiprocessingで処理しようとしたら、メモリが溢れて大変だった。その原因と対処法について書いておく。
multiprocessingはプロセス間でメモリを共有しない。ということは、処理するデータは一々プロセスにコピーされる。3GBのデータを4つのプロセスで並列処理しようとしたら、あっという間に12GB+親プロセスの3GBで15GB埋まる。
いや、それどころか、プロセス間のデータ転送アルゴリズムがpickleなせいで、瞬間的に転送されたデータ+そのpickleが存在することになり、そしてpickleはお世辞にも省容量とは言い難いデータ構造をしている。つまりpickleの分もメモリは消費されると見ておいた方が良い。
こういうことは理解して正しく使えば問題にならないんだけど(つまりメモリ消費が大きくなりそうなときはmultiprocessingはやめとけば良い。大して時間がかからないなら諦めて1コアで実行するか、可能ならcython等に逃げる)、「ヒャッハー! お手軽並列処理だぜ!」な気分で使ってた自分は割と酷い目に合った。反省。
追記
対策記事を書いた。使い方を工夫すればメモリ消費は抑えられるので、うまく使うことが肝心だと思った。
multiprocessing.Poolがやたらメモリを消費するときの対策 - 静かなる名辞
【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通り考えられる。
- 任意のオブジェクトidを指定して新規オブジェクトを生成する
- 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()
使い道は特に思いつかないけど、参照渡しにするためだけにリストオブジェクトを生成するのもちょっと不条理な感じでクール? いや、そんなことはないですね。
【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は直感と違う挙動をするので、メモしておきます。
問題のコード
このようなコードです。
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"])
こうやって呼び出すと、
こんな結果が表示される。あとはmatplotlibなんで、好きにいじれば(文字大きくするとかタイトル/軸ラベル付けるとか)良いと思う。
confusion_matrixを呼ぶとき、明示的にラベルを渡して(sklearnのドキュメントにもなんか正解ラベル+予測ラベルから重複取り除いてソートして使うみたいに書いてあるから、この場合は不要かもしれないけど。自分好みの順番で出力したいときは必要)、そのラベルをpandasデータフレームに渡すindex,columnsに使いまわすのがミソ。