Hatena::ブログ(Diary)

暗号、数学、時々プログラミング このページをアンテナに追加 RSSフィード

2008-04-22

設計作業に入ってみた (1)

| 08:28 | 設計作業に入ってみた (1)を含むブックマーク

f:id:hamatsu1974:20080420180532j:image

注意


このエントリーは先に公開した「そろそろ何かつくるかー。- Google App Engine」の中で述べられた「Project: スパイ大作先生」に関する検討内容をほぼリアルタイムで公開していく目的で作成されています。いきなりこの記事に出くわし、主旨が良く分からんという方は、まずはこちらの導入エントリーをご確認頂くコトをおススメします。


まずはクラスを抽出するッス


スパイ大作先生のシステムを構築するにあたって、重要なクラスや概念を抽出するところから詳細設計のスタートとしよう。どんなのがあるだろうか、、、

オブジェクトとクラスの候補】

クラス役割実例(オブジェクト
SecretMessage預けられるメッセージ情報「太郎より花子へ」
SpyMember会員の情報「太郎」
RestrictionDataメッセージ閲覧に際しての制限情報「閲覧は1回だけ」
Commentメッセージへ付与されたコメント情報「太郎より花子へ」へのコメント

で、上記で抽出した各クラスを、それぞれの関係性を踏まえてクラス図にマッピングしてみる。この時点ではDatastore API の仕様は特に意識せず、各オブジェクトに対してどんな要素とどんな操作が必要か?という観点でのみ整理するに止める。イロイロと考え出すと思考が発散してワケが分からなくなるので。操作(用意するメソッド)にしても、引数を複数にして1つのメソッドにまとめた方が使いやすい、とか、そういうのはとりあえず無視。そんなワケで今日もEclipse用のプラグインとして提供されているツール「AmaterasUML」のお世話になる。


f:id:hamatsu1974:20080422082800j:image


まあ、こんなものだろうか。


そんなワケで明日に続く。。。


一晩置いて、明日、もう一度このクラス図が適切なものかをチェックしていくことにしよう。

webapp を調べたよ (前編) - Google App Engine

| 03:36 | webapp を調べたよ (前編) - Google App Engine を含むブックマーク

【2009.02.11: 通りすがり様の指摘に伴い誤記を訂正】

何か分からないことがあった時にdiscussion Group で質問するにしても、やっぱドキュメント類に一通り目を通しておかないと顰蹙買うよな、、ということで、「Tool and Configuration」のセクションもじっくり読もうかと思って見てみたら、そんなに労力かけずとも訳できそうな感じだったので、APIsに引き続いて超訳作業を進めていくことにする。(まぁ、ここまでくると若干意地である。。)

最初のうちはチュートリアルの前編くらいしかアクセスがなく、みんな表面的な話に食いつくダケなんかなー、と思っていたが、意外と少しずつ他のエントリーにもアクセスが集まるようになってきてるみたい。それなりに需要はあるってコトかな?まぁ、学習のペースは人それぞれ。いつでも見れるのがWebのいいところってコトですな。

さて、今回はApp Engine 用に提供されているフレームワーク"webapp"について見ていく予定。例によって超訳なので、細かなニュアンスまで必要とされる方は原文にてご確認をお願いしたい。

そして、深夜通販番組風の紹介も流石に飽きてきたのでこのシリーズは鬼軍曹風でいこうかと。こういうノリの嫌いな方はあらかじめゴメンなさい。(軽くネタにさせて頂いた梅田望夫さんも、この手の冗談が通じる方であると祈ります…)


webapp フレームワーク、知りたいか?


おお、来たな。英語の出来ないクソったれどもが。ここまで来たからには、お前、プログラミングの腕にはそれなりに自信があるんだろう?正直、ちょっと英語が出来るくらいで、大したことないアプリ開発の記事なんか公開してブクマされまくってるヤツらにムカついてたんだろう? そんなお前に、これからとっておきをくれてやる。これでとっととお前がムカついてるヤツらを蹴散らしてやれ! ヤツ等の3倍のブクマをとって見せろ!

それから最後に言っておくぞ。この特訓の間、お前に許される発言はただ一言、「Google!」だ。試しに言ってみろ。 違う!「グーグル」じゃない! そんな事じゃあウメダモチオに「You はサンノゼじゃ生きていけないネー!」とか言われちまうぞ!

イイか? 「ぐーごぉ!」だ。

ちゃんとソレっぽく言え! ・・・よし。じゃあ、始めるぞ。


webapp フレームワーク


いいか、良く聞け。webapp フレームワークってのはApp Engine で使えるWSGI互換のWebアプリケーションフレームワークだ。だが勘違いするな。別にコイツを使わなきゃApp Engine でアプリが作れないってワケじゃない! App Engine のサーバCGIベースのPythonアプリなら動くんだからな。コイツはただ単に、お前らウスノロ共がApp Engine のアプリ開発にとっとと取り組めるように提供されてるだけってコトだ。有難くて涙が出るよなぁ?おい?


今回は次の流れで進めるぞ。脱落しないでついて来いよ!

【前編】

【後編】

  •  リファレンス
    •  Request クラス
    •  Response クラス
    •  RequestHandler クラス
    •  WSGIApplication クラス

用意はいいな? 返事は?

そう、「Google!」だったな。よく覚えてた。忘れるんじゃないぞ。


  webapp の概要


繰り返すがApp Engine はCGIインターフェイスを持つどんなPythonアプリケーションでもサポートしてる。Webアプリケーションフレームワークってのは、細かな点を任せちまえる事でアプリ開発の本質的な部分に集中させてくれるって代物だ。App Engine では、そんなフレームワークの1つとしてwebappってのが提供されてる。ここまではボンクラなお前の頭でももう理解できてるよな?


: Yesなら「Google!」と叫ぼう


webapp はWSGI互換のフレームワークだ。CGIアダプターを使ってるWSGIフレームワークならwebapp以外だって使えるぞ。例えばPython標準ライブラリで提供されてるようなものとかな。

要するに、この話は聞かなきゃ聞かないでもApp Engine のアプリは作れるってコトだ。

それでも聞くか?


: Yesなら「Google!」と叫ぼう


よし。まずはApp Engine 環境で動くwebapp を使ったアプリの例を見てみろ!

import wsgiref.handlers
from google.appengine.ext import webapp

class MainPage(webapp.RequestHandler):
 def get(self):
   self.response.headers['Content-Type'] = 'text/plain'
   self.response.out.write('Hello, webapp World!')

def main():
 application = webapp.WSGIApplication([('/', MainPage)],
                                      debug=True)
 wsgiref.handlers.CGIHandler().run(application)

if __name__ == "__main__":
 main()

雰囲気が掴めたら、詳細に進むぞ!ついて来い!!


: Yesなら「Google!」と叫ぼう


 アプリを動かせ!


チュートリアルを読んだヤツなら既に理解しているハズだが、webapp アプリケーションには以下の3つのパーツがある。

  • 1つ以上のRequestHandlerクラス(リクエストの処理とレスポンスの生成を担当)
  • WSGIApplicationクラスのインスタンスURLに従ってリクエストをルーティングする)
  • メインルーチン

WSGIApplication クラスってのはWSGI インターフェイスの実装だ。このWSGIインターフェイスってのはWebアプリケーションフレームワークとWebサーバ間の標準インターフェイスのことだ。もっと詳しく知りたいヤツはココでチェックしておけ。それから、WSGI CGIアダプターを使うWSGIフレームワークならApp Engineでの利用が可能になってる。Python標準ライブラリには wsgiref.handlers.CGIHandler ってアダプターが含まれてるぞ。詳しくはココを確認しておけ。

下の例では4つのURLパスと4つのRequestHandlerクラス(ソース上では見えず)のマップ関係を規定し、その後CGIアダプターを使ってアプリケーションを動かす形になっている。

from google.appengine.ext import webapp
import wsgiref.handlers

def main():
 application = webapp.WSGIApplication([('/', MainPage),
                                       ('/newentry', NewEntry),
                                       ('/editentry', EditEntry),
                                       ('/deleteentry', DeleteEntry),
                                      ],
                                      debug=True)
 wsgiref.handlers.CGIHandler().run(application)

WSGIApplication クラスのコンストラクタはRequestHandlerクラスとURLのマップ関係をとってるワケだな。

オプションで指定されている引数「debug=True」はアプリデバッグモードにするのに使われる。webappに対してハンドラが例外を上げた場合にはスタックトレースを表示するように指示するものだ。これがセットされていないデフォルトの状態では、単にHTTPステータスコード500のエラーが返るだけになってるぞ。

ついてこれてるか?ボンクラ


: Yesなら「Google!」と叫ぼう


URL マッピング

マッピングで使われるURLパスは正規表現ってコトになってる。正規表現における特殊文字はエスケープしなきゃならん。ちなみに、この正規表現には部分マッチングに使うためにregexpグルーピングを含めることができるようになってるぞ。この場合、マッチした部分が引数としてリクエストハンドラに渡されることになる。【修正箇所ココ】

class BrowseHandler(webapp.RequestHandler):

 def get(self, category, product_id):
   # Display product with given ID in the given category.

def main():
 # Map URLs like /browse/(category)/(product_id) to BrowseHandler.
 application = webapp.WSGIApplication([(r'/browse/(.*)/(.*)', BrowseHandler)
                                      ],
                                      debug=True)
 wsgiref.handlers.CGIHandler().run(application)
小技:App Engine は、アプリケーションの設定ファイルである「app.yaml」にて規定されているマッピング関係に基づいて各リクエストをPythonスクリプトへとルーチングする。Webapp の WSGIApplication クラスは特定のURLパスに対して更に細かいマッピング関係を規定する。2つをどのように使うかはお前次第だ。静的なURLを一切使わずに、スクリプトが生成する動的なURLをハンドラに引き渡すことも可能だ。あるいは、異なるスクリプトで動作する複数のWSGIアプリケーションをグルーピングすることもできるし、「app.yaml」を使ってアプリとURLのマップ関係を規定することもできる。

リクエストハンドラ


WSGIApplicationクラスがリクエストを受信した場合、そのURLに関連したRequestHandlerクラスのインスタンスを生成する。そして、そのインスタンスHTTPリクエストに対応したメソッドを呼び出す形になる(例えばHTTP Get の場合はget()メソッドが呼び出される)。そしてメソッドはリクエストを解析してレスポンスを用意し、返却する。そこでいよいよアプリクライアント側にレスポンスを送信するって流れになるわけだ。

下の例はGetリクエストを処理するリクエストハンドラを規定する場合の例だ。

class AddTwoNumbers(webapp.RequestHandler):

 def get(self):
   try:
     first = int(self.request.get('first'))
     second = int(self.request.get('second'))

     self.response.out.write("<html><body><p>%d + %d = %d</p></body></html>" %
                             first, second, first + second)
   except (TypeError, ValueError):
     self.response.out.write("<html><body><p>Invalid inputs</p></body></html>")

リクエストハンドラではHTTPリクエストに対応して以下のメソッドを使えるようになってるぞ。

  • get()
  • post()
  • head()
  • options()
  • put()
  • delete()
  • trace()

リクエストデータ


リクエストハンドラのインスタンスはrequestプロパティを使うことでリクエストデータにアクセスできるようになってる。コイツはアプリによってWebObの Requestオブジェクトとして初期化される。RequestオブジェクトはPOSTデータとクエリからパースされた引数を返却するget()メソッドが使えるぞ。ちなみにメソッドは第一パラメータ引数名として取り扱う。例えばこんな感じだ。

class MyHandler(webapp.RequestHandler):
 def post(self):
   name = self.request.get("name")

get()メソッドはリクエストに引数が含まれていない場合には、デフォルトでempty文字('')を返すようになってる。しかし、default_value パラメータが指定されている場合、リクエストに引数が含まれていないならばget()メソッドはempty文字の代わりにそのパラメータ値を返すようになってる。

それから、リクエスト内に引数が複数現れるような場合は、デフォルトではget()は先頭のものを返却する。複数現れる引数を全て使いたい場合には、get()メソッドに「allow_multiple=True」の引数を与えるようにしておけ。

# <input name="name" type="text" />
name = self.request.get("name")

# <input name="subscribe" type="checkbox" value="yes" />
subscribe_to_newsletter = self.request.get("subscribe", default_value="no")

# <select name="favorite_foods" multiple="true">...</select>
favorite_foods = self.request.get("favorite_foods", allow_multiple=True)
for food in favorite_foods:
 # ...

CGIパラメータの組み合わせではないBody部を持ったリクエスト(例えばHTTP PUT)については、Requestオブジェクトは「body」と「body_file」のattributesを用いる形になる。「body」はバイト列で表現されるbody部のコンテンツだ。そして「body_file」は同じストリーム形式(file-like)データへのインターフェイスを提供している。

uploaded_file = self.request.body

ああ、そうだ。WebObってのが冒頭からいきなり出てきて面食らったかも知れんが、コイツはオープンソースの3rdパーティー製ライブラリだ。詳しく知りたければココをチェックしておけ。


 レスポンスの組み立て


リクエストハンドラのインスタンスがresponse プロパティを用いてレスポンスを組み立てるようになってる。コイツはアプリによってemptyなWebObの Requestオブジェクトとして初期化される。Responseオブジェクトの out プロパティはレスポンスのBody部に格納されるストリーム形式(file-like)のオブジェクトだ。

class MyHandler(webapp.RequestHandler):
 def get(self):
   self.response.out.write("<html><body><p>Hi there!</p></body></html>")

out ストリーム はメモリ内に全てのアウトプットをバッファし、最終アウトプットをハンドラへと送る形になる。Webappはクライアントへのストリーミングデータのサポートはしていないから気をつけておけよ。

そして、clear()メソッドはoutput バッファの中を消し去るのに使われるもんだ。

それから、output ストリームに書き出されるデータがUnicodeだった場合(またはレスポンスが「; charset=utf-8」で終わるContent-Typeヘッダを含んでいる場合)、webapp はアウトプットをUTF-8エンコードするようになってる。デフォルトではContent-Typeヘッダは「text/html; charset=utf-8」であると見なされるぞ。但し、Content-Typeに他の文字コードが指定されてた場合は、webapp はそのまま何も手は加えないようになってるから注意しておけ。

さらに詳しい情報についてはココをチェックしておけよ。

どうだ? まだついてこれてるか?


: Yesなら「Google!」と叫ぼう


リダイレクト、ヘッダ、ステータスコード


通常はレスポンスはHTTPステータスコード200を含んでいる。知っての通りコイツはOKを意味するものだな。具体的にはリクエストに使われたURIは正しく、レスポンスには所望のリソースが格納されてる、ってコトだ。そして状況が異なればエラーコードは違うものになる。例えば、サーバ内部エラーに関するものならエラーコードは「サーバエラー」を示す500になるワケだ。

リクエストハンドラではerror(...)メソッドを使ってエラーコード付きのエラーレスポンスを返すことができる。例えばこんな形だ。

class MyHandler(webapp.RequestHandler):
 def get(self):
   self.response.out.write("You asked me to do something.")
   try:
     doSomething()
     self.response.out.write("It's done!")

   except Error:
     # Clear output and return an error code.
     self.error(500)

error(...)メソッドは数字であらわすHTTPステータスコードを使ってリクエストハンドラのレスポンスを組み立てるものだ。ついでにoutputバッファのクリアも行うようになってる。従って、ハンドラはoutを使って成功時用のアウトプットを組み立て、問題が行った場合にはerror(...)を呼び出すような流れにすることが出来るってワケだな。

その他に考えられる一般的なステータスコードの利用法は、異なったURIへのユーザのリダイレクションだ。このリダイレクションは永続性を持つものとしても利用可能だ。つまり、以降のリクエストについては常に新しいURIを利用させるようにするってコトだな。もちろん、あくまで一時的なものとしてリダイレクションを使うこともできる。Webアプリケーションの一般的なテクニックとして、formの送信に成功した場合のレスポンスに一時的なリダイレクションを使うってのがあるな。コイツは、ユーザが間違ってブラウザの戻るボタンを押し、再度formを送信するようなケースを避ける為に考えられたアイデアだ。

RequestHandlerクラスはリダイレクトレスポンスの為にredirect(...)メソッドが使える。

使い方はこんな形だ。

class FormHandler(webapp.RequestHandler):
 def post(self):
   if processFormData(self.request):
     self.redirect("/home")
   else:
     # Display the form, possibly with error messages.

redirect(...)メソッドは第1パラメータを飛ばす先のURIとして扱う。デフォルトで用いられるのは一時的なリダイレクションだ。オプション引数である「parmanent=True」を使うことで、永続的なリダイレクションのコードを扱う形になってるぞ。

RequestHandlerクラスのメソッドであるerror(...)とredirect(...)はレスポンスに使われるステータスコードを変える。Redirect(...)メソッドの場合、さらにHTTPヘッダを使ってクライアントへ新しいURIを通知する。

それからResponseオブジェクトステータスコードHTTPヘッダを直接していするメソッドを提供しているぞ。

まず、Responseオブジェクトのset_status(...)メソッドはレスポンスのステータスコードを変更するのに使われる。この場合、メソッドの第1パラメータには数字のステータスコードを使う。オプションとして第2パラメータには与えられたステータスコードに対して表示するメッセージを指定するようになってるぞ。デフォルト値ではないものにしたい場合に使うものだな。

Responseオブジェクトのheadersプロパティはwsgiref.headers.Headers クラスのインスタンスで、レスポンスのHTTPヘッダを格納するものだ。ヘッダをどのようにセットするか?の詳細についてはココを確認しておけ。

例は以下の通りだ。

class StatusImageHandler(webapp.RequestHandler):
 def get(self):
   img_data = get_status_image_for_current_user()
   self.response.headers["Content-Type"] = "image/png"
   self.response.headers.add_header("Expires", "Thu, 01 Dec 1994 16:00:00 GMT")
   self.response.out.write(img_data)


締め


と、いうことで明日はwebapp で提供される各種クラスのリファレンスについて学んでいくぞ。気合入れて待ってろよ!

返事は? そうだ。「Google!」だ。明日もこの調子で行くぞ!!

umedamochioumedamochio 2008/04/22 04:05 どうぞ自由に使ってください(笑)。僕の例で言いますと、英語の引用を含めた文章を書き、10,000人がアクセスしたとする。あとで読もうと思う人と、英語は読み飛ばして日本語の地の文だけをさらっと読む人が9割(9,000人)。残り1割のうちの9割がざっと英文も眺めるくらい。きちんと英文を読む人の中で、リンク先までちょっと行ってみる人はその1割。リンク先までちゃんと読む人は、またその1割。10,000人に1人という感じでしょうか。それが皮膚感覚です。

hamatsu1974hamatsu1974 2008/04/22 04:59 コメント&使用許可(?)恐縮です。なるほど。原文まで辿って行ったとしても、しっかり読む人はさらに減る、、、そうかも知れませんね。そういえば私も「5つの定理」を読んだ時は、一巡目は英語を読み飛ばして日本語だけで読み、二巡目で初めてしっかりと英語を読んだ気がします。なんというか、言語が混ざると読書リズムが狂うので。。

通りすがり通りすがり 2008/12/17 14:38 >ちなみに、この正規表現には部分マッチングに使うためにregexpグルーピングを含めることができるようになってるぞ。この場合、マッチしなかった部分が引数としてリクエストハンドラに渡されることになる。

マッチした部分ですよね

hamatsu1974hamatsu1974 2009/02/11 08:33 > 通りすがり様

すみません。コメントに今気づきました。。。

原文を見ると「Patterns matched in groupings are passed to request handlers as arguments.」とあるので、ご指摘の通り「マッチした部分が」が正解ですね。なんでこうしてしまったのか、今となっては全く記憶にないのですが、修正しておきます 汗

nakanushinakanushi 2011/11/29 22:56 つい、1週間前からプログラミングをはじめました。
環境構築で手間取り、やっと、流行り(?)のGAE+pythonでやってみようとしたところ、
こちらにいきつきました。僕の知識が足らず、用語的にわからない箇所はあったものの、
どこよりもわかりやすかったです!ありがとうございます。