2010-11-06

素数を列挙する #2

前回の続き。

素数表の検索にバイナリサーチを使う

前回、素数の判定法としてはもっとも素直な「√n 以下の整数で割る」よりは割り算が少なくなると考え、素数表を使った「√n 以下の素数で割る」方法を実装してみた。ところが、列挙範囲の素数表が完成している場合であっても(つまり割り算は一度も行わない)、「√n 以下の整数で割る」方が速いという結果になった。

その原因は Array#include? を使った素数表の検索にあるのではないかと考え、今回は検索に「バイナリサーチ(二分検索)」を使ってみることにした。

(listprimes_binsearch.rb より)
def ($primes).binsearch(n)
  len = self.size
  first = 0
  last = len - 1
  while len > 0
    mid = first + len / 2
    v = self[mid]
    if n == v
      return true
    elsif n < v
      last = mid - 1
    else
      first = mid + 1
    end
    len = last - first + 1
  end
  false
end

結果として、Array#include? を使ったものより高速になったことはもちろん、(素数表が完成していれば)列挙する範囲によっては「√n 以下の整数で割る」ものよりも速くなった。とはいえ、やはり素数表を生成しながらの場合は、比較にならないほど遅い。

エラトステネスの篩

素数表の生成でもう少し速そうな方法を試してみることにした。有名な「エラトステネスの篩」だ。コードを以下に示す。

「√n 以下の素数で割る」版、「篩」版、そして「シンプルに √n 以下の整数で割る」版を実行してみた結果は以下のようになった。

[imac] mnbi% rm primes.txt
[imac] mnbi% time ./listprimes_binsearch.rb 100000 100
START (99991):
100003 (66)
100019 (66)
100043 (66)
100049 (66)
100057 (66)
100069 (66)
Elapsed: 0.000275
./listprimes_binsearch.rb 100000 100  17.60s user 0.02s system 100% cpu 17.601 total
[imac] mnbi% time ./listprimes_sieve.rb 100000 100
100003
100019
100043
100049
100057
100069
Elapsed: 0.019148
./listprimes_sieve.rb 100000 100  0.02s user 0.00s system 83% cpu 0.033 total
[imac] mnbi% time ./listprimes_simple.rb 100000 100
100003 (158)
100019 (158)
100043 (158)
100049 (158)
100057 (158)
100069 (158)
Elapsed: 0.00028
./listprimes_simple.rb 100000 100  0.01s user 0.01s system 89% cpu 0.020 total

「篩」版は確かに速いが、「シンプル」版はさらに速い。

もっとも、これは表示させる領域を大きな数を基点として「狭めて」いるから起きることだ。1 から 100000 までの範囲の素数を列挙するなら「シンプル版」の方が遅くなる。

一番、速いのは

範囲(それも比較的大きな数から始まるものを)を指定して列挙させる場合、速いのは「√n 以下の整数で割る」方法をシンプルに実装したものだ。2 から始まる素数表を生成する方式では、範囲の上限に至るまでのすべての素数を生成する分不利になる。

たとえば、画面に一定範囲にふくまれる素数を表示する(だけの)アプリを作るとしたら、素数表を作る方式は適さないことになる。画面に表示できる範囲は限られているのだから、素数表を(作って)保持しておくより、見えている分だけをシンプルな方法(√n 以下の整数で割る)で判定する方がずっと素早く表示できるはずだ。

関連リンク

関連記事

2010-11-05

素数を列挙する

プログラミングの練習。指定された範囲にある素数を列挙するプログラムを作ってみる。

素数表を使った判定

利用するアルゴリズムは自明のもの。つまり、正整数 n に対して √ n を超えない素数で割り切れるかどうかをチェックしている。初回の実行では 2 (最小の素数)から始めて素数表を作っている。実行中に作った素数表は最後にファイルに書き出す。2 回目以降の実行では、以前に作った素数表を読み込んでいる。

列挙した素数の後にカッコ付きで表示する数字は計算回数だ。

検証

念には念を入れて、listprimes.rb で作られた primes.txt を検証する。以下がそのプログラムだ。素数を判定するアルゴリズムは大して変わっていない(素数表を使わず √ n 以下の整数で割り切れるか否かをチェック)から、どちらかと言えばプログラムのバグを確認するためのものというべきだ。

実行結果

[imac] mnbi% ls primes.txt
ls: primes.txt: No such file or directory
[imac] mnbi% ./listprimes.rb 1000 50
START (2):
1009 (12)
1013 (12)
1019 (12)
1021 (12)
1031 (12)
1033 (12)
1039 (12)
1049 (12)
Elapsed: 0.00011
[imac] mnbi% tail primes.txt
991
997
1009
1013
1019
1021
1031
1033
1039
1049
[imac] mnbi% ./listprimes.rb 1000 50
START (1049):
1009 (0)
1013 (0)
1019 (0)
1021 (0)
1031 (0)
1033 (0)
1039 (0)
1049 (0)
Elapsed: 0.000444

初回の listprimes.rb の実行で、primes.txt が作られ、2 回目の実行では、(素数判定のための)計算がされていないことが(計算回数の表示から)読み取れる。意外なことに計算のない 2 回目の方が時間がかかっている。

「√ n 以下の整数」を使うより「√ n 以下の素数」の方が計算回数がぐっと少なくなると考えて素数表を使うようにしたんだが、実際のところでは素数表(配列)の操作にかかる時間の方が律速段階になっているようだ。素数表を使う方法で 2 回目以降の実行(つまり割り算は一切ない)でも、Time で計測してもわかるほどの差が出る。

素数表の検索を工夫すれば逆転するだろうか?

関連リンク

twitter より (2010-11-04)

  • 10:34  うん。まさにその通りだよ。「現場から入ってしまった人は、まず目的に必要なものをでっち上げ、後でそれの裏にある背景や理論を勉強すると、「なるほど〜 だからアレはあーなってたのかー!」と楽しく学習することができます。」→ http://docs.komagata.org/4654
Powered by twtr2src.

2010-11-04

MacRuby 0.7 を iMac にインストール

Mac mini に MacRuby 0.5 beta 2 をインストールしたのが、ほぼ 1 年前のこと。以来、バージョンは 0.5、0.6 と進み、先月(2010-10-01)、0.7 がリリースされた。

以前、参照した 公式 Roadmap はどうやらメンテナンスされておらず記述が古いままになっている。公式ブログから 0.5 beta 2 以降の変更を追跡してみる。

MacRuby のこれまで (0.5 〜 0.7)

0.1 〜 0.5 beta 2 までは以前にまとめた(→ 「MacRuby のこれまで (0.1 〜 0.5 beta 2)」)。ここでは、それ以降の変化を追う。

0.5 (released @ 2010-01-31)

ブログに書かれているのは以下の 3 つ。

  • HotCocoa が MacRuby プロジェクトから独立して GitHub で管理されるようになり、gem 化されたこと(使うには require 'hotcocoa' より前に require 'rubygems' が必要になる)
  • AOT (Ahead-of-Time) コンパイラで(macrubyc)、複数の rb ファイルをまとめてダイナミックライブラリにすることができるようになったこと。また、macrubyc 用のマニュアルページもできた。
  • GCD サポートが完成したこと。GCD を MacRuby で使うためのドキュメントもできた(→ An Introduction to GCD with MacRuby)。
0.6 (released @ 2010-04-30)

0.5 のリリースから 3 ヶ月で 0.6 がリリースされた。ブログには、数多くの新機能が追加されたとともに、全体的な安定性も大きく向上したと書かれている。

主要な変化として挙げられているのは以下の通り。

  • Cocoa アプリを開発できるだけの安定性を備えたこと。Xcode 上で MacRuby の AOT コンパイラを使ったアプリを開発できるようになった。
  • 実験的に、コマンドライン版のデバッガ macrubyd が提供されたこと。デバッグモードでコンパイルすれば、gdb と同様のデバッグを行うことができる。
  • GCD に対して、より抽象度の高い API (dispatch ライブラリ)が提供されたこと。チュートリアルも用意されている(→ Grand Central Dispatch for MacRuby)。
  • 基礎的なクラスを書き直したこと。
    • HashNSMutableDictionary を継承する新しいクラスとなった(これまでは NSMutableDictionary の別名だった)。
    • StringNSMutableString を継承する新しいクラスとなった。エンコーディングの変換に ICU framework を使うようになった。
    • Symbol がユニコード文字を扱えるように書き直された。
    • Regexp が 鬼車に代わって ICU framework を使うように書き直された。
  • Ruby (MRI) との互換性が向上したこと。
    • MRI 用に書かれた C 言語による拡張機能に対するサポートを提供。これにより Nokogiri、SQLite3、そして PostgreSQL 拡張を MacRuby から使えることを確認ずみ。
    • RubySpecs の互換性テストのうち 85% に合格。修正版ながら Rails 3 を動かすことができるようになった。MRI 1.9 のエンコーディングのサポートも向上。
0.7 (released @ 2010-10-01)

0.6 から 5 ヶ月で 0.7 がリリース。ブログによると、目立った機能の追加はないが、その分既存の機能の強化に努めたとのこと。

主要な変化として挙げられているのは以下の通り。

  • Cocoa サポートが向上したこと。
    • C ブロック のサポートを追加。ただし、BridgeSupport をインストールする必要がある。
    • sandbox 機能のサポート(Sandbox クラスを提供)。
  • 並列性とパフォーマンスが向上したこと。
  • MRI との互換性が向上したこと。RubySpecs で 90% を達成。ただし、Rails を無修正で動かすまでには至っていない。

iMac にインストールする

MacRuby のパッケージをダウンロード

公式サイトの右上には「Current Version: 0.7.1」と出ている。ところがダウンロードページにある Lates Stable Release にあるのは 0.7 へのリンク。0.7.1 を落とすには配布ファイルの置き場所を開く必要がある。以下に、0.7.1 へのリンクとあわせて書いておく。

MacRuby をインストール

ダウンロードした zip を展開すると(Finder からダブルクリックで OK)、MacRuby 0.7.1 というフォルダが作られる。この中にある MacRuby 0.7.1.pkg がインストーラだ。これをダブルクリックするとインストールが始まる。ライセンスに同意して、管理者のパスワードを入力すればインストールは完了。

ターミナルを開いて確認。

[imac] mnbi% hash -r
[imac] mnbi% which macruby
/usr/local/bin/macruby
[imac] mnbi% macruby -v
MacRuby 0.7.1 (ruby 1.9.2) [universal-darwin10.0, x86_64]
HotCocoa をインストール
[imac] mnbi% macgem list

*** LOCAL GEMS ***


[imac] mnbi% macgem search hotcocoa --remote

*** REMOTE GEMS ***

hotcocoa (0.5.1)
[imac] mnbi% sudo macgem install hotcocoa
Password:
Successfully installed hotcocoa-0.5.1
1 gem installed
[imac] mnbi% macgem list

*** LOCAL GEMS ***

hotcocoa (0.5.1)
[imac] mnbi% ls /Library/Frameworks/MacRuby.framework/Versions/0.7.1/usr/lib/ruby/Gems/1.9.2/gems
hotcocoa-0.5.1/

着実な進歩

どうやらこの 1 年間で MacRuby は着実に進歩したようだ。まあ、ブログのアナウンス記事を斜め読みするぐらいでは、具体的に何ができて、この先どこを目指しているのかまではわからないけどね。

関連リンク

関連記事

2010-11-03

twitter より (2010-11-02)

  • 09:34  なるほど。Air ってそういう位置付けだと思えばいいのか。革新(と核心)は SSD の搭載にあったってことか。軽くて(薄くて)サクサク動く。唯一の弱点は電池の持ちの悪さだな。 → http://bit.ly/cSud1e
  • 11:25  いまさらだけど、先日の Apple Special Event のビデオを見た(新しい Air が発表になったやつ)。これを見ると、今度の Air が、Mac というよりむしろ iOS デバイスに近いと思えてくる。キーボードがついて、OSX が動く iPad に見えてくる。
  • 11:27  来年(2011)の夏、Lion がリリースされて、Launchpad や Mission Control が利用できるようになれば、ますますその感が強くなるに違いない。
  • 11:30  iPhoto 11 のデモを見ていて感じたのは「あれ、これってiPadなのか(・ω・)?」ってことだ。それぐらい、iPadアプリの影響が色濃く現れていた。画面のデザイン(意匠)はとくに。
  • 11:31  iPhoto 11 のような(外観の)アプリを、Xcode 標準のビューやコントロールだけで作れるんだろうか?
  • 13:31  Mac OS X Lion の新機能として挙げられていた(説明はなかったけど)、(アプリの)自動セーブと起動時のレジューム(これもアプリの話だよね)が実現されると実は画期的なことだと思う。
  • 13:35  前者はアプリユーザをファイルの管理と操作から解放してくれるし、後者もアプリの使い方を大きく変えるはず。もちろん、アプリ側での対応ができてからの話。ま、どちらも iOS アプリでは当たり前のことだけどね。
  • 13:38  けど、はたしてユーザに受け入れられるだろうか? iOS アプリを使い慣れているユーザなら問題ない。Launche PadとMission Control に支えられたフルスクリーンアプリ(自動セーブとレジューム付き)は、iOS アプリのユーザ体験をそのまま取り込んだものだから。
  • 13:40  iOSって何、それオイシイの? と思うユーザにとってはどうだろうか。データを保存するタイミングが自由にならないことにとまどわないだろうか。ファイルが表に出てこないことに違和感を感じないだろうか。
  • 13:43  ああ、そうか。iTunes や iPhoto なんかでファイルを意識しないアプリには慣れているかもな。アプリによるってことかな。ファイルを意識させないユーザ体験を提供できるアプリは新しい操作環境で使われ、ファイルベースの古臭いアプリは旧来のデスクトップの下で使われる、と。
Powered by twtr2src.

2010-11-02

一意性の確保 - ハッシュ関数

以下は実験中の Blogger Glass のコード、そのセッション管理を担当するモジュールの一部でセッション ID を生成す関数になっている。

(src/session.py より)
def generate_session_id(seed):
    m = hashlib.sha1()
    m.update(datetime.datetime.now().isoformat())
    m.update(str(seed))
    return m.hexdigest()

以前、ウェブアプリにおけるセッションを病院に治療に来る患者の関係にたとえた(→「Cookie の使い方 - GAE におけるセッションの保持」)。病気(やケガ)の治療では病院側が患者についてさまざまな情報を記録しておき(カルテ)、その記録と患者個人をひもづけるために番号を使うと書いた。この番号に相当するのがセッション ID なのだ、とも書いた。

カルテと患者のひもづけで大事なことはそれが一通りに定まることだ。これが混乱すると(あるカルテが別の患者のものだと扱われる等)大変なことになる。これを確実にするための 1 つの方法は、空白のカルテ用紙にあらかじめ重複しない通し番号を振っておくというものだ。番号の一意性 (uniqueness) が確保できていることが、患者という個人の識別に利用できる理由だ。

ウェブアプリにおけるセッション ID についても、同じことがそのままあてはまる。すなわち、ウェブアプリとクライアント(ブラウザ)のひもづけには、一意性の保証された何かを利用しなければならない。↑で示した関数は、この一意性の保証された何かを、ハッシュ関数(一方向ハッシュ関数とも言う)によって生成している(標準ライブラリの hashlib)。

ところで、このハッシュ関数が生成する「何か」は本当に一意 (unique) なのだろうか? あるいは完全に一意なものではないとしてどれぐらい一意なのだろう?

一方向ハッシュ関数

先のコードで使っている hashlib.sha1 という関数はハッシュ関数の中でも一方向ハッシュ関数(Wikipedia:ja では暗号学的ハッシュ関数)と呼ばれるものだ。

(「入門SSH」p.44 より)
一方向ハッシュ関数は、任意の長さの入力から固定長の出力を返す関数です。一方向ハッシュ関数の出力は一方向性(出力から入力を得るのが困難)と衝突困難性(同じ出力を得る 2 つの入力を得るのが困難、衝突耐性)を持ちます。この特徴から、ファイルなどが改ざんされていないことのチェック(もしファイルが改ざんされていれば、出力が異なるはず)や電子署名のために入力を短く固定長にするためなどに利用されます。[...snip...]

代表的な一方向ハッシュ関数には、MD5、SHA-1、SHA-256、SHA-512 があります。

もともと、一方向ハッシュ関数はデータの改ざんの有無を検査するような(セキュリティに関係する)目的に使われるが、上記の 2 つの特性のうち「衝突困難性」があるため「一意性の保証された何か」を生成するためにも利用できるわけだ。

さて、hashlib.sha1 で使われている SHA-1 という一方向ハッシュ関数では常に 160 ビットの値を生成する。言い換えると、生成できるのは高々 2^160 個の「何か(符号なしの整数値)」でしかない。もし、毎回異なる値を生成できるとしても、2^160 回以上 ID を生成させれば同じ ID が出てくることになる。そう、これで生成できる「何か」は完全に一意なものではないのだ。

ほどほどの一意性

確かに一方向ハッシュ関数では「完全な一意性」を得ることはできない。でも、良く考えてみれば 2^160 (2 の 160 乗) というのは、結構大きな数だ(→ 「2^64 (2 の 64 乗) って、どれぐらい?」)。うん、いや、結構っていうか、日常生活のスケールでは決して出会うことのないぐらいの大きさだ。ちょっと、Python で計算させてみた。

[imac] mnbi% python
Python 2.6.1 (r261:67515, Feb 11 2010, 00:51:29) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> n = 2 ** 160
>>> len(str(n))
49
>>> print n
1461501637330902918203684832716283019655932542976

10 進数で 49 ケタ。つまり、10^48 より大きく、10^49 より小さい。読み方は、10^48 が「極(ごく)」なので……

(2^160 の 10進数表現の読み方)
1    (極)
4615 (載)
0163 (正)
7330 (澗)
9029 (溝)
1820 (穣)
3684 (じょ)
8327 (垓)
1628 (京)
3019 (兆)
6559 (億)
3254 (万)
2976

……となる。つまり「一極四千六百十五載(飛んで)百六十三正七千三百三十澗九千(飛んで)二十九溝千八百二十穣三千六百八十四じょ八千三百二十七垓千六百二十八京三千(飛んで)十九兆六千五百五十九億三千二百五十四万二千九百七十六」だ。

意図的に(たいていは悪意を持って)同じハッシュ値を作ろうとしない限り、同じ値が生成されることはないと言って良い。「完全な」ではないにしろ、「ほどほどの一意性」(実際には「実用的な」と言うべきだろうな)は保証されているようだ。

ちなみに「新版暗号技術入門 秘密の国のアリス」によれば、SHA-1 の「強衝突耐性」は 2005 年に破られたとのこと(同 p.176)。ここで「強衝突耐性」とは

(「新版暗号技術入門 秘密の国のアリス」p.170 より)
強衝突耐性とは、ハッシュ値が一致するような、異なる 2 つのメッセージを見つけ出すことが非常に困難である、という性質です。

のことだ。

もっとも、ウェブアプリとクライアント(ブラウザ)のひもづけるセッション ID としてハッシュ値を使う場合、セッション ID を知られただけでアウトだけどね。病院のたとえで言えば、診察券に書かれた患者番号を誰かに知られてしまい、診察券を偽造されてしまうかもってところか。ま、病院と患者の場合は、そんなことをしても意味がないがね(診察券に病院で治療を受ける以外の使い道があれば別)。

ウェブアプリのセッションを管理する「何か」としてハッシュ値を使う場合、大事なことは異なるクライアントに同じ「何か」をわたさないようにすることだ。「何か」を(通信の)途中で盗まれないようにすることは別問題。

そもそも、Blogger Glass のような「公開情報を読み取り専用で扱う」アプリでセッションを盗まれたとしても大した問題にはならないよ。

参考文献

入門SSH (My UNIX series (04))
春山 征吾
アスキー ( 2004-11 )
ISBN: 9784756145536
おすすめ度:アマゾンおすすめ度
新版暗号技術入門 秘密の国のアリス
結城 浩
ソフトバンククリエイティブ ( 2008-11-22 )
ISBN: 9784797350999
おすすめ度:アマゾンおすすめ度

「6.4 ハッシュ法」の一部としてハッシュ関数を扱っている(p.487 〜 492)。ただし、扱っているのは一般的なハッシュ関数であり一方向ハッシュ関数ではない。

関連リンク

関連記事

twitter より (2010-11-01)

  • 08:31  キレイな図でわかりやすい。ところで、どこからも参照されないコミットは削除されるとは知らなかった。実験はブランチしてどんどんコミットして、いならくなったらブランチを消せばコミットも消える、と。今さらだけど良くできてるなあ > git → http://bit.ly/afxII7
Powered by twtr2src.

2010-11-01

リングバッファを作る - リクエスト URL をためておく仕組み (BloggerGlass)

「Back」ボタンを実装するためには、戻るべき画面を記録しておかなければならない。現状の画面遷移だけなら 1 つ前の画面を覚えておけば十分なはずだが、将来のことも考えて、画面遷移の履歴を保持するための仕組みを作ってみることにした。

履歴保存用クラス

実体はリクエスト URL (文字列)を保存しておくためのリスト(Ruby で言うなら配列)に過ぎない。ただし、「前の画面に戻る」ための履歴だから、最後に登録した URL を最初に取り出すことになる。つまり、LIFO (Last In First Out)と呼ばれるデータ構造になる。別の呼び方にスタックというのもある。

スタックなら、操作のためのインタフェースは以下のようになる。

スタックの操作
名称 機能
is_empty 空かどうかを返す
push データを 1 つ追加する
pop 最後に追加したデータを取り出す(取り出しされたデータはスタックから削除する)
peek 最後に追加したデータを見る(取り出さない)

最低限必要なのは push と pop で、他はあると便利なものだ。

また、この履歴保存用クラスは、スタックであると同時にリングバッファでもある。これは平たく言えば、バッファが一杯になったとき、古いデータが新しいデータで上書きされていくデータ構造だ。

リングバッファにしたかったのには理由がある。直前の画面に戻るための履歴なのだから、バッファが一杯になったからと言って履歴が保存できなくなるのは困る。一方で、古い履歴よりも新しい履歴の方が重要だ。そういう意味で、新しいデータを常に保存することができる(その代わり古いデータは消えていく)リングバッファは最適だと言える。

export_historyimport_history は、履歴をセッションデータとしてデータストアに収めるときに使用する(ことになるはず)。

単体テスト

RequestHistory クラスは、以下のような単体テストを書きながら、そしてテストしながら実装した。50 行足らずの短いプログラムだと言うのに意外に難しかった。何度「これで良い(はず)!」と思ってからテストに失敗したことか。こういう一般的なプログラムを書くときは、本当にテストが役に立つ。

(tests/util_test.py より)
import unittest

import pathconf
# target module 
import util

class RequestHistoryTest(unittest.TestCase):
    def setUp(self):
        self.history = util.RequestHistory()

    def test_is_empty(self):
        """empty?(EMPTY) == True"""
        self.assert_(self.history.is_empty())

    def test_peek(self):
        """peek(push(EMPTY, A)) == A,
        then pop() also returns A
        """
        self.history.push('/1')
        self.assertEqual(self.history.peek(), '/1')
        self.assertEqual(self.history.pop(), '/1')

    def test_pop_against_empty(self):
        """pop(EMPTY) == None"""
        self.assertEqual(self.history.pop(), None)

    def test_push_1_pop_1(self):
        """pop(push(EMPTY, A)) == A"""
        self.history.push('/')
        self.assertEqual(self.history.pop(), '/')

    def test_push_2_pop_2(self):
        """pop(push(push(EMPTY, A), B)) == B
        pop(pop(push(push(EMPTY, A), B))) == A
        """
        self.history.push('/')
        self.history.push('/foo')
        self.assertEqual(self.history.pop(), '/foo')
        self.assertEqual(self.history.pop(), '/')

    def test_push_10_pop_10(self):
        """H = push(push(...(push(EMPTY, 0), 1), ...), 9)
        then, pop(pop(...(pop(H))...)) = 0
        """
        for i in range(10):
            self.history.push("/%d" % i)
        for i in reverse_range(10):
            self.assertEqual(self.history.pop(), ("/%d" % i))

    def test_push_11_pop_10(self):
        """H = push(push(...(push(EMPTY, 0), 1), ...), 10)
        then, pop(pop(...(pop(H))...)) = 1
        """
        for i in range(11):
            self.history.push("/%d" % i)
        r = reverse_range(11)
        r.pop()
        for i in r:
            self.assertEqual(self.history.pop(), ("/%d" % i))

    def test_export_history(self):
        for i in range(10):
            self.history.push("/%d" % i)
        h = self.history.export_history()
        for i in range(10):
            self.assertEqual(h[i], ("/%d" % i))

    def test_import_history(self):
        h = []
        for i in range(10):
            h.append("/%d" % i)
        self.history.import_history(h)
        r = range(10)

    def test_pickle(self):
        """Make sure that it can be pickled in and out."""
        for i in range(10):
            self.history.push("/%d" % i)
        pickled = pickle.dumps(self.history)
        history = pickle.loads(pickled)

        for i in reverse_range(10):
            self.assertEqual(history.pop(), ("/%d" % i))

def reverse_range(n):
    r = range(n)
    r.reverse()
    return r

def suite():
    return unittest.TestSuite((
            unittest.makeSuite(RequestHistoryTest, 'test'),
            ))

if __name__ == '__main__':
    unittest.TextTestRunner().run(suite())

関連リンク

関連記事

2010-10-31

Cookie の使い方 - GAE におけるセッションの保持

前回(「iPhone アプリらしく #2」)にも書いたように、Blogger Glass は「画面遷移だけの(とても古臭い)ウェブアプリ」だ。これを iPhone アプリらしく見せるためには、ぜひとも「Back」ボタンが必要だ。というのも、iPhone アプリでは、ある画面からボタンやら何やらを押すことで子画面を開き、そこで作業が完了すると親画面に戻る、という操作が良く実装されている。このユーザ体験を実現することは、iPhone アプリとしてごく標準的なことなのだ。

しかし、ウェブアプリでこれ(一つ前に開いていた画面に戻る)をやろうとすると、セッションの保持(と管理)という壁にぶつかる。ステートレスな HTTP 上に作られるウェブアプリの宿命だ。

Blogger Glass では、ここまでセッション管理にまつわることを避けてこれたが、それもそろそろ限界。このあたりで、ちゃんとセッション周りを実装することにしよう。

実現方法

ウェブアプリにおけるセッションの概念とは、たとえるなら病院(医者)とそこに治療に来る患者の間にあるものだ。初診時に治療セッションが始まり、完治によって終了する。

患者は病気なりケガなりの治療で数回、病院を訪れることになる(セッションの継続)。その度に、病院(医者)の側が患者のことをすっかり忘れてしまっては治療は成立しない。病院(医者)は個々の患者について、病気(やケガ)の状態と治療の経過を記録しておかなければならない。それが患者ごとに用意されるカルテと呼ばれる記録だ。

一方、カルテに書かれた記録を有効に利用するためには、患者一人一人を識別できるようにする必要がある。大抵は(カルテに書かれた)患者の名前で間に合うが、中には同姓同名の患者もいるから常に確実な方法とは言えない。そんなわけで患者に一意の番号を割り当て、それでカルテと患者をひもづける。でも、患者にとったら何桁にもなる番号を覚えるのは大変なので、病院はこの番号を記録したカード、すなわち診察券を用意して、初診時に患者にわたす。

病院がウェブアプリで患者がブラウザ、カルテはウェブアプリが記録するセッション情報で、カルテと患者をひもづける番号がセッション ID という対応関係になる。

残るは診察券に対応するモノ(ブラウザ側にセッション ID をわたすための仕組み)だが、これには 3 つの方法がある。すなわち、(1) URL に埋め込む方式、(2) HTML のフォームに隠し要素として埋め込む方式、(3) cookie を使ってわたす方式、の 3 つだ。

練習も兼ねて、今回は (3) の方式でセッションを実現してみる。

サンプルプログラム

以下のサンプルプログラムでは、セッション情報(整数値 1 つ)は GAE の提供するストレージサービスの 1 つである memcache を利用している。このサービスはもう 1 つのデータストアとは違い、「揮発性」のストレージだ。容量も(データストアに比べればかなり)小さい。その代わり、ずっと高速に動作するらしい。頻繁にアクセスする少量のデータは、memcache に置く方が良い。

このプログラムを GAE アプリのリクエストハンドラにしてアクセスすると、「Back」と「Forward」という 2 つのリンクを持つページが開く。Forward をたどると memcache 中のデータ(カウンタ)がインクリメントされ、Back をたどればデクリメントされる。ただ、それだけのプログラムだが、内部的にはセッション管理がなされており、memcache に保持される値はクライアントごとに用意される。実際に複数のブラウザで開けばそのことがわかる。

memcache に保存するカウンタとは別に、データストアにも 1 つ値を保存している。これはセッション管理そのものとは関係ない。この値は、セッションを作るために必須の ID (先の病院と患者のたとえで言うなら、カルテと患者をひもづける番号だ) を作るための「種」になっている。

セッション管理の仕組み自体は単純で、リクエストを受けたら(ハンドラの get メソッドが呼ばれたら)、ブラウザが送ってきた cookie からセッション ID を取り出す。次にセッション ID をキーとして memcache からカウンタの値を取り出す。

ブラウザが cookie を送ってこなかった、あるいは(このプログラム用の)セッション ID がふくまれていないときは、新しくセッション ID を生成しレスポンスヘッダに入れる。

「Back」ボタンを実装するには

Blogger Glass (の iPhone 用画面)に「Back」ボタンを実装するには、画面を開くときにリクエスト URL をセッション情報として保存すれば良い。「Back」ボタンには専用のリクエスト URL を用意しておく(たとえば /back)。そのリクエストハンドラでは、セッション情報から保存されたリクエスト URL を取り出し、そこにリダイレクトする。

まだ、コードを書いていないから確信はないけれど、だいたいこんな感じで動きそうだ。

関連リンク

関連記事

twitter より (2010-10-30)

Powered by twtr2src.