2010-10-16

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

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

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

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

Python ではクラス変数は、

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

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

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

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

(クラス変数とインスタンス変数へのアクセス)
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.