Windowsの小物はほぼPythonで作っているわけですが。

WindowsにPythonをインストーラーを使ってインストールすると*.pyにデフォルトだとpython.exeを関連付けしてくれます。 これは非プログラマな人にプログラムを渡すとき非常に便利で、とりあえずPythonをインストールして、このファイルをダブルクリックしろ、というだけでオッケー。exe化して無駄な容量を食わなくても大丈夫なのです。

んで、作るのはだいたいコンソールで実行するもの。 これがダブルクリックで実行できるのはいいんですが、当然、プログラムが終了するとウインドウ(DOSプロンプト)が閉じちゃうから結果が見れない。 いちいちDOSプロンプトから実行するのもめんどくさい。しかも見た目的に非プログラマにはいかつい。

なんとかならないかなー、と思ってTkを使ってコンソールアプリを作るためのライブラリを作ってみた。

# vim: fileencoding=utf-8
from Tkinter import *
from ScrolledText import ScrolledText
import sys
import thread
import time

class GUIConsole(Frame):
  def init(self):
    self.init_input()
    self.init_output()

  def init_input(self):
    self.input_var = StringVar()
    self.input = Entry(self, width=100, textvariable=self.input_var)
    self.input.pack(side=TOP)
    self.input.bind('<Return>', self.input_enter)

    self.input_var.readline = self.readline
    sys.stdin = self.input_var

  def init_output(self):
    self.out = ScrolledText(self, width=100, height=30)
    self.out.pack(side=TOP)
    self.out.write = self.write
    sys.stdout = self.out

  def write(self, str):
    self.out.insert(END, str)
    time.sleep(0.0001)
    self.out.yview_scroll(str.count("\n") + 1, "units")

  def readline(self, size=None):
    self.input.focus()
    self.input_entered = False
    while True:
      time.sleep(0.5)
      if self.input_entered == True:
        break
    result = self.input_var.get()
    self.input_var.set("")
    return result

  def input_enter(self, event):
    self.input_entered = True

  def __init__(self, title, master=None):
    self.input_entered = False
    Frame.__init__(self, master)
    self.pack()
    self.master.title(title)
    self.init()

def start(main_func, title="Python") : 
  app = GUIConsole(title)
  thread.start_new_thread(main_func, ())
  app.mainloop()

使い方はこんなかんじ。

import guiconsole

def main():
  while True:
    var = raw_input()
    print var

guiconsole.start(main, "GUIコンソールのテスト")

でこんな風にみえる。

image

一番上に入力欄があって、その下に結果表示エリア。

Pythonの場合、stdoutはwriteというメソッド、stdinはreadlineというメソッドさえもっていればどんなオブジェクトでもオッケー。 なので単純にstdoutとstdinをTkのウィジットに置き換えてメインループに入り、別スレッドでメインの処理を実行してやっているだけ。

見栄えも生のDOSプロンプトよりはいいし、処理が終わってもウィンドウを閉じない限り結果を見ることができる。 プチ便利なので、はやくも自分で使いまくりです(笑


Pythonの練習がてら、アクセサの生成をやってみる。どうせ探したらいっぱいコードが転がってるだろうし、練習にはうってつけかな、と。

まず、ダメそうだけど、Rubyをやってる人からするとこうかきたい、というコード。

class Test(Accessor):
  attr_accessor("__test", "__test2", "test3", "_test4")

  def __init__(self):
    self.__test = "test_value"
    self.__test2 = "test2_value"
    self.test3 = "test3_value"
    self._test4 = "test4_value"

こんな感じ。まぁ、絶対にダメそうだ(笑 でも組み込みとはいえ、classmethodやstaticmethodみたいなのもあるから無理やりにならできるのかもしれない。

def test():
  print sys._getframe(1).f_code.co_name

class Test:
  test()

#=> 文字列"Test"を出力

こんな感じで呼び出しもとのクラス名は取得できる。ならevalすれば、というところなのだが予想通り

def test():
  exec "cls = %s" % sys._getframe(1).f_code.co_name

class Test:
  test()

#=> NameError: name 'Test' is not defined

この時点ではクラスオブジェクトの生成が完了していないので無理なようだ。Rubyのようにはいかない。

結局のところ__metaclass__を使うことになる。 マニュアルにもズバリ > メタクラスは限りない潜在的利用価値を持っています。これまで試されてきたアイデアには、ログ記録、インタフェースのチェック、自動デリゲーション、 自動プロパティ生成 、プロキシ、フレームワーク、そして自動リソースロック/同期といったものがあります。

と書いてあるのだ。

  • 後からもアクセサを追加したい(メソッドとしても独立させたい)
  • 当然、読み出しと書き込み、そして両方を行えるインターフェイス
  • オーバーライドはできないとまずい

というアクセサ生成としては至極当たり前なことを考えつつ、コードを書いてみる。

# vim: fileencoding=utf-8
from itertools import * 

def property_accessor(cls, *names): 
  map(lambda n : _add_setter(cls, n[0], n[1]) or 
                 _add_getter(cls, n[0], n[1]),izip(names, _real_names(cls, names)))

def property_reader(cls, *names):
  map(lambda n : _add_getter(cls, n[0], n[1]),izip(names, _real_names(cls, names)))

def property_writer(cls, *names):
  map(lambda n : _add_setter(cls, n[0], n[1]),izip(names, _real_names(cls, names)))

def _real_names(cls, names) :
  cls_name = cls.__name__
  return imap(lambda n : n.startswith("__") and "_%s%s"%(cls_name, n) or n, names)

def _add_setter(cls, name, real_name) :
  setter_name = "set_%s" % name.lstrip("_")
  if cls.__dict__.has_key(setter_name): return 
  setattr(cls, setter_name, lambda self, v: setattr(self, real_name, v))

def _add_getter(cls, name, real_name) :
  getter_name = "get_%s" % name.lstrip("_")
  if cls.__dict__.has_key(getter_name): return 
  setattr(cls, getter_name, lambda self: getattr(self, real_name))

class AccessorType(type):
  def __new__(cls, class_name, class_bases, classdict):
    cls = type.__new__(cls, class_name, class_bases, classdict)
    list = ["__accessor__", "__reader__", "__writer__"]
    methods = imap(lambda n: eval("property_%s"%n.strip("_")), list)
    map(lambda n: 
          classdict.has_key(n[0]) and
            n[1](cls, *classdict[n[0]]), izip(list, methods))
    return cls

class Accessor:
  __metaclass__ = AccessorType

class Test(Accessor):
  __accessor__ = ["__test2", "test3", "_test4"]
  __reader__   = ["__test"]

  def __init__(self):
    self.__test = "test_value"
    self.__test2 = "test2_value"
    self.test3 = "test3_value"
    self._test4 = "test4_value"

  def get_test3(self) :
    return "changed_test3_value"

obj = Test()

print obj.get_test2()
# => "test2_value"
print obj.get_test3()
# => "changed_test3_value"

property_writer(Test, "__test")
obj.set_test("new_test_value")
print obj.get_test()
# => "new_test_value"

テストもろくにしてないし、汚いコードだけどなんとなくそれっぽい動き。だいたい40行くらいで実装できる。パフォーマンスを考えなければitertoolsがなくても大丈夫。

そして、回答をさがしてみる。とりあえず「python accessor __metaclass__」あたりでググるとすぐにコードが見つかる。

見事なくらい同じようなコード。ただ、自分で書いたやつのほうが、オーバーライドできる、あとからもアクセサが生成できる、という点ではいい感じ。

感想

ここらへんのメタプログラミングはやっぱりRubyのほうが柔軟性があってかつ、一貫しているので楽かな。 Pythonはやっぱり関数をオブジェクトとして扱いやすい、というのが楽。Rubyのobj.some_methodでメソッドが呼び出せるのは便利だが、その点ではやっぱキツい。ループもPythonほうが好きだな。

ま、やっぱり個人の好み、としかいいようがないなあ。最近ではWindowsの小物はほとんどpythonで書いてるけど(インストーラーがあって関連付けまでしてくれて、楽)、Linuxのほうはそうでもないし。


nil.to_sが”nil”になるような言語は(ry というのは置いといて、まだまだPythonをリハビリ中。 Rubyより関数志向での開発がしやすくて楽しいですよねえ。

リハビリを兼ねてweb.pyで個人的なツールを作ってます。web.pyはシンプルで軽いので、個人用途(非公開でツールとして使う)だったらレンタルサーバーでCGIとして動作させても問題なさそうです。(XREAでは簡単なものなら負荷率0ptでした。Railsとかならいれるだけでダメなんで、素晴らしい)

そこで、scriptaculousのAjax.InPlaceEditorを使っているわけですが、困ったことがひとつ。InPlace___Editor___なわけで、値を当然入力します。でその値がサーバー側でバリデーションエラーな時どうしたもんかと。

理想的な動作としては

  • エラーをユーザーに通知する
  • サブミットする前の値のままtextareaを表示しておく

なんですが、コンストラクタの中のoptionsあたりをのぞいてもそれらしきオプションはなさげ。

ということでとりあえず場当たり的に書いてみたものが以下。

(function(){
Ajax.RollbackableInPlaceEditor = Class.create();
var dummy = function(){};
dummy.prototype = Ajax.InPlaceEditor.prototype;
Ajax.RollbackableInPlaceEditor.prototype = new dummy;
Ajax.RollbackableInPlaceEditor.prototype.constructor = Ajax.RollbackableInPlaceEditor;
})();
Object.extend(Ajax.RollbackableInPlaceEditor.prototype, {
  initialize : function(element, url, options) {
    var self = this;
    options = Object.extend({
      callback: function(form) {
        self.oldValue = self.editField.value;
        return Form.serialize(form);
      },
      onFailure : Prototype.emptyFunction
    }, options || {});
    Ajax.InPlaceEditor.prototype.initialize.apply(this, [element, url, options]);
  },
  onclickCancel: function() {
    this.onComplete();
    this.element.innerHTML = this._oldInnerHTML;
    this.element.show();
    return false;
  },
  onComplete: function(transport) {
    this.leaveEditMode();
    if(transport && !Ajax.Base.prototype.responseIsSuccess.call({transport:transport})) {
      this.options.onFailure.bind(this)(transport, this.element);
      return this.enterEditMode();
    }else {
      this.oldValue = null;
    }
    this.options.onComplete.bind(this)(transport, this.element);
  },
  onFailure: function(transport) {
    return false;
  },
  getText : function() {
     if(this.oldValue != null && this.oldValue != undefined){var v = this.oldValue; this.oldValue = null; return v;}
    this._oldInnerHTML = this.element.innerHTML;
    return this.element.innerHTML;
  }
});

サーバー側でエラーがあったときは適当に500とか406とかでレスポンスを返せばオッケーです。

なのでサーバー側ではたとえばweb.pyなら

# errors = ['データが長すぎます。'] など
if len(errors) != 0:
  web.header("Content-Type", "text/javascript"), 
  web.ctx["status"] = "406 Not Acceptable"
  print "".join(["alert('", "\n".join(errors), "');"])
  return 

とすればエラーになって(エラーメッセージがアラートされる)、サブミットした内容のままで再度編集できます。

豆知識ですが、以上のようにサーバー側でtext/javascriptでレスポンスを返すとprototype.jsでは自動的にjavascriptとして評価してくれます。

うーむ、まさに場当たり的。いろいろまずそうだけど、まぁ動いてるっぽいし気にしないことにしとこう・・・・