PyQtでお手軽GUI開発♪―――は可能だったか? 最終回
さて、なんだか後半は妙なテンションになってしまいましたが、最後のまとめをしてみたいと思います。
今回の連載なんですが、2月の初頭、ふとPyQtを使ってみようかと思い立ったのがきっかけで、それからマンデルサーバーができるまでの約1ヶ月半、いろいろと試してみた内容を元に書いています。
試行錯誤しながらもこの期間でこの程度までできてしまったということが、PyQtがお手軽に開発できる環境だったことを示していると思いますが、さらに変な紆余曲折がなければもっと短期間でできたのも間違いなく、そんなこともこのコラムを書く動機になりました。
しかし同時にいくつかの問題点も感じました。
Qt雑感
まずPyQtとはそもそもPythonでQtというGUIツールキットを使う仕組みですが、ウィンドウや各ウィジェット、画面描画などについてすべてがQtの機能で、Pythonはただそれを呼びだしているだけだということです。
QtというのはX-WindowのKDEのベースとなっているもので、それが実現している機能そのものは貧弱ではありません。実際Qtを利用している大型アプリも多々あります。
しかしそれにも関わらずWindowsという環境でQt単体を利用するメリットはあまり感じられませんでした。
なぜならWindows上ではVisual Studioが事実上の標準になっていますが(その他Delphiなんて物もありますが)Qtにはそれらを差し置いてまで利用する利便性がないからです。むしろ劣っているところさえありました。
まず仕方のないことながら、WindowsにQtはデフォルトで入っていないので使いたければインストールする必要がありますが、これだけで大きなハンデです。
またQtを利用するのは無料ですが、最近ではVisual Studio Community エディションという無料版もあります。
しかし何よりも、今回のようなコラムが成立してしまうように、Qtではイベントのハンドリングが複雑だったり、モデルビューの使い方が分かりにくかったりと、Visual Studioなどよりも簡単に開発ができるとは言えません。
さらにQt最大のメリットであるクロスプラットフォーム対応ですが、これも異なったOS上で同じアプリケーションを動かさなければならない状況というのはあまりないうえ、そんな場合の選択肢も今ではWebアプリをはじめ、たくさんあります。
最後に日本語入力関係が明らかに弱いのも致命的です。
前述の通り、Spyderでは日本語入力が非常にやりにくいし、単なるtextEditでも再変換が問題になるということは、それを直したかったら少なくともサブクラス化、場合によったらQtのソースを持ってきて元から修正しなければならないかもしれないということで、ちょっと簡単に手が出せるところではありません。
などということを考慮すればやはりQt単体(QtとC++という構成)をWindows上でわざわざ利用する必然性はほとんどないという結論にならざるを得ません。
―――と、ここまではQtをデフォルトの状態、すなわちC++で記述する場合の話をしてきました。
しかしここで開発言語がC++の代りにPythonになったなら話はまったく異なってきます。
PythonとQtの相互補完
それはPythonがC++に比べて極めて便利で使いやすく、機能も遜色ない言語だからです。
以下その理由を箇条書きにしていくと……
- Pythonはインタープリタ言語なので、スクリプトを書いただけですぐに動かせます。
- インデント構文に代表されるように、非常に分かりやすい言語構造をしています。
- 型無し言語(動的型付け言語)なので面倒な宣言の手間がなく、プログラムがシンプルになります。
- デフォルトで整数が多倍長整数になっているなど、強力なデータ型が簡単に使えます。そのためにマンデルブロ集合の高精細計算を簡単に行うことができました。
- リストや辞書、タプルなどの高次の使いやすいデータ構造があります(これを使ったら配列などはもう使いたくなくなります)
- ガベージコレクトやコンテキストマネージャがあるのでリソースの解放に神経を注がなくていいのも便利です(DelphiなどではCreateと書けば何はともあれFreeと書く習慣ができていました。これをうっかりすると例のぬるぽが発生します)
- 便利で多彩なライブラリが標準で付いてきます。本編で並列処理などがあんなに簡単にできてしまったのはそのためです。
- NumbaやCython、更にはNumpyなどという強力なサードパーティーのライブラリ群もあります。これもデフォルトのPIPで簡単にインストールできるし、Anacondaなどを使えばさらに簡便になります。
―――などなど、いくらでも列挙することができます。
ところがそんなPythonなのですが、付属のGUIツールキットのtkinterがショボくて使えません。そのせいでPythonはグラフィカルな処理に弱くなってしまっていました。
ところがその方面には例えば今回のマンデルブロ集合描画など、面白いターゲットがたくさんあります。
またデータ編集ツールのようなユーザーインターフェースが伴う物も簡単には作れませんでした。
ところがQt及びQtDesignrと組み合わせることで、その弱点が完全に克服されたわけです。
- 実はPythonで使えるGUIツールキットは他にも色々ありますが、今のところQtDesignrのようにビジュアルにレイアウトが定義できるのは他にないようです。GUI開発の面倒くささの大部分がウインドウのレイアウトがらみです。言い換えればそういう便利ツールがあれば別にQtでなくともよかったことになりますが……
本コラムを読んでもらえば、Qtにはいろいろ分かりにくいところはあっても基本はしっかりしているので、コツさえ呑み込めば普通に使えます。
私個人の印象ではGUIアプリの開発のしやすさという点では、PyQtはDelphi6の統合環境の8割くらいには達してるんじゃないかと感じます。
そして言語としてPascalとPythonでは、明らかにPythonの方が使いやすいのは間違いありません。
―――ということで、私がハマったような穴の回避さえできれば、PyQtはWindows上でも簡便かつ強力な開発環境だと言えるでしょう。
しかし、何から何まで手放しで褒めるわけにもいきません。
Qtについては前述の通りだし、さらにはPythonにもPythonならではの欠点があるからです。
型付き言語ではあり得ない問題
まず第一に当然のことながら、PyQtはPythonインタープリタとQt、それにPyQt自身がないと動きません。そのため一人でやっている場合はともかく、プログラムをちょっと配布したいと思った場合に困ってしまうのです。
そのためにPythonスクリプトをEXE化するツールがいくつかありますが、現在Python3.5ぐらいまでに対応してるのがcx_Freezeしかありません。しかしこれを使ってみたら、最初のHelloWorldの容量が200MBとか、ちょっとくらくらしてきます。
多分現在一番簡単な方法が「このソフトを使うのならAnacondaを入れてください」と言うことでしょうが、それはそれで結構な騒ぎになります。ちょっとしたツールのためにそれでは大げさすぎると誰もが感じてしまうことでしょう。
もう一つにライセンスの問題があります。PyQtはGPLというライセンスになっているため、PyQtで作ったソフトウェアを配布する場合はGPLライセンスにしなければなりません。そのため会社などで開発したソースを社外秘にできないといったことになります。
これに関してはPySideがありますが、現時点ではQt5に対応するPySide2は、まだ正式リリースされていないようです(ただこれに関しては時間の問題だとは思いますが)
しかし私の思うところ、何よりも大きな問題はPythonが型無し言語だと言うことでしょう。
―――これは今さっきPythonの利点として褒めた所なんですが、実はまさに諸刃の剣なのです。
まず何よりもこれこそがPythonの実行速度が遅くなっている元凶です。もちろん本編でもやったとおりNumbaやCythonを使うことで大抵は何とかできそうですが、そのために余計な手間がかかってしまうのは事実です。
しかしそれよりも致命的なのが型付き言語ではあり得ないバグの存在でしょうか。
C++やDelphiのような型付き言語でプログラムをしていると型の互換性について嫌でも学ぶ羽目になります。変数や関数は必ず型宣言しなければならないし、演算だけでなく代入を行う場合も互換性のある型同士で行わなければコンパイルエラーになって実行することさえできません。
しかしPythonの場合、変数にはどんな型の値でも代入できるし、異なった型同士で演算していても実行時エラーにはなりますが、事前の構文チェックではエラーにはなりません。
- これはSpyderが利用しているPylintという構文チェッカーの問題かもしれませんが。
しかし、そもそも型が違うということは一般的には異質のデータであることを示しています。
従って異なった型同士の演算や代入があったら普通は間違いで、意図したのであれば明示的な型変換が必要というのが型付き言語の立場です。
型無し言語ではそんな面倒くさいルールを守る必要がない反面、型付き言語で未然に防げていた間違いが防げないのです。
しかも出てくるのが実行時エラーだということで、これは稀にしか実行されないパスにそんな間違いが潜んでいたら、手遅れになるまで気づかれないかもしれないということも意味しています。
さらにバグで一番困るのは、間違っているにも関わらず中途半端に動いてしまうことです。
例えばクラスのインスタンス変数のメソッドを呼んでいるような場合、Pythonでは同じ引数で呼び出せる同名のメソッドがあれば、元がまったく無関係のクラスでも普通に動作します。
型付き言語では変数に代入したり関数で呼び出す時点で型の互換性がチェックされてダメならエラーになりますが、Pythonではそこのところはスルーです。
- もちろんこれが一方的に問題になるわけではなく両刃の剣の別の例というべきです。メソッドさえ合ってればクラスに関わらず使えるという性質が便利な場合もあって、Delphiなどではそのためにインターフェースというのが定義できるようになっているんだと思います。
確かにそんなことが起こる可能性が低いのは間違いありませんが、でも型付き言語の場合はそういうことは論理的にあり得ないのに対して、絶対ないとは言い切れないというのがPythonなのです。
この型はなんだ?
また同じ原因でプログラムを組んでいるときにも非常に不便なことがあります。
というのは、PyQtを使っていると関数のパラメータはほとんどがクラスになっています。
例えばQWidgetのイベントハンドラは以下のように書きましたが……
def mousePressEvent(self, event);
#マウスを押したときの処理
この関数のパラメータeventはQMouseEventクラスのインスタンスで、そのメンバにボタンの状態とか座標情報が入っています。ところがそれを参照しようと思っても、Spyderのコード補完機能では出てきてくれません。
これはSpyderの責任ではなく、Pythonの仕様上、関数の引数になったらどこでどう呼び出されるかなんて分からないので、eventという変数が実際にどんな型か推測できないからです。
しかしQtのこんな山のようにあるクラスのメンバなど一々覚えていられません。そのため一々Qtのドキュメントを見に行かなければなりません。
一応裏技的に……
def mousePressEvent(self, event=Qg.QMouseEvent);
#マウスを押したときの処理
―――と、書いておく手はあります。そうすればeventというのがQg.QMouseEventという型だと分かるのでコード補完できるようになりますが、実際にそういうデフォルトパラメータがあるわけではないので、書き終わったら忘れずに消しておかなければなりません。
今回Spyderのことにはあまり詳しく触れられませんでしたが、これには結構困りました。なぜならDelphiの場合はコード補完できない=コンパイルエラーがあるということだったのですが、こちらの場合は本当に間違っていることもあれば、単に型推定できなかっただけの両方のケースがあるからです。
そしてそんな状況でうろ覚えのクラスのメンバ名などを書くのは大変危険です。
なぜならそれで綴り間違いなどをしようものなら、型付き言語なら「そんなメンバはうちにはおらん! 帰れ‼」と門前払いされてしまいますが、Pythonだと「ふふっ。も・ち・ろ・ん実行時にはそんなメンバがいるんだよね? 君を信じてるよ」と優しく通してくれちゃうからです。
シンプルすぎるクラス仕様
同様にPythonのクラスの仕様も、少々シンプルすぎ―――悪くいえば「ちゃち」というべきでしょうか。
オブジェクト指向プログラムを学習する際、まずクラス定義について勉強するわけですが、Delphiなど多くの言語ではクラスのメンバにはプライベート、プロテクト、パブリックとなどいった区別があり、メソッドにも静的メソッドと動的メソッドがあってサブクラスではオーバーライドが云々とか、まあ最初は何のことかまず分かりません。
しかしPythonではクラスのメンバにもメソッドにもそんな区別はありません。上の言い方ならすべてパブリックで、すべて動的メソッド(バーチャル)です。
ところがそれでも実は、Qtのような抽象的なスーパークラスから具体的なサブクラス継承していくという構造を作るのには困りません。でなければQtをPythonから使うことなんてできません。
しかしそのためにオブジェクトのもう一つの機能であるモジュール化に関しては大きなな弱点を抱えています。
- ここで言うモジュールはPythonのmoduleのことではなく、プログラムの部品といった概念のことです。
というのは何故クラスのメンバにプライベートやプロテクトなどがあるかといえば、これは究極的には馬鹿に勝手に中を触らせない仕組みなのです。
プログラムの構造化というのは要するに、インターフェースの決まったモジュールを組み合わせて構築していこうということです。そうすれば出入り口のところさえ変わらなければ、モジュールの中身をどう修正しても全体に影響が出ないからです。
そしてクラス内部でだけ使う変数や関数はプライベート、サブクラスであれば使えるのがプロテクト、誰でも参照できるのがパブリックといった区別がなされているわけですが、多くの言語では例えばプライベート変数を外から参照しようとしても参照できないようになっています。なぜならそんな内部変数を勝手に参照されたり変更されていたら、バグ修正したら関係ないところがクラッシュしたなんてことにもなりかねません。
ところがPythonの場合そのあたりが性善説でできていて、一応プライベートメンバにしたい変数や関数名はアンダーバーで始めるというルールになっていますが、その気になればいくらでもルール破りはできます。
要するに厳しいルールも存在意義があるから存在しているわけで、それを取り払ったのであればそれ相応のリスクは覚悟しなければならないのです。
ところがこれまで出てきた問題点ですが、これはプログラムが小さいうちはさして問題にはなりません。一人で全体を見渡せる程度の規模ならば、ちょっと注意していれば防げる程度のことでしょう。
しかし、そのリスクはプロジェクトの規模が大きくなればなるほど顕在化してきます。
たとえばPythonではエラーがランタイムでしか分かりません。すると当然書いた部分は必ずテストしなければなりませんが、デートの時刻が迫っているんで手抜きをしたり、簡単な修正だから大丈夫さっ! などとテストを省略したり―――なんてことはないでしょうか?
またあるクラスにすごく便利そうな _very_useful() という関数があったんでちょっと使っちゃえとか? あるいは最初はパブリックで定義した関数を後からプライベートに変更したけど、名前変更が面倒なんでそのままにしてあったとか?
あなたがそんなことはしないと誓っていても、他人や半年前の自分はどうだったでしょうか?
―――要するにPythonで大きなプロジェクトを組もうと思ったら、プロジェクト管理は徹底する必要があるだろうということなのです。
- まあ、逆にこんな風に地雷がはっきり見えていれば誰もがそれに気づくので、むしろしっかり管理できたりするのかもしれませんが……
Pythonの思想
ところでどうしてこんな仕様になっているのかというと、結局それがPythonという言語の設計思想だからというほかはありません。
Pythonの特徴は一言で言えば、誰にでも分かりやすく簡単に書けるということでしょう。
インタープリタであるのも、変数に型がないのも、クラスがシンプルなのも、プログラムの敷居を低くしてすぐ使えるようにするためでしょう。でなければC++とかを使えばいいのですから―――しかしそういう言語だと、プログラムが動かせるようになるまでによく分からないお約束や呪文を大量に覚えなければならなくなて、そのあたりで疲れてしまいます。
そのあたりは公式FAQにあるPython は初心者プログラマに向いている言語ですか?というところにはっきりと書かれています。
Python を学ぶことで、生徒は問題の分析やデータ型の設計など、重要なプログラミングスキルに集中することができるのです。
そこの記述は主に教育目的についてですが、これは一般のプログラマにとっても同じことです。Pythonではプログラマがプログラムの本質的なところに集中できるように、それ以外の余計な(と設計者が判断した)要素を可能な限りそぎ落とした言語だということでしょう。
このあたりがPythonの型無しへのこだわりにも現れています。
実はPython3以降では関数や変数の付加情報(アノテーション)という新仕様が入っています。これは最近のバージョンでは原則として型情報を入れておくものにしたいということになっています。
以前の所のサンプルでもこそっと使っていますが、Python3以降では言語仕様として、関数の定義には以下のように型ヒントが書けるようになっています。
def index(self, row: int, column: int, parent: Qc.QModelIndex) -> Qc.QModelIndex:
これは関数indexのrow, column というパラメータはint型で、parentはQc.QModelIndexというクラスで、結果としてQc.QModelIndex型の変数を返すことを示しています。
しかし、こう書いたからといって例えばrowパラメータに文字列を入れて呼び出してもそれだけではエラーにはなりません。今まで通りに内部処理で型が合わなくなったときに初めて実行時エラーになります。これはあくまで型のヒントであって、処理系はそれを元に型チェックなどは行わないというのです。
そんな物がなんの役に立つかといえば、事前の構文解析のときです。こう書いておけば変数型が推測できるようになるのでコード補完もできるようになるし、異なった型の代入や演算がある場合に警告を出したりできるでしょう。
- 現在のバージョンのSpyder3.1.2にはまだその機能はありませんが、まあそのうち実装されるのではないかと思います。PyCharmというIDEではもうできるという話もあります。
またNumbaやCythonなどのツールのさらなる最適化にも役立ちそうです。Cythonのところでも書きましたが、こちらは独自の型宣言文ではなく正式の構文なので、CythonがまさにPythonコンパイラそのものになります。
―――しかしそんなことならもうこういう優れた機能はサードパーティーに任せずに、Pythonの処理系に組み込んでしまった方がよかったりはしないのでしょうか?
ところがアノテーションの仕様を記述したPEP 0484 -- 型ヒント(Type Hints)には……
Python は依然として動的型付け言語のままです。 Python の作者たちは(たとえ規約としてであっても)型ヒントを必須とすることを望んではいません。
―――と太字で書かれています。
どうしてそこまでこだわっているのでしょうか?
これについては私の想像ですが、こういうことは中途半端では逆に無意味だということです。
実はDelphiにもバリアント型という型があります。これはどんな型でも代入できる型で、要するにPythonの型無し変数のような物なのですが、正直使ったことがありません。
なぜならそんな物を使ったら遅くなるし、メモリも無駄に食うし、しかも結局はバリアント型で型宣言しなければなりません。たとえば変数のSWAPのような、型に依存しない抽象的な処理をするような場合には便利なのかもしれませんが、まあ普通はintegerとかdoubleとかstringとかを使ってれば事足りてしまうからです。
もし中途半端に静的型付けで高速化とかいったことになったら、間違いなく利用する側では「特に理由がなければとりあえず型宣言しておけ。そうすれば効率がいいしね🖤」といった運用になるのは目に見えています。そうなったら結局Pythonは「中途半端な型付き言語」になってしまって、開発者の求めていたコンセプトとはかけ離れた物になってしまうでしょう。
そんなわけでPythonの制作者は型付けをすればいろいろ良いことがあるとは分かっていても、そうしないという選択を行ったのだと思います。
まあそんな感じで首尾一貫しているところがPythonを気に入った理由なんだと思いますが……
というわけで……
PyQtでお手軽GUI開発は可能だったか? というのがこのコラムのタイトルでしたが、結論としては「可能だったか?」というよりも……
PyQtはお手軽にGUIプログラムを作ってみたいときにこそ一番向いている!
―――というべきでしょう。
自分で楽しんでプログラミングしたいような場合には、たとえWindows環境であってもあえてPyQtを使うという選択肢は大いに有りです。
それにPyQtに可能なことのポテンシャルはC++などの言語にも劣らないし、開発効率はむしろそれ以上でしょう。いざとなればCythonなどでC++などの関数を呼ぶことも可能です。
従ってPyQtを仕事などで使うというのもありだとは思いますが―――プロジェクトが大きくなってしまったら、管理者の方は死なない程度に頑張ってください🖤
それにしてもPyQtというのは今ひとつ盛り上がってないようですが、それはやはりQtが取っつきにくかったからではないかという気がします。しかしこうして分かってみればそれなりに使えるわけで、どうしてそんなところを分かりやすくまとめてくれたチュートリアルがないんだろう? といった気分になったので、今回思い立ってこんなコラムを書いてみました。
ってなわけでお役に立てれば幸いです。
- なお、全体を通じ間違いなどがありましたらこちらの方にコメントしてください(質問等でも構いませんが、多分答える能力があまりないのではないかと思います)
- 最後のおまけはうちで見つけたマンデル画像の中で、なんだか一番気に入った奴です。