Hatena::ブログ(Diary)

cooldaemonの備忘録 RSSフィード

2013-01-06

Python の amqplib とか py-amqp で Message を Consume する際, ヘッダに x-death が付与されていると落ちる件

データに謎の 'A' という型が定義されているのが問題. AMQP の Elementary domains を見たのだけれど 'A' が何か見当たらない…。助けて偉い人orz

とりあえず, amqplib 1.0.2 は, 以下の黒魔術で回避可能. kombu 2.5.0 から amqplib に代わり py-amqp がデフォルトで使用されるのだけれど, そちらはおいおい.

from struct import unpack
from decimal import Decimal
from amqplib.client_0_8.serialization import AMQPReader

def _patched_read_table(self):
    """
    Read an AMQP table, and return as a Python dictionary.
    """
    self.bitcount = self.bits = 0
    tlen = unpack('>I', self.input.read(4))[0]
    table_data = AMQPReader(self.input.read(tlen))
    result = {}
    while table_data.input.tell() < tlen:
        name = table_data.read_shortstr()
        ftype = ord(table_data.input.read(1))

        if ftype == 65: # 'A' これが新しく加わった!!
            len = unpack('>I', table_data.input.read(4))[0]
            ftype = ord(table_data.input.read(1))

        if ftype == 83: # 'S'
            val = table_data.read_longstr()
        elif ftype == 73: # 'I'
            val = unpack('>i', table_data.input.read(4))[0]
        elif ftype == 68: # 'D'
            d = table_data.read_octet()
            n = unpack('>i', table_data.input.read(4))[0]
            val = Decimal(n) / Decimal(10 ** d)
        elif ftype == 84: # 'T'
            val = table_data.read_timestamp()
        elif ftype == 70: # 'F'
            val = table_data.read_table() # recurse
        else:
            raise ValueError('Unknown table item type: %s' % repr(ftype))
        result[name] = val
    return result

AMQPReader.read_table = _patched_read_table

2012-12-09

Python Web Framework Advent Calendar 2012 (9日目) Django Model で Named Scope

前置き

この記事は、2012 Pythonアドベントカレンダー(Webフレームワーク) - connpass の 9 日目の記事となります。

今回は、Rails の Named Scope の真似を Django Model で実現する方法と、それを利用した論理削除の紹介を行います。

Django Model で Named Scope を実現する

そもそも何をしたいのか?

まず、ECサイトやソシャゲー等のユーザ情報から、最近登録したユーザの中から直近のアクセス順に上位5人を取得する例を挙げます。

import datetime
from django.utils.timezone import get_default_timezone

# 一週間以内の登録を "最近登録した" とみなす
dt = datetime.datetime.now() - datetime.timedelta(weeks=1)

# utc でも良いけれど、何となくローカライズ
dt = get_default_timezone().localize(dt)

User.objects.filter(created_at__gt=dt).order_by('-logged_in_at')[:5]

Named Scope を使用すると、次のようになります。

User.objects.by_newbie().order_by_active()[:5]
Named Scope を作る

では、実際の実現方法を紹介します。

import datetime
from django.utils.timezone import get_default_timezone

from django.db import models
from django.db.models.query import QuerySet

# Manager と QuerySet で同様のメソッドを使用するので Mix-in Class として切り出す
class UserScopesMixin(object):
    _newbie_term = datetime.timedelta(weeks=1)

    def by_newbie(self):
        dt = datetime.datetime.now() - self._newbie_term
        dt = get_default_timezone().localize(dt)
        return self.filter(created_at__gt=dt)

    def order_by_active(self):
        return self.order_by('-logged_in_at')


# QuerySet に Scope を Mix-in する
# 継承順は賛否分かれる所ですが、この記事では、社内の目があるので Mix-in Class を後ろに羅列します(w;
# 蛇足ですが、私は、私用で Python を書く場合に限り、Mix-in Class を前に羅列する派です
class UserQuerySet(QuerySet, UserScopesMixin):
    pass


# Manager に Scope を Mix-in し, 上記で定義した QuerySet を返すようにする
class UserManager(models.Manager, UserScopesMixin):
    def get_query_set(self):
        return UserQuerySet(self.model)


# 上記で定義した Manager を objects に設定する
class User(models.Model):
    objects = UserManager()

    created_at = models.DateTimeField(auto_now_add=True, index=True)
    logged_in_at = models.DateTimeField(auto_now=True)

    @classmethod
    def get_active_newbies(cls, limit=5):
        return User.objects.by_newbie().order_by_active()[:limit]

結局、get_active_newbies() を定義するのであれば、Named Scope なんて不用ではないか?と思われるかもしれません。

しかし、get_active_newbies() の様なメソッドを多数定義する場合、スッキリ書けるのでオススメです。

また、Named Scope が癖になっていると、そもそも QuerySet をカスタム済みであるため、他のカスタム QuerySet を組み込む際に労力が減るという副作用もあります。*1

論理削除の実例

物理的にレコードを削除せずに、削除フラグを立てて削除した事にするアレ。

class LogicalDeleteScopesMixin(object):
    def by_alive(self):
        return self.filter(deleted_uuid='')

    def delete(self):
        self.update(deleted_uuid=uuid.uuid4(),
                    deleted_at=datetime.datetime.now(pytz.utc))


class LogicalDeleteQuerySet(QuerySet, LogicalDeleteScopesMixin):
    pass


class LogicalDeleteManager(models.Manager, LogicalDeleteScopesMixin):
    def get_query_set(self):
        return LogicalDeleteQuerySet(self.model).by_alive()


# Mix-in Class であるため、object を継承したいが、
# Django Model の制約により models.Model を継承する必要がある。
class LogicalDeleteModelMixin(models.Model):
    class Meta:
        abstract = True


    class RedeletedError(Exception):
        pass


    objects = LogicalDeleteManager()

    # delete_at を有効/無効の確認に利用すると、
    # 一意キー制約を設けた際に、一秒以内の delete が使えないため、
    # 有効/無効を判断するための UUID フィールドを設ける。
    # 初期値に NULL を指定すると、NULL はレコード毎に異なる値と認識されるため、
    # UUID フィールドを一意キー制約に含められない。
    # そこで、初期値には空文字列を明示的に指定しておく。
    deleted_uuid = models.CharField(max_length=255, db_index=True, default='')

    # 念のため、記録として削除日時を残しておく。
    deleted_at = models.DateTimeField(blank=True, null=True)

    def delete(self):
        if self.deleted_uuid:
            raise self.RedeletedError, self.pk

        self.deleted_uuid = uuid.uuid4()
        self.deleted_at = datetime.datetime.now(pytz.utc)
        self.save()

早速、先ほどの User Model で使用してみましょう。

# LogicalDeleteScopesMixin を継承する
class UserScopesMixin(LogicalDeleteScopesMixin):
    pass # 内容に変更がないため省略


# LogicalDeleteScopesMixin を継承して UserScopesMixin を定義したので
# UserQuerySet に変更はない。
class UserQuerySet(QuerySet, UserScopesMixin):
    pass


# by_alive() を使用する必要がある
class UserManager(models.Manager, UserScopesMixin):
    def get_query_set(self):
        return UserQuerySet(self.model).by_alive()


class User(models.Model, LogicalDeleteModelMixin):
    pass # 内容に変更がないため省略

その他

私は、Python 歴 = Django 歴 = 半年未満であり、Django 以外の他の Python Web Framework の知識は皆無という状態ですが、今回紹介させて頂いた Named Scope や、Class Based View の存在から、Django は OO 設計し易いフレームワークだと認識しており、これからも末永くお付き合いできれば嬉しいなぁ〜と考えております。

参考 URL

*1:拙作に MemoizePerRequestQuerySet と MemcacheQuerySet というものがあるのですが、そちらは、Python 系勉強会で紹介予定です

2012-06-03

Z会三年生中学受験コース5月のてんさく問題を Python で解いてみた

妻と娘から次の質問をされた。

4けたの数について、それぞれの位の数字を大きいじゅんにならべた数から小さいじゅんにならべた数をひくという計算を行います。

1974 について、この計算を 100 回行った答えを書きなさい。

転職先の会社で Python を使うことになっているが、今まで座学ばかりで一行もコードを書いていなかったので、試しに妻と娘の前でライブコーディングをしてみた。

n = '1974'
for c in range(100):
  n = str(int("".join([x for x in reversed(sorted(n))])) - int("".join(sorted(n))))
  print "%s: %s" % (c, n)

reversed が破壊的だった所に少しハマった。

出力結果は下記の通り。

$ python test.py
0: 8262
1: 6354
2: 3087
3: 8352
4: 6174
5: 6174
// 以下略

って事で答えは 6174 でしたと。