読者です 読者をやめる 読者になる 読者になる

静かなる名辞

pythonと読書

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

ctypes python

※注意! この記事はネタ記事です。この記事に書いてある方法で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