静かなる名辞

pythonと読書

【python】pythonでもprintf/scanfしたい!

※注意! この記事はネタ記事です。この記事に書いてある方法でpythonからprintf/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.2 ドキュメント

 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で入ります。
pypi.python.org

おすすめのテキストエディタ

 テキストエディタと言っても、vimとかemacs秀丸だのサクラだの、Sublime TextだのAtomだのの話をしたいのではない。そりゃ、真面目にソースコードだの文章だのを編集するなら大掛かりなエディタだって必要かもしれないけど、普段ちょっとしたテキストファイルを開きたいとき、そんなでかいものは要らない(vimは小さいけど)。だけどwindowsのメモ帳とか、geditあたりはいまいち納得がいかない。そんなとき用のエディタ。

GreenPad - kMonos.NET

 GreenPad、とても使いやすいです。

【python】multiprocessingはアホみたいにメモリ食うよって話

 タイトルで落ちてるんだけど、それなりに大きい(それでも数GBとかそんなもん)データをmultiprocessingで処理しようとしたら、メモリが溢れて大変だった。
 multiprocessingはプロセス間でメモリを共有しない。ということは、必然的にそうなるしかないのだが、処理するデータは一々プロセスにコピーされる。3GBのデータを4つのプロセスで並列処理しようとしたら、あっという間に12GB+親プロセスの3GBで15GB埋まる。それどころか、プロセス間のデータ転送アルゴリズムがpickleなせいで、瞬間的に転送されたデータ+そのpickleが存在することになり、そしてpickleはお世辞にも省容量とは言い難いデータ構造をしている。つまりpickleの分もメモリは消費されると見ておいた方が良い。
 こういうことは理解して正しく使えば問題にならないんだけど(つまりメモリ消費が大きくなりそうなときはmultiprocessingはやめとけば良い。大して時間がかからないなら諦めて1コアで実行するか、可能ならcython等に逃げる)、「ヒャッハー! お手軽並列処理だぜ!」な気分で使ってた自分は割と酷い目に合った。反省。

【python】sklearnの次元削減クラスSelectPercentileとSelectKBestについて

 ネットの巷には「sklearnで次元削減するのでとりあえずPCA使ってみました」という記事が溢れかえっているのだが、PCAは基本的に線形な世界でしか動かないので、扱うデータによっては「PCAかけてから分類器に入れたら精度がガタっと落ちました」みたいなことが起こり得る。こういう問題に対してはカーネルPCAという手法があり、SVMよろしくカーネル法を使って上手いこと計算してくれるらしい。また、非線形データをなんとかするアルゴリズムは他にもあるようだ。だけど、そんなややこしいもの使いたくないよね。よくわからないし。
 そんな駄目人間でも使える次元削減アルゴリズム(厳密には特徴選択と言うべきなんだけど)が、sklearnの提供するSelectPercentileとSelectKBestである。
 両者は共に、「次元ごとの効き目(分類するときどれくらい有益な情報を持っているか)」を算出し、効き目の高い次元を残すという動作をする。PCAみたいに次元を混ぜ合わせたりは特にしない(要するに分類に効く情報が入った次元とノイズしかない次元があるとき、前者だけを取り出す)。あと、基本的には是が非でも分類に使える情報を取ってくる奴なんで、教師なしでデータをクラスタリングして可視化したいみたいな状況で使うのは考え直した方が良いと思う。この辺の細かいところは公式ドキュメントを読んでください。

 使い方は超簡単。
 まずSelectPercentileの場合。

from sklearn.feature_selection import SelectPercentile

...

sp = SelectPercentile(k=50) #こう書くと次元数が半分になる
vectors = sp.fit_transform(vectors, labels) 

 次にSelectKBest。こっちも大して変わらない。

from sklearn.feature_selection import SelectKBest

...

skb = SelectKBest(k=250) #こう書くと上位250次元を取ってくる
vectors = skb.fit_transform(vectors, labels)

 特に頭を使う要素がなくて助かってます。

 ただ、実際にこのまま使うことはちょっとできない。この手法は正解ラベルを必要とする。要するに、データをテストデータと訓練データに分けてクロスバリデーションやるんだとか言ってるときに、特徴抽出の段階でデータ全体の正解ラベルを持ってきて貼り付けて次元削減に使っちゃうと、だいぶセコいんじゃね? と思うのである。

 なのでこういうときは、こういうもの(↓)を書いて、

class SelectClassifier:
    """
    欲しいメソッドとかあったら自分で追加してね!
    """
    def __init__(self, clf, selector="p", k=10):
        self.clf = clf
        if selector == "p":
            self.selector = SelectPercentile(k=k)
        elif selector == "kb":
            self.selector = SelectKBest(k=k)
        else:
            sys.exit("aho")
        self.k = k

    def fit(self, vectors, labels):
        if vectors.shape[1] > self.k:
            self.selector.fit(vectors, labels)
            vectors = self.selector.transform(vectors)
        self.clf.fit(vectors, labels)

    def predict(self, vectors):
        if vectors.shape[1] > self.k:
            vectors = self.selector.transform(vectors)
        return self.clf.predict(vectors)

 そしてこうやって使ってやる。

clf = SVC() #たとえば
clf = SelectClassifier(clf, "p", k=10)

 訓練データの正解ラベルを使う分には、文句を言われる筋合いはないはず。
 え、なんでわざわざベクトルの次元数がkの値より大きいことを確認する処理なんか書くのって? SelectKBestは指定したkより小さい次元数のベクトルを渡すと例外吐いて止まるんである。そんなの素通りさせてくれれば良いでしょ、と100回思ったけど、どうにもならないので愚直に書いた。この処理だとSelectPercentileで100次元以下のデータを扱うとき、次元数とkの値の兼ね合いで次元削減が実行されない可能性がある。けど、それまで真面目に対応する気力はなかった。

 これの使い所としては、たとえば「ン万次元あるけど、これ大体ノイズだよね」みたいなときに使えば良いでしょう。自然言語処理のBoWなんかを相手にするときは良さそうですね。PCAみたいに軸を混ぜ合わせる訳ではないので、「高次元のデータを低次元で上手く表現する」用途には使えないです。 

参考文献

sklearn.feature_selection.SelectPercentile — scikit-learn 0.18.1 documentation
sklearn.feature_selection.SelectKBest — scikit-learn 0.18.1 documentation
 具体的にどういう処理をして有用な次元/そうでない次元を決めているのかという話も載っています。

【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()

 使い道は特に思いつかないけど、参照渡しにするためだけにリストオブジェクトを生成するのも不条理感があって中々良いかもしれないと思えるようにはなった。

【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の使用メモリが載っちゃってるか、それとも一千万個も生成した整数オブジェクトをぜんぶ回収しないうちに次の処理が進んでるんだろうか。そこに関しては詳しくないので謎。

【python】random.shuffleについて

問題のコード

 こういう奴。

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

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

正しいやり方
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はいわゆる「破壊的操作」で、リストの参照を受け取って(pythonのリストは参照渡しだからね)、その参照先をシャッフルします。Cかよ。
 ともかく、こいつは関数じゃないのです。pythonがうたう『マルチパラダイム言語』の中にはオールドファッションな手続き型も入ってると思うんで、こういうものが標準ライブラリの中にいても怒ってはいけません。僕は思わず画面に湯呑みを投げつけそうになりました。

でも元のリスト破壊されたら困るときもあるじゃん

 random.shuffleは参照先をそのままいじるので、元々どんな順番で要素が並んでいたのかという情報は残してくれません。元のリストもとっておきたいときは自前でやる必要があります。
 色々なやり方があると思いますが、とりあえずlist()しとけば別オブジェクトになるのでそれで良いんじゃないかな。

import random
lst = list(range(10))
shuffled = list(lst); 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

"""

 ひとつづきの処理であることを強調するために、セミコロン使って同じ行に入れてみました。ちなみに、random.shuffle(list(lst))とかすると、random.shuffleさんはちゃんと健気に動いてくれるものの、結果を取り出す手段がなくなって死にます(オブジェクトが)。