2010-10-16

クラス変数とインスタンス変数 (Python編)

恥ずかしい告白をしなければならない。今日、「初めてのPython」の 「23 章 クラスのコーディング (基礎)」を読むまで、Python におけるクラス変数とインスタンス変数の記述の仕方を混同していた。というより、class ステートメントの内側に記述した(代入した)変数は、すべてインスタンス変数になると勘違いしていたのだ。

ここで、「クラス変数」とは、クラスのインスタンスにではなく、クラス自体が(つまりクラスオブジェクトが)持つ変数のことで、そのクラスのすべてのインスタンスオブジェクトで共有されるものを指す。一方で「インスタンス変数」は、それぞれのインスタンスオブジェクトに固有の変数のことだ。「初めてのPython」では、変数ではなく属性という言葉で表現しているが、この記事内では(わたし自身に馴染のある)変数という言葉で呼ぶことにする。

混同の原因は表記によるもの?

Python ではクラス変数は、

(クラス変数)
class Foo:
    value = 100

のように定義する。一方で、インスタンス変数は、以下のようにメソッド中で self に関連付けて定義する。

(インスタンス変数)
class Bar:
    def __init__(self, val):
        self.value = val

ややこしいのは、どちらも <インスタンスオブジェクト>.<変数名> としてアクセスできることが。つまり、以下のようなコードが書ける(FooBar は上述の定義のもの)。

(クラス変数とインスタンス変数へのアクセス)
foo = Foo()
bar = Bar(300)
print foo.value, bar.value

これを実行すると、

100 300

と表示される。本来、Foo クラスのクラス変数 value へのアクセスは Foo.value と表記すべきだが、Foo クラスのインスタンスを通して foo.value でも参照できるのだ。さらにややこしいことに、foo.value に対して代入すると、今度はクラス変数の value への代入ではなく、新しく value という名のインスタンス変数が定義される。つまり、その時点で foo オブジェクトはクラス変数としての value とインスタンス変数としての value の両方を持つオブジェクトに変わる。ただ、代入でインスタンス変数が定義されるのは foo オブジェクトに対してだけなので、新しく Foo クラスのインスタンスを作ったなら、そのオブジェクトはクラス変数の value しか持っていない。

ああ、ややこしい。

サンプルプログラム

このことを確かめるためのサンプルプログラムを書いてみた。Foo クラスは上述のものと同じ。一方、Bar は同名のクラス変数とインスタンス変数を持ったものにしてある。

(variables.py)
 1: #!/usr/bin/python2.5
 2: 
 3: class Foo:
 4:     value = 100
 5: 
 6: class Bar:
 7:     value = 200
 8:     def __init__(self, val):
 9:         self.value = val
10: 
11: print "Foo:"
12: foo1 = Foo()
13: foo2 = Foo()
14: print "foo1.value = %d, Foo.value = %d" % (foo1.value, Foo.value)
15: print "foo2.value = %d, Foo.value = %d" % (foo2.value, Foo.value)
16: 
17: print "---- set 200 to foo1.value"
18: foo1.value = 200
19: print "foo1.value = %d, Foo.value = %d" % (foo1.value, Foo.value)
20: print "foo2.value = %d, Foo.value = %d" % (foo2.value, Foo.value)
21: 
22: print "---- set 50 to Foo.value"
23: Foo.value = 50
24: print "foo1.value = %d, Foo.value = %d" % (foo1.value, Foo.value)
25: print "foo2.value = %d, Foo.value = %d" % (foo2.value, Foo.value)
26: 
27: print "Bar:"
28: bar1 = Bar(300)
29: bar2 = Bar(400)
30: print "bar1.value = %d, Bar.value = %d" % (bar1.value, Bar.value)
31: print "bar2.value = %d, Bar.value = %d" % (bar2.value, Bar.value)
32: 
33: print "---- set 500 to bar1.value"
34: bar1.value = 500
35: print "bar1.value = %d, Bar.value = %d" % (bar1.value, Bar.value)
36: print "bar2.value = %d, Bar.value = %d" % (bar2.value, Bar.value)
37: 
38: print "---- set 600 to Bar.value"
39: Bar.value = 600
40: print "bar1.value = %d, Bar.value = %d" % (bar1.value, Bar.value)
41: print "bar2.value = %d, Bar.value = %d" % (bar2.value, Bar.value)

これの実行結果は以下のようになる

[imac] mnbi% python2.5 variables.py
Foo:
foo1.value = 100, Foo.value = 100
foo2.value = 100, Foo.value = 100
---- set 200 to foo1.value
foo1.value = 200, Foo.value = 100
foo2.value = 100, Foo.value = 100
---- set 50 to Foo.value
foo1.value = 200, Foo.value = 50
foo2.value = 50, Foo.value = 50
Bar:
bar1.value = 300, Bar.value = 200
bar2.value = 400, Bar.value = 200
---- set 500 to bar1.value
bar1.value = 500, Bar.value = 200
bar2.value = 400, Bar.value = 200
---- set 600 to Bar.value
bar1.value = 500, Bar.value = 600
bar2.value = 400, Bar.value = 600

14 〜 15 行目の出力で、Foo のクラス変数をインスタンスオブジェクト経由で参照できることがわかる。さらに 18 〜 20 行目では、<インスタンスオブジェクト>.<変数名> という表記に対する代入がインスタンス変数を(そのオブジェクトに対してのみ)定義することがわかる。また、23 〜 25 行目では、明示的にクラス変数を指定した代入が可能なこともわかる。

Bar を使った例では、クラス変数とインスタンス変数が同名の場合、<インスタンスオブジェクト>.<変数名> という表記ではインスタンス変数の方が優先されて参照されることがわかる。

これまでに書いたコードへの影響

この(恥ずかしい)混同のため、Blogger Glass のコードではクラス変数とインスタンス変数の区別が付いていない。よくもまあ動いているものだ。

実際のところは、参照だけなら(初期化後に一切代入していないなら)クラス変数であれ、インスタンス変数であれ、複数のインスタンスオブジェクトから参照されたとしても問題にならない。また、メソッドの中で(self 経由で)代入した途端、インスタンス変数が作られるのだから、他のインスタンスに影響を及ぼすこともない。ただ、意図しないところでインスタンス変数が作られたり、そのおかげでクラス変数が無駄になっていたりするだけだ。

たとえ影響がないとしても直さないと……。みっともないからな。

参考文献

初めてのPython 第3版
Mark Lutz
オライリージャパン ( 2009-02-26 )
ISBN: 9784873113937
おすすめ度:アマゾンおすすめ度

twitter より (2010-10-15)

Powered by twtr2src.

2010-10-15

GAE アプリでユーザごとの設定を可能にする #5

今回は、新機能のメインとなる、ストレージサービスを使ってユーザごとの設定を保存する部分を作り込む。

データモデルの定義

今のところ「設定画面(ビュー)」で設定可能なのは「Blog ID」のみ。したがって、データモデルもこの ID を保持するプロパティだけで良い。型は文字列(→ StringProperty)。

ただ、管理とデバッグが容易になると想定して、「account」と「mod_time」を加えておく。この 2 つは、画面からは変更できないもので、それぞれ(ログインしている)ユーザ情報と設定が変更された日時を保持するために用いる。

config と setting

「設定画面」は別にして、他の画面では設定項目がストレージサービスに保管されたユーザ設定(の項目)なのか、あるいは(これまでの)構成情報(config.yaml)に書かれたものなのかを意識したくはない。このため util.get_settings() を用意し、ユーザ設定と構成情報をラップすることにした。

具体的には、以下のようにして「Blog ID」にアクセスできる。これで、ログインしていないとき、あるいはログイン中であってもストレージに設定が保管されていないときなどのように、ユーザ設定として「Blog ID」の値が存在しないときは、構成情報(config.yaml)に書き込まれたものが返される。

(設定情報の利用方法)
blog_id = util.get_settings().get('blog_id')

設定画面

画面は「GAE アプリでユーザごとの設定を可能にする #3」に書いた通りの簡単なもの。

リクエストハンドラ(SettingsViewHandler)の get メソッドは、これまでの他のハンドラのものと大差ない。違いは、フィードではなくユーザ設定の内容を表示するという点だけだ。また、このハンドラ内では上述のラッパーを利用せず、データモデルとしてのユーザ設定に直接アクセスしている。

一方、このハンドラはフォームからの POST 要求を扱うための post メソッドが定義されている。「Save Settings」か「Delete Settings」かを判別し、model.UserSettings.put()model.UserSettings.delete() を呼ぶコードになっている。また、「Save Settings」の処理では、データのごく簡単な検証も行っている(空文字列ではないか、と同じ文字列が指定されていないか、の 2 点だけ)。

その他の変更

app.yaml

設定画面はログイン状態であることが前提なので、「login: required」を付けている。また、今回からアプリバージョンを 2 に上げた。

(app.yaml)
application: bloggerglass
version: 2
[...snip...]
handlers:
[...snip...]
- url: /settings/.*
  script: settingsview.py
  login: required
[...snip...]
info.py

設定画面用の SettingsViewInfo の定義を追加した。

main.py と postview.py

これまで構成情報(config.yaml)を参照していた「blog_id」を、上述のようにユーザ設定と構成情報をラップした util.get_settings() 経由で参照するように変更した。

これでバージョン 2

すでに書いたように、今回の配備からapp.yaml 中のアプリバージョンを 1 から 2 に上げた。これには、今回の機能追加ではユーザサービスとストレージサービスを使うようになり今までとは違う、という意味を込めたつもりだ。

バージョンを上げる際には管理コンソールでの操作が必要になる。アプリバーションを上げた app.yaml を配備するだけでは、アプリのデフォルトバージョンは旧バージョンがのままになっている。新バージョンをデフォルトにするには、アプリの管理コンソールから「Administration」>「Versions」を開き、新バージョンを選んで「Make Default」ボタンを押す。

ストレージサービスを使うことで、コードだけではなく、サーバ上に保管されたデータの管理も必要になる。実はまだ、データの保守管理についてはほとんど調べていない。たとえば、データモデルを更新した(プロパティの追加等)場合に、従来のデータをどうやって更新すれば良いのか、データのバックアップやリストアは可能なのか、といったこともわからない。この先、設定可能項目を増やす際には、アプリ外でのデータの取り扱いについて調べる必要がある。

関連リンク

関連記事

2010-10-14

GAE アプリでユーザごとの設定を可能にする #4

今回は、「設定」画面を追加する前の最後の下準備として、メニューを追加する。また、ログイン(とログアウト)にも対応する。

メニューの追加

ここで言うメニューとは、BG の主要機能のリクエスト URL へのリンクのことだ。具体的な項目については「GAE アプリでユーザごとの設定を可能にする #3」を参照のこと。

ウェブアプリにおけるメニューは、特定のリクエスト URL へのリンクだ。具体的に言えば、「List」は "/" への、「Settings」は "/settings/" へのリンクとなる。

メニューの項目とその並びの情報を保持するため専用のオブジェクトを定義する。

(メニュー用オブジェクト)
class Menu:
    class Item:
        name = ""
        request_url = ""
        def __init__(self, name, request_url):
            self.name = name
            self.request_url = request_url

    def __init__(self):
        self.menu_items = []
        self.menu_items.append(self.Item('List', '/'))

    def menu(self):
        return self.menu_items

メニュー項目が増えたら、Menu オブジェクトの初期化(__init__)で、menu_items に追加する。

これを画面に表示するためにテンプレートには以下のような記述を追加する。このとき、リクエストハンドラからテンプレートにわたす AppInfo オブジェクトに Menu.menu() の戻り値をセットしておく。

(テンプレート内のメニュー表示)
<div id="top-menu">
  {% for item in app.menu %}
  <span class="menu-item"><a href="{{ item.request_url }}">{{ item.name }}</a></span>
  {% endfor %}
</div>

ログインによるメニュー項目の切り替え

Menu オブジェクトの変更点

ログインしているかどうかに応じてメニュー項目を切り替えるために、上述の Menu オブジェクトの定義を以下のように変更する。

(Menuオブジェクト; ログイン対応版)
class Menu:
    [...snip...]
    def __init__(self, request_url):
        self.menu_logged_in = []
        self.menu_logged_out = []
        # menu items those are enabled when a user logged in.
        [...snip...]
        self.menu_logged_in.append(self.Item('Logout',
                                             users.create_logout_url(request_url)))
        # menu items those are enabled when a user does not logged in.
        [...snip...]
        self.menu_logged_out.append(self.Item('Login',
                                              users.create_login_url(request_url)))

    def menu(self):
        user = users.get_current_user()
        if user:
            return self.menu_logged_in
        else:
            return self.menu_logged_out

初期化時に request_url をわたしているのは、ログインとログアウト用の URL を生成するため(ログインまたはログアウト画面からの戻り先になる)。この値として、メニューの生成時にはリクエストハンドラから self.request.uri をわたす必要がある。このため、fill_appinfo に引数を増やした。

(util.py; fill_appinfo)
def fill_appinfo(appinfo, request_url):
    [...snip...]
    appinfo.menu = Menu(request_url).menu()
リクエストハンドラの変更点

リクエストハンドラ側の(fill_appinfo の)呼び出しは以下のように変更した。

(リクエストハンドラの変更点)
class MainHandler(webapp.RequestHandler):
    [...snip...]
    def get(self):
        fill_appinfo(self.appinfo, self.request.uri)
        [...snip...]
テンプレートの変更点

メニューを表示するため、各画面のテンプレートに以下の記述を追加する。

(テンプレート内のメニュー表示部分)
<div id="top-menu">
  {% for item in app.menu %}
  <span class="menu-item"><a href="{{ item.request_url }}">{{ item.name }}</a></span>
  {% endfor %}
</div>

実装の方針

一覧表示も内容表示もログインの状況には無関係だ。ログインしていようがいまいが機能の動作に変化はない。だから、リクエストハンドラとテンプレートにもログインを判別するコードは入れないようにした。つまり、ハンドラ内で判別してメニューを切り替えるのではなく、メニュー自体がログインの状況を判別するようにした、ということ。またこれは、すべてのリクエストハンドラに(ログイン判定という)同じコードが入ることを防ぐためでもある。

関連記事

twitter より (2010-10-13)

  • 10:47  む、MobileMe でパスワードが違うと言われる。変更してないぞ。何かトラブッてるのか?
Powered by twtr2src.

2010-10-13

Blogger Glass をリファクタリングする #2

「設定」画面を付け加えると、リクエストハンドラがもう 1 つ増えることになる。「BG をリファクタリングする」で書いたように、BG の画面の構成要素には共通部分が多い。当然、それを作るハンドラにも共通部分が多くなる。新しいハンドラを作る前に「コピペプログラミング」に陥らないよう、共通部分のくくり出しを行おう。

今回のお題は「リクエストハンドラから共通部分をくくり出す」だ。

リクエストハンドラから共通部分をくくり出す

今回のリファクタリング対象は、google.appengine.ext.webapp.RequestHandler を共通の親クラスに持つ、2 つのリクエストハンドラ・クラスだ。

(google.appengine.ext.webapp.RequestHandler)
    +--- MainHandler
    +--- PostViewHandler

リファクタリングの方針としては以下の 2 通りのものが考えられる。

  1. 共通部分を、google.appengine.ext.webapp.RequestHandler を継承する中間クラスに移動し、各リクエストハンドラはこの中間クラスを継承する。
  2. 共通部分を別クラスや関数として独立させ、各リクエストハンドラから利用する。

前者は「継承」によるリファクタリングで、後者は「移譲」によるリファクタリングだ。どちらにも一長一短はあるが、今回はリファクタリングの主眼は実装の整理なので、「移譲」を使ってみる。一般的に言って、継承階層の中間にクラスを挟み込む方法は、制御の流れをわかりにくくするので、デバッグしづらくなる(BG 程度の規模ならあまり影響はないが)。また、単体テストという観点から言っても(BG ではやってないけど)、インスタンス化しづらい(できないことも多い)中間クラスは避けた方が良い。

責務分析

上述の 2 つのリクエストハンドラは、GET リクエストに対応する get メソッドのみを実装しており、どちらのクラスでもそれは Atom フィードを取得、解析し、その内容をもとに画面に表示する情報を組み上げるようになっている。

  1. 画面の共通エリア(アプリ固有情報)を作成する。
  2. リクエスト情報からフィードを取得するためのパラメータを取り出す。
  3. フィードを取得し、解析を行う。
  4. フィードのデータから画面(ビュー)に固有の内容を組み立てる。
  5. テンプレートをロードし、画面表示を生成する。

このうち、3. はすでに外部クラス(feed.core.AtomFeed 等)に移譲されている。また、5. はもともとテンプレートエンジン(Django)の役割だ。

残りの 3 つのうち、リクエストハンドラごとに異なるのは 2. のパラメータ取り出しと 4. の固有の表示内容の組み立てだ。言い換えると、1. の共通エリアのための情報の作成が共通部分としてくくり出せる責務になる。また、リファクタリング後に行う「設定」画面の追加では、ログイン/ログアウトの仕組みを組み込むことになるが、これも 1. にふくまれると言って良い。

リファクタリングの実際

util.py

今回のリファクタリングで追加したモジュール。以下の 2 つの関数が定義されている。

util.py で定義されている関数
関数名 機能
fill_appinfo(appinfo) アプリ固有情報を作成する。引数には info.AppInfo のインスタンスがわたされることを想定している。
render_template(handler, filename) google.appengine.ext.webapp.template.render を呼び出す。関数というよりマクロ。
main.py

__init__ で行っていたアプリ情報の作成が util.fill_appinfo に移ったので、__init__ は削除した。また retieveutil.fill_appinfo にあわせて名前を fill_viewinfo に変更した。

postview.py

main.py と同様の変更なので、説明は省略。

関連リンク

関連記事

2010-10-12

GAE アプリでユーザごとの設定を可能にする #3

GAE が提供するユーザサービスデータストアサービスの使い方が必要なだけはわかったので、Blogger Glass に追加する機能の具体的なデザイン(意匠と設計)に入ろう。

設定画面

設定画面には、設定可能な項目ごとに現在の値と編集のための入力フィールドが表示される。また、画面には「保存(Save Settings)」と「キャンセル(Cancel)」、さらに「削除(Delete)」の 3 つのボタンがある。それぞれの役割は以下の通り。

ボタン 機能
保存 (Save Settings) 編集した値を保存する。保存後、元の画面に戻る。
キャンセル (Cancel) 編集内容を破棄して元の画面に戻る。
削除 (Delete) 設定自体を削除するためのものだ。削除後、元の画面に戻る。

いずれの場合も、元の画面に戻ることが「ユーザ体験」上意味がないのであればトップ(一覧表示)画面に戻る。

画面遷移

これまでの「一覧表示(List)」、「内容表示(Post)」の 2 つの画面(ビュー)に加え「設定(Settings)」が加わる。3 つの画面間の遷移は以下のようになる。(1) が List、(2) が Post、(3) が Settings を表している。

Blogger Glass: 画面(ビュー)遷移

画面遷移の種類は以下の 4 つ。始めの List -> Post は従来からある遷移で、記事一覧の各項目のリンクをたどって個別の記事内容を表示させるときのものだ。

  • List -> Post
  • List -> Settings
  • Post -> Settings
  • Settings -> List

以下で、新しく増える 3 つの遷移について説明する。

List -> Settings

一覧表示画面から設定画面に移る遷移。一覧表示画面中の「設定」リクエストを発行するリンクをたどることで起こる。設定画面はログイン前提だから、必要に応じて「ログイン画面」が遷移中に挟まる。

Post -> Settings

内容表示画面から設定画面に移る遷移。一覧表示画面中の「設定」リクエストを発行するリンクをたどることで起こる。設定画面はログイン前提だから、必要に応じて「ログイン画面」が遷移中に挟まる。

Settings -> List

一覧表示画面から設定画面に移ってきた場合、設定の保存またはキャンセルを選ぶと、元の画面(一覧表示)に戻る。そのときの遷移だ。

「Settings -> Post」がない理由

「Settings」ではブログを指定する「Blog ID」を切り替えることができる(ようになる予定)だ。ところが、「Blog ID」が変わると、「Post」で記事を指定している「Post ID」が変わる。だから、単純に元の「Post」画面(の URL)に戻ると、記事が存在しないことになり、空の画面が表示される。

そもそもブログが変わっているのだから、記事の内容表示画面に戻る意味がない。

もっとも、「Settings」で設定できる項目が増えたなら、ブログの切り替えを伴わない変更も出てくる。その場合は「Post」に戻る方が(ユーザ体験として)使いやすい。この件は、設定項目が増えたときに、もう一度考えることにしよう。

メニュー

これまでは特に機能を実行するメニューを用意していなかったが、今回から「ログイン(ログアウト)」および「設定」機能が追加されるため、メニューを用意する。言うまでもなく、メニューだから全画面で共通に表示される。

メニュー自体は全画面で共通だが、項目はその画面と状況で有効なものだけが表示される。

メニュー項目 (並び順)
項目 機能
List 一覧表示画面に移る。ログインの状態に関係なく、この項目は常に表示される。
Settings 設定画面を開く。設定機能はログイン前提なので、ログインしていない状態ではこの項目は表示されない。
Login ログインする。ログイン画面に移る。ログインしている状態ではこの項目は表示されない。
Logout ログアウトを実行する。ログイン必須の画面で実行した場合、トップ(一覧表示)画面に戻る。ログインしていない状態ではこの項目は表示されない。

リクエスト形式

「設定」画面への遷移を行うリクエストを追加する。現時点のすべてのリクエスト形式を以下に示す。

リクエスト 機能
/ page=0 を指定した場合と同様。
/?page=<number> number で指定したページの一覧を表示する。
/post/ 最新の記事の内容を表示する。
/post/?id=<id> id で指定した記事の内容を表示する。
/settings/ 設定変更のための画面を開く。

関連リンク

関連記事

twitter より (2010-10-11)

Powered by twtr2src.

2010-10-11

MacBook SMC ファームウェア・アップデート

MacBook につないだ iPod touch の同期(と充電)が完了したので、MacBook のふたを閉じようとしたところ、ソフトウェアアップデートがあるぞ、というダイアログに気付いた。詳細を表示させてみたところ、こういうことだった(↓)。

MacBook SMC ファームウェア・アップデートの詳細

MacBook でファームウェアのアップデートは初めてかもしれない。早速、インストールと再起動を行う。ファームウェアのアップデートなので、いつもの OS のアップデートとは(↑の詳細に書かれているとおり)以下の点が異なる。

  • 再起動時に「グレイの画面にアップデートの進行状況を示すステータスバーが表示され」ること
  • 「アップデート中はコンピュータのファンが全速で回転しますが、アップデート完了後に通常の速度に戻」ること

ステータスバー(プログレスバーって言う方が良いんじゃないか?)はともかく、ファンのことは注意書きを読まなかったらビビってただろうな。

アップデート完了後に、どんな問題が解決されるのかを見に行ってみた(アップデートする前に見ておくべきなんだよ、本当は)。それによれば、……

(TS3499 より)
In certain situations when you connect a new 60W or 85W MagSafe power adapter to the MagSafe port of an older MacBook or MacBook Pro computer, the adapter may not charge the computer and the LED indicator light on the MagSafe connector of the adapter may not illuminate.

……ということ。新しい電源アダプタを古い MacBook や MacBook Pro につなぐと、充電されないことがある、ってことらしい。具体的にどれぐらい古いものが影響を受けるのかは、リストがついている。

(TS3499 より)
Products Affected
MacBook (13-inch Late 2007), MacBook Pro (17-inch, Early 2008), MacBook Pro (15-inch Early 2008), MacBook (13-inch, Early 2008), MacBook (13-inch, Late 2008)

ウチの MacBook (Early 2008) も確かにリストに入っている。けれど、新しい電源アダプタを持っていないから、症状が出ることはなかっただろうな。

関連リンク

関連記事

2010-10-10

開発中の Google App Engine 用アプリに iPhone/iPad から接続する

Mac 版 Google App Engine SDK の GoogleAppEngineLauncher.app から開発中のアプリを起動した場合、localhost 以外からは接続できない。Mac や PC から(もちろんブラウザで)使うためのアプリであれば問題にはならない。しかし、開発しているアプリが iPhone や iPad をターゲットとしているものだったら?

開発中の GAE アプリに iOS デバイスから接続したい場合、上記の問題への対処方法は 2 つある。

  1. iPhone シミュレータを使う
  2. GAE アプリをオプションつき(サーバのアドレスを指定する)で起動する

1. については特に言うことはない。ただ iPhone シミュレータを起動して Mac の Safari で使うのと同じアドレスを開くだけ。

iPhone や iPad の実機を使いたい場合には 2. の方法になる。以降では、この方法について説明する。

GAE アプリをオプションつきで起動する

dev_appserver.py を直接起動する

dev_appserver.py は、その名の通り開発用のアプリケーションサーバだ。引数には起動したい GAE アプリのディレクトリ(app.yaml が置かれているディレクトリ)を指定する。

GAE Launcher の初回起動時に「シンボリックリンクを張るか?」という問いに「Yes」と答えていれば/usr/local/bin に(シボリックリンクが)置かれている。「No」と答えていれば、GAE Launcher のメニューから「GoogleAppEngineLauncher」>「Make Symlinks...」を実行すれば後からでもシンボリックリンクを張ることができる。/usr/local なんかを作りたくない、というなら以下の場所に実体がある。

/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/dev_appserver.py

さて、この dev_appserver.py には、アプリサーバのアドレスやポート番号を指定するオプションがある。このアドレスを指定するオプション(--address)に Mac の LAN 上で有効な IP アドレスを指定することで、localhost (IP アドレスで書くと 127.0.0.1) 以外のクライアントからのアクセスできるようになる。無線 LAN でアクセスできれば、iPhone や iPad からでも OK。

[imac] mnbi% /usr/local/bin/dev_appserver.py --port=8080 --address=192.168.0.1 /somewhere/helloworld

iPhone や iPad から名前解決できるのであれば(LAN 内にプライベートな DNS サーバが稼動している等)、アドレスとしてホスト名を指定しても良い。

[imac] mnbi% /usr/local/bin/dev_appserver.py --port=8080 --address=imac.private /somewhere/helloworld

Mac に割り当てられている IP アドレスがわからない(調べるのがメンドウ)、さらには名前解決もできないというなら、「ワイルドカード」アドレスを指定することもできる。

[imac] mnbi% /usr/local/bin/dev_appserver.py --port=8080 --address=0.0.0.0 /somewhere/helloworld

Python のバージョンが気になるなら、明示的にインタープリタを指定して dev_appserver.py を起動すれば良い。まとめると、iOS デバイス(実機)で接続したい場合の GAE アプリの起動方法は以下のようになる。

[imac] mnbi% python2.5 /usr/local/bin/dev_appserver.py --port=8080 --address=0.0.0.0 /somewhere/helloworld

ちなみに、dev_appserver.py にはこの他にもデバッグ用のロギングを有効にする --debug オプションもある。これはデフォルトでは無効になっているが、有効にすれば自分で書いたアプリ内のデバッグログが表示されるとともに、リクエストに関する詳細な情報が表示されるようになる。

GAE Launher でアプリ起動時のオプションを指定する

実は GAE Launcher でも、上述のオプションを指定することができる。そのためには Launcher でオプションを指定したいアプリを選び、メニューから「Edit」>「Application Settings...」を実行する。以下のパネルが現れるので、下部にある「Lauche Settings」の「Extra Flags」にオプションを記述すれば良い。

GoogleAppEngineLauncher.app のアプリ設定編集パネル

ターミナルから dev_appserver.py を直接起動するより、こちらの方が簡単か。

アドレス指定の謎

ところで、dev_appserver.py--address オプション経由でわたされたアドレスはどう使われるのだろうか? もっとはっきり言えば、サーバのアドレスを指定することで何が変わって、LAN につながった他の機器からの接続を受け付けるようになるのか。疑問に思ったので、dev_appserver.py からライブラリ(Python 2.5 用)のソースをたどってみた。その過程でわかったことだが、--address オプションを指定しない場合、デフォルト値として "localhost" が使われる。

dev_appserver.py
    (dev_appserver_main.py が実行される)
google/appengine/tools/dev_appengine_main.py
    ParseArguments(argv):
        [...snip...]
        if option in ('-a', '--address'):
            option_dict['ARG_ADRESS'] = value
        [...snip...]
    main(argv):
        args, option_dict = ParseArguments(argv)
        [...snip...]
        serve_address = option_dict[ARG_ADDRESS]
        [...snip...]
        http_server = dev_appserver.CreateServer(..., serve_address,...)
        [...snip...]
google/appengine/tools/dev_appserver.py
    CreateServer(..., serve_address,...):
        [...snip...]
        server = HTTPServerWithScheduler((serve_address, port), handler_class)
        [...snip...]
    class HTTPServerWithScheduler(BaseHTTPServer.HTTPServer):
        [...snip...]
        def __init__(self, server_address, ...):
            [...snip...]
            BaseHTTPServer.HTTPServer.__init__(self, server_address,
                                              request_handler_class)
        [...snip...]

ざっと流れをたどると、dev_appserver.py にわたされた --address オプションの値は、dev_appserver_main.pydev_appserver.py (最初のとは別物) を経て、ビルトインライブラリにわたる。

-- built-in libs --
BaseHTTPServer.py
    class HTTPServer(SocketServer.TCPServer):
        (no __init__ definition)
SocketServer.py
    class TCPServer(BaseServer):
        [...snip...]
        def __init__(self, server_address, RequestHandlerClass):
     (BaseServer.__init__ で self.server_address に代入される)
            [...snip...]
            self.socket = socket.socket(self.address_family,
                                        self.socket_type)
            self.server_bind()
            [...snip...]
        def server_bind(self):
            [...snip...]
            self.socket.bind(self.server_address)
        [...snip...]

ビルトインライブラリに入ってからは、BaseHTTPServer.HTTPServer の親クラスである SocketServer.TCPServer にわたり、そこで socket オブジェクトの bind メソッドにわたる。

-- built-in libs --
socket.py
    [...snip...]
    import _socket
    from _socket import *
    [...snip...]
    _realsocket = socket
    [...snip...]
    _socketmethods = (
        'bind', [...snip...])
    [...snip...]
    class _socketobject(object):
        [...snip...]
        def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, _sock=None):
            if _sock is None:
                _sock = _realsocket(family, type, proto)
            self._sock = _sock
        [...snip...]
        _s = ("def %s(self, *args): return self._sock.%s(*args)\n\n"
              "%s.__doc__ = _realsocket.%s.__doc__\n")
        for _m in _socketmethods:
            exec _s % (_m, _m, _m, _m)
        [...snip...]
    socket = SocketType = _socketobject

_socketobject__init__ の定義中の exec によって bind の定義として以下のコードが実行される。

def bind(self, *args): return self._sock.bind(*args)

つまり、TCPServerserver_bind 内で呼び出されている self.socket.bind(self.server_address) は、_socket モジュールからインポートされた socket (インポート直後に _realsocket にリネームされている) の bind メソッドだと言うことになる。で、ここから先は C で記述されているモジュールになる。

C のソースは簡単にしか見ていないが、Modules/socketmodule.csock_bind がどうやらそれらしい。この中では bind(2) が呼ばれている。最初にわたされたサーバのアドレスは最終的にこのシステムコールにわたるわけだ。

以上をまとめると、デフォルトでは "localhost" が、--address を指定した場合にはその値が bind(2) にわたる、ということだ。

(「UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI」p.89 より)
プロセスは、ソケットに特定の IP アドレスを bind することができる。bind する IP アドレスは、そのホストのインタフェースに属するものでなければならない。[...snip...] TCP サーバでは、この操作により、そのソケットではバインドした IP アドレスに向けたクライアントからのコネクション要求のみを受信するようになる。

要は、localhost (127.0.0.1)と LAN につながっているネットワークインタフェースとは別物だってこと。IP アドレスが違うんだから当然と言えば当然か。

bind(2) はわたされたアドレスをサーバ用のソケットに束縛するため、デフォルトでは localhost (127.0.0.1)だけがサーバ用のアドレスとなるわけだ。Mac の LAN につながっているアドレスの方は無視される。だから、LAN 側からのアクセスができないのだ。

一方、明示的に LAN につながっているアドレスを指定すれば、そちらをサーバ用のアドレスとして束縛するため、きちんと LAN 側からアクセスできる。逆にこのときは Mac からであっても localhost としてアクセスできなくなる。

(「UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI」p.111 より)
ワイルドカードアドレスをバインドすることによって、マルチホームシステムでは、どのローカルインターフェースにあてたコネクションでも受け付けることをシステムに指示することになる。

また、アドレスとしてワイルドカード(0.0.0.0)を指定すれば、LAN 側からも、localhost としてもアクセスできる。

参考文献

UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI
W.リチャード スティーヴンス
ピアソンエデュケーション ( 2000-04 )
ISBN: 9784894712058
おすすめ度:アマゾンおすすめ度

関連リンク

関連記事

twitter より (2010-10-09)

  • 00:16  ノート型だと縦方向が不足していると感じる。とくにPDFのようにページに区切られたコンテンツを見るときには。けどスクロール前提のコンテンツ(ブラウザとか)だと、縦に長くてもやっぱりスクロールしているよ。 → http://bit.ly/dgSCYY
  • 00:17  縦に収まり切らないんだからスクロールしないと隠れている部分が見えないのは当然なんだけど、そうじゃなくて、見えている部分だとしても、スクロールさせて視線の位置まで持ってきているってこと。
  • 00:20  縦方向が 1440 だろうが、1600 だろうが変わらない。横方向には視線を移動させるんだけど、縦方向には不思議と視線を移動させない。その変わりコンテンツを移動させる。画面の上下に視線を移動させるのって、コードを読むときぐらいじゃないかな。
  • 00:23  一方、本(電子書籍をふくむ)のようにページで区切られたコンテンツだと視線の移動を強制される。どっちが読みやすいんだろう?
  • 00:29  Magic TrackPad に慣れると、コンテンツをスクロールさせることが、ほとんど無意識に行えるようになる。視線を移動させるのと同じぐらい無意識に。そのことも、視線の移動の代わりにコンテンツをスクロールさせる読み方に貢献しているのかも。
  • 00:38  これ(27インチシネマ)も、昔の30インチシネマに比べて縦方向は減ってるんですがね(1600→1440)。(スラドの記事の)縦方向の減少といい、表面の仕上げ(グレアとノングレア)といい、ディスプレイの変化の方向がユーザ体験の向上を目指してのものだとは思えないんですよね。
  • 00:54  あ、ふと思ったけど、ヒトの首って左右にくらべて上下は稼動(って言葉で良いのかな?)可能な範囲が、ぐっと狭いんだよ。それでいて動かすことによる負担も大きい。ひょっとしたら目玉も左右に動かすより上下に動かす方が大変だったりする? 縦方向はあまり大きくても使いにくいのかもな。
  • 10:27  ブクマ代わり。あとで読む。「スーパーコンピューティングの将来」 → http://www.artcompsci.org/~makino/articles/future_sc/face.html
Powered by twtr2src.