PyQt4で画像を扱うアプリケーションをMac OS X上でバンドルする

MacPorts入れる

まっさらなMacに、XcodeMacPortsを導入する。
んで、

sudo port install python26 py26-pyqt4
sudo python_select python26

とすれば、開発環境が整う。かなりコンパイル時間がかかるので注意。

バンドルする

Mac OS Xで配布用バイナリ(.appのディレクトリ)にbundleするには、
py2appが定番のようだ。


んで、py2appでサックリ.appを作ってみたけど、画像の読み込みがうまくできない。
Qtのimageformatsプラグインの読み込みに失敗しているのであろう。


Mac OS Xの動的ライブラリには、dylibとbundleの2つの形式があるらしい。
Mach-Oの仕様で決まっているんだって。
dylibは共有ライブラリ、bundleは動的にロードできるモジュール、ということ。


dylibもdlopen()で開くことができるんだけど、dlclose()はできないっぽい。
bundleはdlopen()/dlclose()両方OK。
(本当は別のAPI群があるらしいけど、どうせラッパ関数しか使わないからねー)


Mac OS XでQtのimageformatsは、bundle形式で提供されている。
というわけで、data_filesにbundle形式のライブラリを突っ込めばおk、
なのかと思ったらうまくいかない。


py2appでバンドルした場合には、どうやらbundleの読み込みがうまくいかないようだ。
py2exeでbundle_filesを3以外に設定したときと同じようなエラーメッセージが表示される。


strace的なもので共有ライブラリのロードを調べたい。Macの場合にはktraceがそれにあたる。
もしくは、おそらくpy2appはpy2exeのbundle_filesが3以外の挙動と似ている可能性があるので、
py2exeのほうで問題を追ったほうが理解しやすいかもしれない。
なにせ、Macでの開発は経験が足りないからねー…


PyInstallerの場合にはもしかしたらimageformatsの読み込みに対応しているのかもしれない、
という希望があったりします。
対応していなくても、開発が動いているのであればパッチも送れるし。
そうすると、Windows版の.exe作成もPyInstallerでいいじゃん、という話になり、
結局cx_Freeze->py2exe->PyInstallerと全部試してしまうことになる悲しい結末です。
でも、PyInstallerってWindows上で使いづらいんだよねぇ…

cx_Freezeからpy2exeに移行作業したときのメモ

py2exeはencodings以下を手厚く入れてくれる。

cx_Freezeの場合、encodings以下のモジュールについて必要なものは、おのおの指定する必要があった。
py2exeはencodings以下をどっちゃり入れてくれる。

py2exeでPyQtで画像を扱う場合の注意

前エントリで紹介したように、PyQtで画像ファイルを扱っている場合には、
imageformats以下にあるQtのプラグインを配置しなければならない。
cx_FreezeでPyQtアプリを固めた時に詰まったところ - グニャラくんのグニャグニャPython備忘録


が、僕はココでかなり詰まった。
imageformats以下のファイルを配置しているのにロードしてくれないのだ!
しかも、コンソールには以下のようなエラーが大量発生する。んぺぺ!

QObject::moveToThread: Current thread(xxxxxxxx) is not the object's thread(xxxxxxxx).
Cannot move to target thread (xxxxxxxx)

長い間試行錯誤した挙句、原因が判明。
'bundle_files' = 3にすること!
あああああああ

py2exe実行時に、msvcp90.dllがねーよ!といわれる

C:\Python26に、msvcp90.dll/msvcm90.dll/msvcr90.dllの3つのファイルを放りこんだらpy2exeが実行できた。

py2exeで作ったバイナリが、他の環境で「初期化に失敗」だの「構成が正しくない」だのエラーを出す

Visual C++のCランタイムライブラリ(さっきのmsvcp90.dllみたいなの)のロードに失敗しているため。
手っ取り早いのは、以下のファイルをインストールすること。


しかし、僕も正直

Pythonをインストールするのが面倒なのでexeに変換したのに、わざわざ別のものインストールさせたら手間かわんねーじゃん?
http://d.hatena.ne.jp/AMENOHI/20090202/1233575464

ってな気分です。


cx_Freezeの場合には、msvcp90.dllを配布物の中に入れてくれます。
py2exeの場合には、Windowsの標準システムDLLかどうかを判定するロジックによって
依存DLLを配布するかしないかを判断する関数があり、
その関数による判定の結果、msvcp90.dllは配布されません。


というわけで、判定関数をのっとります。

origIsSystemDLL = py2exe.build_exe.isSystemDLL
def isSystemDLL(pathname):
  if os.path.basename(pathname).lower() in ('msvcp90.dll'):
    return 0
  return origIsSystemDLL(pathname)
py2exe.build_exe.isSystemDLL = isSystemDLL

これでmsvcp90.dllはdistディレクトリの中に放り込まれたのでした。わーい!


んでも、カレントディレクトリにmsvcp90.dllがあったからといって、
それが使われるわけじゃないらしいんですね。
Microsoft.VC90.CRT.manifest」ってファイルをexeのカレントディレクトリに放りこめばいいのかもしれないし、
setup()のconsole引数に、

{'other_resources': [(24, 1, manifest)]}

的なものを指定して、そのmanifest中でVCのランタイムを指定すればいいのかもしれない。
まだ調査できてないし理解も出来ていない。調査できたら追記すると思う。

cx_FreezeでPyQtアプリを固めた時に詰まったところ

cx_Freezeにて、setup.pyで設定を行い場合の注意

たとえば、以下のような要件があったとする。
sipモジュールを追加で読み込みたい。
・アプリケーションにアイコンを指定したい。

cx_Freezeのドキュメントでは、
以下のような書き方でOKなように読める。

import sys
from cx_Freeze import setup, Executable

base = None
if sys.platform == 'win32':
  base = 'Win32GUI'

exe = Executable(script = 'app.py',
                 includes = ['sip'],
                 icon = 'app.ico',
                 base = base)
setup(name = 'myapp',
      version = '0.0.1',
      description = 'my test application',
      executables = [exe],
)

でも、これだとNG。


以下の記法でうまくいった。

import sys
from cx_Freeze import setup, Executable

base = None
if sys.platform == 'win32':
  base = 'Win32GUI'

exe = Executable(script = 'app.py',
                 icon = 'app.ico',
                 base = base)

setup(name = 'myapp',
      version = '0.0.1',
      description = 'my test application',
      executables = [exe],
      options = {
        'build_exe': {
          'includes': ['sip'],
        },
      })

cx_Freezeで固めたバイナリでのみ、画像が表示できない

詳しくは、

を参照のこと。

たとえば、imageformats/qjpeg4.dllの配布も行いたい場合、
cx_Freezeのoptionsはこうなる。

      options = {
        'build_exe': {
          'includes': ['sip'],
          'include_files': [('imageformats/qjpeg4.dll',
                             'imageformats/qjpeg4.dll')],
        },
      }

僕はめんどうくさがりなので、
標準ライブラリパスであるアプリケーションディレクトリにimageformatsをコピーすることによって回避した。

cx_FreezeとPyQt4で生じる問題

cx_Freezeは、Windowsインストーラパッケージとして
msiファイルを生成する。
msiファイルを生成する際、Pythonのmsilibモジュールを使っているのだが、
複数個のドットが含まれるファイル名のパッケージングで不具合がある。


よって、PyQt4.QtCore.pydとPyQt4.QtGui.pydのパッケージングに問題が生じた。
上記issue1128に投稿されたパッチを適用して解決した。

cx_Freezeでbase = 'Win32GUI'にした場合に、sys.exit()でエラーダイアログが出る問題

項目名のとおりで、sys.exit(0)なんかを実行してしまうとエラーダイアログが出る。
cx_Freezeのsource/bases/Win32GUI.cを読んでみると、
SystemExit例外をハンドルしている部分でエラーダイアログが出ているようだ。

    PyErr_Fetch(&type, &value, &traceback);
..(snip)..
    if (PyInt_Check(value))
        exitCode = PyInt_AsLong(value);
    else {
        message = StringifyObject(value, &valueStr);
        MessageBox(NULL, message, "cx_Freeze: Application Terminated",
                MB_ICONERROR);
        Py_XDECREF(valueStr);
        exitCode = 1;
    }

んだもんで、試しに、

raise SystemExit, 0

としてもやっぱりエラーダイアログが出る。もう知らん!(不貞寝

まとめ

cx_Freezeは現在メンテされていない、とどこかのMLアーカイブで読んだし、
pyInstallerはsvn headを使えと言ってるし、
やはり素直にWindows版バイナリ生成はpy2exeを使うか…

PyQtでアプリを作る

PyQtでhtmlをごにょごにょするアプリを作りたい@Windows
今まではwxPythonを使っていたが、画面デザインツールがいまいち使いづらい。
Qtのデザインツールを使ってみたらよさげだったので、PyQtに移行しよう。
最近LGPL 2.1もライセンスの選択肢に入ったので、もはや避ける理由もないだろう。
(追記)PyQtそのものは、現在GPL/商用ライセンスのみ選べるようです。情報提供元は、http://d.hatena.ne.jp/methane/20090509/1241873274
SQLiteまで入っていてオトクですね。


htmlの解析は、BeautifulSoupを使うのも手だが、
今回の用途ではパース速度も重視したい。よってlxmlを使う。


以下のものをダウンロードする。

setuptoolsは、Windows向けPython 2.6用パッケージが存在しない。
よって、setuptools-0.6c9.tar.gzをダウンロードする。
解凍後、

c:\Python26\python setup.py bdist_wininst

的な感じでdist\setuptools-0.6c9.win32.exeが出来る。これをインストールする。


setuptoolsが入れば、lxmlはeasy_install.pyを使って導入できる。

c:\Python26\Scripts\easy_install.exe lxml

setuptoolsなしで、lxmlのパッケージlxml-2.2.win32-py2.6.exeを入れてもよい。
setuptoolsがあれば、他にも便利なパッケージを導入できるだろう。

PyQtで作ったアプリを配布する

WindowsのexeとしてPyQtのアプリを配布したい。
定番としてpy2exeがあるが、今回はcx_Freezeを用いる。
コイツはLinuxでも使える。

cx_FreezeでPyQtやlxmlを用いたスクリプトを配布する場合には気をつける必要がある。
なぜなら、cx_Freezeでは検知できないモジュールが実行のために必要となるからだ。
PyQtの場合はsip、lxmlの場合はlxml._elementpathなどを教えてあげる必要がある。
僕が作ったアプリの場合、gzipやencodings.utf_8・encodings.asciiも必要だった。


具体的には、以下のようなスクリプトで実行ファイル一式を得ることが出来た。

c:\Python26\Scripts\cxfreeze --include-modules="sip,lxml._elementpath,gzip,encodings.utf_8,encodings.ascii" application.py

Qt/PyQtのドキュメントを読む

PyQtは、ローカルのドキュメントツールがある。
QtやPyQtのドキュメントは以下のサイトでも読むことができる。

PyTC 0.5をリリースしました。

PyTC、ビルドできなくなっていたんですね…外国人の方に「ビルドできひん」というメールもらって初めて気づきました。

tchdb.hからいくつかの定数がなくなっていたので、それらを削除して新しい定数を加えたPyTC 0.4をリリースしました。さらに、addint/adddoubleを追加し、キーがない場合にはPyTCErrorの代わりにKeyErrorを出すようにしたPyTC 0.5をリリースしました。

addint/adddoubleについては、Pythonの整数/小数を渡して、足した結果の整数/小数が返ってきます。が、値の初期化や通常の取り出しはバイナリで行われるので、struct.pack/unpackを使ってください。具体的には、以下のようにしてください。これで、auto_incrementサーバ的なものが簡単にできますね。

    # 0で初期化
    db['int'] = struct.pack('i', 0)
    # 1を足して、足した結果を表示
    print db.addint('int', 1)
    # 結果を表示
    print struct.unpack('i', db['int'])[0]
    # 結果表示について、めんどい人はこうしてもOK
    print db.addint('int', 0)

[追記]BZIPオプションのサポート忘れてました

というわけで、BZIPオプションを付与したPyTC 0.6をリリース!

Python温泉 #4に参加中。

ゆるくて好きなイベント、Python温泉 #4に参加中。
お尻が蒸れているのだが、後ろにid:moriyoshiがいるためお尻がかけない。


せっかくリンクを貼ってもらったのでマトモなことを書こう。

  • 禁酒&三食食べるという生活で、ちょっと真人間に戻った。
  • 日本でPythonやってる人っていろんな言語を経た人が多いので、Python温泉といいつついろんな言語の話題が飛び交っていて面白い。
  • 僕はPython7割、Ruby1割、論文読み2割って感じで作業できた。Newmanのグラフクラスタリングの手法の調査と、PW Voronoi Treemapsの調査・実装。ニコニコ大百科にも新機能用のデータ取得を仕込んだ。
  • PyTCをCythonで書き直そうと思ったけど、Cのライブラリをラッピングするという点ではCythonなりにキツい部分がある。むしろ、ctypesでh2xml.py&xml2py.pyのほうがよいかもしれない。PyTCの修正は今のコードを修正でいくと思う。ココを直せ!この関数使いたい!というのがあればぜひツッコミください。
  • PyTTはid:perezvonのPyrex製のものを使うといいと思う。
  • CythonはPythonで書いたコードの1部を高速化したいという用途にはよく向いていると思う。たとえば、PW Voronoi TreemapsをPythonで実装したものをCythonに移植しただけで、10倍早くなった(もちろん、いろんな型はcdefに変更した)。ループがくるくる回る系のアルゴリズムだとかなり有利になると思う。
  • PythonRubyとを同時に書くテクニックをやっと身に着けた。
  • Java勢は元気。Java温泉やるんだったら参加しよう。Javaは5年くらい書いてないけど。
  • PSPを持っていったら良かった。
  • メシは食いすぎない、ということを1日目に学んだ。
  • アクセンスの方々の、システム開発を楽しみつつも詰めている感じが良かった。顧客志向を技術で裏打ちする、って感じかな。
  • 熱海駅id:nishiohirokazuに声かけしておいてホント良かった。あれはホントに死んでもおかしくなかったよ…
  • 帰りの電車で携帯サイトを開発する意欲を得た!
  • こういうイベントって、新しい人が入ったほうが面白いと思うので事前申し込みはしなかった。また数回後には申し込みをしてみようかな。

Senna DB APIのPython bindingsをCythonを使って書く

id:shnに「PyrexでPyTC書き直してみたら?」と言われて、
id:moriyoshiに「Cythonのほうがいいお」と言われたので(どちらも意訳)、
Cythonをちょこちょこと触っていた。


Senna DB APIのbindingsを書いてみた。
一応説明しておくと、Senna DB APIはkey-record型のDBです。
key-value型だと直に値を保持します(ex. TokyoCabinet etc.)。
key-record型はrecordの中にカラムが複数定義できます。
あるカラムで絞り込みをしつつ別のカラムでソートして
上位n件のkey-recordを取り出す、
ということができたりするのがkey-value型に比べたメリットです。
key-record型っていう用語は超怪しいですけどね…


俺的Q&A

Pyrexとどこが違うのよ?

いろいろあるみたいだけど、
僕が気に入っているのは

  • bint型
  • classの中身でコードがちゃんと実行できる
  • エラーが超親切

の3点です。

bint型は、bool的なintを格納する型です。
classの中身でコードが実行できるのは、以下のようなことができるってことですね。
classmethodについては、たぶんデコレータは使えないと思う。試してないけど。

  cdef class Blah:
    def some_method(self):
      print self
    some_method = classmethod(some_method)
    a = 2*3
    print "hi", a

エラーが超親切なのは言わずもがな。

Pythonの__init__を呼ばずにインスタンス作りたい

C言語のライブラリ側で何かのインスタンスを表すポインタが得られるとします。
このポインタを保持するPythonオブジェクトを作りたいとします。
普通にCythonでオブジェクトを作ると、__init__が呼ばれてしまいます。
ということは、ポインタ->Pythonのobject->ポインタという無駄な変換をしないといけないわけです。
直にオブジェクト作りたいっすよね。


以下のようにPY_NEWを定義したヘッダを作っておきます。tekito.hとか。

#define PY_NEW(T) \
     (((PyTypeObject*)(T))->tp_new( \
             (PyTypeObject*)(T), __pyx_empty_tuple, NULL))

んで、以下のようにするとC言語側でPythonのオブジェクトを作ってあげることができます。
ファクトリメソッドが外に出ちゃうのはあんまり気にしないで。

cdef class Tekito:
    cdef void *ptr
    def __init__(self):
        raise TypeError('This class cannot be instantiated from Python')

cdef extern from "tekito.h":
    cdef object NEW_CLASS "PY_NEW" (object t)

cdef ExampleClass _factory(void *ptr):
    cdef Tekito instance
    instance = NEW_CLASS(Tekito)
    instance.ptr = ptr
    return instance

初期化はどうするのよ?

普通にモジュールトップで関数を呼び出してください。

終了処理はどうするのよ?

atexit使ってください。

import atexit
def fin_function():
  fin_c_function()

atexit.register(fin_function)

==cdefで定義された関数も直にregisterできます。ステキですね。==
うそでした。お詫びして訂正します。上のコードは訂正後です。
以前のCythonでは、どうやら特有のatexitの代わりを行う__Pyx_RegisterCleanup()という関数が挿入されていたようです。
(Py_AtExitは16個しか関数が登録できないということで)。
手元のCython 0.9.8では、Pythonのatexit.registerを呼んでいるようです。


というわけで、今書くならPyrexよりCythonダネッ!
とりあえずコミットしたので、
memcachedプロトコルの実装をlibevent+Pythonでやってみるか。