DjangoテンプレートとGenshiを比較する

これはgumi Engineer's Diaryに書いたものを修正・転載したものです。


はじめに
世の中には、Advent Calendarなるものが存在しているみたいです。
僕は不勉強なのもあって今年になって初めて知りました。
そのうちの一つPython Web フレームワーク アドベントカレンダー にいかがですか?と @terapyonさんに声をかけて頂き、@RyoAbeさんからバトンが回って来たので、何かそれっぽいことを書いてみようかと思います。
とは言え、僕自身はDjango以外のPython Web フレームワークを使った経験があまりないので、今回は前職でちょろっと使っていたGenshiとgumiで使っているDjangoテンプレートの比較をしてみようかなーと考えています。



Djangoテンプレート
そんなわけでDjangoテンプレートです。
Djangoには専用のテンプレートシステムがあります。
詳しくはこの辺を見てください。
Djangoテンプレートだと
変数は

<h1>{{ section.title }}</h1>

条件分岐は

{% if foo %}
  {{ bar }}
{% endif %}

{% for topic in topics %}
<ul>
 <li>{{ topic }}</li>
</ul>
{% endfor %}

みたいに書けます。

書きやすくて便利です!

実際の例はこんな感じ。

{% extends "base_generic.html" %}

{% block title %}{{ section.title }}{% endblock %}

{% block content %}
<h1>Hello, ${w}</h1>

{% if b == True &}
<hr color="#00FF00" />
{% else %}
<hr color="#FF0000" />
{% endif %}

<ul>
{% for link_tag in l %}
 <li>{{link_tag}}</li>
{% endfor %}
</ul>
{% endblock %}

書きやすいのですが、制御文がテキストベースなので、構造が入り組んでいるとごちゃっとしていてわかりにくいですし、ブラウザでテンプレートを見てみてもよくわかりません><
gumiではデザイナさんにhtmlのコーディングを頼んでいるのですが、そのためにはデザイナさんにもDjangoにある程度習熟してもらう必要もあって、結構ハードルが高いです。



Genshi
そんな訳でGenshiです。
詳しくはこっちもUser Guideとかを見てもらうとして、ざっと説明すると
変数は

<h1>${ section.title }</h1>

条件分岐は

<b py:if="foo">${bar}</b>

<ul py:for="topic in topics">
 <li>${topic}</li>
</ul>

みたいに書けます。

制御文がテキストベースではなくタグベースですが、Djangoとあまり変わらないですね!

実際の例はこんな感じ。

<html xmlns="http://www.w3.org/1999/xhtml"   
      xmlns:py="http://genshi.edgewall.org/" py:strip=""  
      xmlns:xi="http://www.w3.org/2001/XInclude"  
      >
<head>
</head>
<body>
<h1>Hello, ${w}</h1>

<py:choose test="b()">
<hr py:when="True"  color="#00FF00" />
<hr py:otherwise="" color="#FF0000" />
</py:choose> 

<xi:include href="testapp/include.html" />

<ul py:for="link_tag in l"> 
  <li>${link_tag}</li> 
</ul> 
</body>  
</html>  

タグに制御文を埋め込むのが最初はキモく見えるかもしれませんが、すぐに慣れると思います。
タグに埋め込んである分シンプルでわかりやすいですし、何よりブラウザで見ても普通のhtmlなので、デザイナさんがそのまま作業が出来るのが良いですね!

ただテンプレートが厳密なxmlでなくてはダメです。
前職で使用していた際によくありましたが、<br /> でなく <br> と書いてしまうとエラーが出て怒られるので気をつけてください。



まとめ
Genshiを思い出しながらDjango上で動かしてみましたが、正直一人で手軽に作るのならDjangoテンプレートの方が楽ですね。
Djangoテンプレートに慣れているのもあって、厳密なxmlかどうかとか考えながらhtmlを書くのは面倒でした。
ですが、デザイナーさんやhtmlコーダーさんにテンプレートを修正してもらう場合は、Genshiテンプレートの方が確認等がしやすくて良いのではないかと思います。
専任のデザイナーさんやhtmlコーダーさんがいるような環境ではGenshiを使ってみるのはどうでしょうか?
その際の環境構築には「djangoで、テンプレートエンジンgenshiを使う」を参考にしてみてください!


ネタもなかったので、Genshiの簡単な紹介みたいになってしまいました。。。
日頃からアンテナを張り巡らせたり色々試したりしていないと、こういう時にあたふたとしてしまって良くないですね><
ハードルは下げておいたので、次は@ransuiさんお願いします!

モバイル向けCSSのインライン化ライブラリを公開しました

これはgumi Engineer's Diaryに書いたものを修正・転載したものです。


はじめに
今日のお題は悪名高きガラケーCSSのお話です。
キャリアごとに色々あって面倒くさいガラケーCSSも、これを読めばPCサイトのように簡単に扱えるようになります。


モバイルでもCSSを使おう!
最近のウェブサイトでは装飾と構造を分離するためにCSSを使用するのが一般的です。
しかし、docomoの古い携帯ではPCサイトと同じようにはCSSが使用出来ません。
そのため本来はCSSで表現すべきものをインラインのstyle要素としていたりすることが多いです。
ですが、perlPHPにはこういう状況を解決するために、styleタグで指定した外部CSSファイルを読み込んで、インラインのstyle要素に入れ込んでくれるライブラリが存在しています。
実に羨ましい限りですね!
なんでpythonにはないんでしょう><
とは言え、ないことを嘆いていても仕方ないのでHtmlCssIncludeというライブラリを自分で作ってみました。


HtmlCssIncludeの使用にあたって
http://pypi.python.org/pypi/HtmlCssInclude/
そんなわけでHtmlCssIncludeです。
ネーミングセンスが悪いのは勘弁してください。
htmlのパースにlxmlを使用しているので、lxmlのインストールが必要です。
また、python2.5以上でしか、動作確認をしていません。
ライセンスはPSFなので好きに使ってください。


使い方
使い方は、インポートして、初期化して、変換するだけです。
これだけで普通のウェブサイトがモバイル向けになります。

from  HtmlCssInclude import CssInclude

# 初期化
css_include = CssInclude(agent='docomo', is_vga=True)

input_html = file('test.html').read()

# 変換
converted_html = css_include.apply(input_html)

用意するhtmlは普通のウェブサイト向けのhtmlでOKです。
style要素のCSSでもlink要素のCSSでもインラインのstyleに変換します。
簡単ですね!


パラメータとおすすめフォントサイズ
初期化時のパラメータは以下になります。

  • agent

  docomoezwebsoftbankのどれかを指定すると、文字サイズを適切に変更します。

  • base_dir

  link要素CSSのベースとなるディレクトリ。

  True/False。Trueにすると画像サイズを自動的に2倍にしたりします。

フォントサイズは「10px」「16px」の2種類を使うのがおすすめです。
と言うのも、これらの場合に携帯に合わせて良いようにフォントサイズを変更するように作ってあるのです。
なので、フォントサイズが2通りで良い場合は「10px」「16px」を使うと、キャリアごとの見た目を考える必要がないので楽だと思います。


まとめ
HtmlCssIncludeを使えばpythonでも3キャリア対応のモバイルサイトが簡単に作成出来ます。
使ってみて、こんな機能もあったら便利!とか、ここが駄目!とかあったら教えてくれると嬉しいです。

Djangoでキャッシュ機能付きモデル

これはgumi Engineer's Diaryに書いたものを修正・転載したものです。


キャッシュを自動化しよう
ソーシャルアプリのようなトラフィックが高いサービスを作るときはDBアクセスを減らすことが重要になります。
Djangoにはそのための機能であるdjango.core.cacheが存在していますので、DBアクセスの結果は積極的にキャッシュしたいところです。
ですが、各モデルにいちいちキャッシュの機構を組み込むのは面倒ですし、万が一消し忘れたりすると大変です。
そこで、作成したゲームではキャッシュする抽象モデルクラス(AbstructCachedModel)を作って、ある程度のキャッシュを自動化しています。


AbstructCachedModelの仕組み
キャッシュ機能付き抽象モデルクラスであるAbstructCachedModelに実装してあるメソッドは6つです。

キャッシュのキーを取ってくる

  • get_cache_path()
  • get_cache_all_path()

実際にデータを取得する

  • get()
  • get_all()

忘れずにキャッシュを削除するためのオーバーライド

  • save()
  • delete()

これだけあれば大抵は何とかなります。
条件付きのデータ取得がしたい場合などはこれを継承した個々のクラスで追加しています。


AbstructCachedModelの実装

class AbustractCachedModel(models.Model):
    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
        super(AbstractCachedModel, self).save(*args, **kwargs)
        cache.delete(self.get_cache_path(self.id))
        cache.delete(self.get_all_cache_path())

    def delete(self, *args, **kwargs):
        cache.delete(self.get_cache_path(self.id))
        cache.delete(self.get_all_cache_path())
        super(AbstractCachedModel, self).delete(*args, **kwargs)

    @classmethod
    def get_cache_path(cls, pk):
        return '%s/%s/' % (cls._meta, pk)

    @classmethod
    def get_all_cache_path(cls):
        return '%s/all/' % (cls._meta)

    @classmethod
    def get(cls, pk):
        cache_path = cls.get_cache_path(pk)
        model = cache.get(cache_path, None)
        if model is None:
            try:
                model = cls.objects.get(pk=pk)
                cache.set(cache_path, model, 86400)
            except cls.DoesNotExist:
                pass
        return model

    @classmethod
    def get_all(cls):
        cache_path = cls.get_all_cache_path()
        models = cache.get(cache_path, None)
        if models is None:
            models = list(cls.objects.all())
            cache.set(cache_path, models, 86400)
        return models

使い方は model.objects.get(pk=pk) の代わりに model.get(pk=pk)、model.objects.all() の代わりに model.get_all() を使うだけです。
もし、わからなかったらコメントしてください!


まとめ
AbstractCachedModelを継承すればキャッシュ機能付きのモデルが簡単に作れます。
他に必要な機能があったら適宜拡張して使ってください。
こんな機能もあったら便利!とかあれば、教えてくれると嬉しいです!

15行で書くソーシャルゲームのリアルタイム・ランキング

これはgumi Engineer's Diaryに書いたものを修正・転載したものです。


はじめに
ソーシャルアプリでは、DBへの負荷を減らすためにKVSを使ったりすることが多いです。
そこで今回は実際にKVSを使用している例として、リアルタイム・ランキングを取り上げたいと思います。
一見難しそうに思えますが、リアルタイムでのランキング処理なんてメインのロジックは15行もあれば書けちゃうんですよ!


リアルタイム・ランキング
ゲームを扱うコンテンツ・サービスにおいてランキングは重要な要素となりますが、データの件数が多くなると単純な方法では負荷が高くなってしまうので難しいです。
その辺の難しさや解決策は GREE Engineer's Blog の「リアルタイム・ランキングを考える 」にまとまっているので読んでみて頂けると理解がしやすいんじゃないかと思います。
ただ、GREEさんの記事は素晴らしいのですが、実際どうやっているのか等の細かいところまでは書かれていません。
なので、この記事では実際の実装も含めて出し惜しみをせずに紹介したいと思います。


gumi の取り組み
GREEさんのブログではMySQLを例として説明されていましたが、gumiでは頻繁に更新があるランキングのデータはKVSのTokyoTyrantに格納しています。
例えば勝ち数ランキングですと、キー:バリューが、勝ち数を:順位 として入っています。
具体的にはこんな感じです。

'Ranking::Win::0' : 100
'Ranking::Win::1' : 50
'Ranking::Win::2' : 30
'Ranking::Win::3' : 10
'Ranking::Win::4' : 1

勝ち数が2の人の順位は 'Ranking::Win::2' で引いて返ってくる 30位となるわけです。
こうやってデータが格納されている状態なら、順位を求めるのは簡単ですし、負荷もかかりません。
素晴らしいですね!


でも、どうやってこのような状態を作っているのでしょうか?
先月リリースしたハッピーモデルではこんな15行程度のコードで行っています。

    def add_point(point, add=1):
        """ 
        値が増えた際の順位の更新
         point : 変化前の値
         add : 増加量
         """
        # 変化前の点数より上にプレイヤーが一人増えるため  
        # 変化前の点数に対応する順位を+1する。
        now_key = "Ranking::Win::%d" % point
        tokyotyrant.addint(now_key)

	# 1以上増えた場合は間の順位も+1する。
        for i in range(0, add):
            next_key = "Ranking::Win::%d" % (point+add-i)
            next = tokyotyrant.getint(next_key)
            # 今までになかった場合は1位を入れる。
            if not next:
                if i == 0:
                    tokyotyrant.putint(next_key, 1)
                else:
                    tokyotyrant.putint(next_key, 2)
            elif i != 0:
                tokyotyrant.addint(next_key)

コードだけ見てもよくわからないですね!
ごめんなさい。説明します。


ランキングの仕組み
初期状態はこんな感じです。

'Ranking::Win::0' : 1
人数:いっぱい

リリース時はまだ誰も勝っている人がいないので、全員1位です。


次に誰かが勝つとこうなります。

'Ranking::Win::0' : 1 → 2
人数:いっぱい
'Ranking::Win::1' : 1 # New Key
人数:1人

1勝した人だけが1位で残りが2位となります。


では、他の0勝の人が勝つとどうなるのでしょうか?

'Ranking::Win::0' : 2 → 3
人数:いっぱい
'Ranking::Win::1' : 1
人数:2人

同率1位の人が2人いるので、残りの人は3位です。


1勝している人が更に勝った場合は

'Ranking::Win::0' : 3
人数:いっぱい
'Ranking::Win::1' : 1 → 2
人数:1人
'Ranking::Win::2' : 1 # New Key
人数:1人


基本的にはコメントにもあるように
「変化前の勝ち数より上にプレイヤーが一人増えるため、変化前の勝ち数に対応する順位を+1する。」
これだけで順位テーブルが適切に更新されます。
簡単ですね!


まとめ
gumiでは、こんな感じでランキングを実装しています。

今の実装では下がることがある値(勝ち数ではなく勝率など)には対応していないのですが、それにも今後対応する予定です。

今回紹介した方法は (その間の全ての順位を更新する必要があるため) 1勝の人が100勝になるなどの急激な値の変化には向いてないのですが、人数に関わらず負荷が一定でトランザクションの必要がないという素晴らしい方法です。

リアルタイム・ランキングの実装で悩んでいる方がいましたら、是非是非使ってください!

世界はどんどん広くなっている

http://anond.hatelabo.jp/20100425122119
要約すると、昔はインターネットは世界への窓だった。
ところが今は日本だけ身内だけのものに閉じてしまっている。
だから、インターネットという場所がどんどん狭くなっているのではないかって話が展開されている。


でも、実際はそんな事はなくて、昔は狭かった為に日本だけでなく世界にも目を向けられた。
現在はあまりに広くなり身近の事で足りるようになってしまった為、世界には目を向ける必要がなくなった。
というのが、正しいのだと思う。
僕も昔は情報が無かったので、英語のページだって読んだし、コンタクトを取ったりもした。
現在だって日本語の情報が無ければ英語のページを読む事もよくある。
でも、昔よりは日本語の情報が増えたので、英語のページを読む機会が無くなった。
つまりは、そういうことなのだろう。


全体が広くなると、相対的には自分の手が届く割合は狭くなる。
それでも、遠くに手を届かせようと思っている人間はいるだろうし、昔に比べてだって絶対数は増えている筈だ。
ただ、比率が下がっている為に目立たないだけなのではないだろうか。
世界が広くなるという事は、きっとそういうことなのだ。

ドミニオンのシミュレーション改善

id:nishiohirokazu 氏がpythonドミニオンのシュミレーションという琴線に触れる事をやっていた。
ドミニオンのシミュレーション - 西尾泰和のはてなダイアリー
これは、

ドミニオンのコインを買う戦略をする際に、初手で礼拝堂を買うべきかどうかについて。
初手終了後10ターンの間「礼拝堂が出たら屋敷を捨てる。買えるなら銀貨や金貨を買う。」という戦略をとった場合に、デッキ1枚あたりのコイン価値がどうなるかを1万回シミュレーションした。結果、「礼拝堂+銀貨」は「銀貨+銀貨」、「礼拝堂+礼拝堂」、「銀貨+堀」のいずれよりも優れていることがわかった。

という、ある程度ドミニオンをやっている人であれば当然体感的に理解しているであろう事の裏付けを取ったものであるが

なお、この実験では礼拝堂を屋敷を捨てるためだけに使っているが実戦では銅貨を捨てることに使ってもよいので「礼拝堂+銀貨」と「礼拝堂+礼拝堂」はより高い値になりうる。

という記述にあるように、銅貨を捨てることがシミュレーションに盛り込まれていなかったので、実際によく行うように、銀貨/金貨を買うのに余った銅貨を捨てる remove_copper を追加してみた。

結果、初手で礼拝堂と銀貨を買った場合、平均1.87、SD 0.14。礼拝堂2枚を買った場合、平均1.83, SD 0.15とより高い結果が得られた。

from random import shuffle
from copy import copy

NUM_TRIAL = 10000

def draw(deck, used, n):
    if len(deck) < n:
        shuffle(used)
        deck.extend(used)
        used = []
    hand = deck[:n]
    deck = deck[n:]
    return deck, used, hand

def count_money(cards):
    return cards.count("1") + cards.count("2") * 2 + cards.count("3") * 3

def remove_copper(cards):
    coppers = cards.count("1")
    others  = cards.count("2") * 2 + cards.count("3") * 3

    if others >= 6:
        while "1" in cards:
            cards.remove("1")
    elif others + coppers >= 6:
        removable = others + coppers - 6
        i = 0
        while "1" in cards:
            if removable <= i:
                break
            cards.remove("1")
            i += 1
    elif others + coppers >= 3:
        removable = others + coppers - 3
        i = 0
        while "1" in cards:
            if removable <= i:
                break
            cards.remove("1")
            i += 1

def test(initial_deck):
    sum = 0
    sumsq = 0.0

    for triel in range(NUM_TRIAL):
        deck = copy(initial_deck)
       shuffle(deck)
        used = []
        for i in range(10):
            deck, used, hand = draw(deck, used, 5)
            if "C" in hand:
                while "E" in hand:
                    hand.remove("E")
                remove_copper(hand)
            elif "M" in hand:
                deck, used, got = draw(deck, used, 2)
                hand += got

            money = count_money(hand)
            if money >= 6:
                used.append("3")
            elif money >= 3:
                used.append("2")

            used += hand

        deck += used
        money = count_money(deck)
        x = float(money) / len(deck)
        sum += x
        sumsq += x * x

    from math import sqrt
    print sum / NUM_TRIAL, sqrt(sumsq / NUM_TRIAL - (sum / NUM_TRIAL) ** 2)


print "a silver only"
test(list("1111111EEE2"))
print "silver and chapel"
test(list("1111111EEE2C"))
print "two silvers"
test(list("1111111EEE22"))
print "two chapels"
test(list("1111111EEECC"))
print "silver and moat"
test(list("1111111EEE2M"))