昨日見ておもしろかったので(auto-vivification)

直接元のgistを見ないで再現してみた。再現できていたので理解は合ってた。良かった。

code

## https://en.wikipedia.org/wiki/Autovivification

from collections import defaultdict

def tree():
    return defaultdict(tree)

def tree_as_dict(t):
    if hasattr(t, "__iter__"):
        return {k:tree_as_dict(t[k]) for k in t} 
    else:
        return t

t = tree()
t["x"]["y"]["z"] = 1
t["x"]["y"]["a"] = 2
t["x"]["a"]["b"] = 2
t["x"]["b"]["a"] = 2

print tree_as_dict(t)
# {'x': {'y': {'a': 2, 'z': 1}, 'a': {'b': 2}, 'b': {'a': 2}}}

see also

one-line tree in python ― Gist https://gist.github.com/2012250

dictをparseするコード書いた。

install

$ easy_install tinyschema

demo

from tinyschema.schema import Schema
from tinyschema import fields

class Person(object):
    def __init__(self, name=None, age=None):
        self.name = name
        self.age = age

class PersonSchema(Schema):
    name = fields.string()
    age = fields.integer()

class SuperPersonSchema(PersonSchema):
    skill = fields.noduplicated()

class ParentsSchema(Schema):
    mother = PersonSchema(after_parse= lambda kwargs : Person(**kwargs))
    father = PersonSchema(after_parse= lambda kwargs : Person(**kwargs))

print PersonSchema.parse({"name": "foo",  "age": "10"})
print SuperPersonSchema.parse({"name": "foo",  "age": "10", "skill": ["foo", "bar", "baz", "foo", "bar"]})
params = {"mother": {"name": "foo", "age": "30"}, 
          "father": {"name": "bar", "age": "30"}, 
          }
print ParentsSchema.parse(params)

# {'age': 10, 'name': 'foo'}
# {'skill': set(['baz', 'foo', 'bar']), 'age': 10, 'name': 'foo'}
# {'father': <__main__.Person object at 0x7fd3e0693ad0>, 'mother': <__main__.Person object at 0x7fd3e0693b10>}

追記

## object schema
from tinyschema.schema import ObjectSchema
class PersonSchema(ObjectSchema):
    name = fields.string()
    age = fields.integer()

print PersonSchema.parse({"name": "foo",  "age": "10"})
# <class '__main__.PersonSchema'>: ['age', 'name']
print PersonSchema.parse({"name": "foo",  "age": "10"}).name
# foo
print PersonSchema.parse({"name": "foo",  "age": "10"}).age
# 10

from tinyschema import schema
ParentsSchema = schema.schema_factory("ParentsSchema", ObjectSchema,
                                     mother=PersonSchema(), father=PersonSchema())
print ParentsSchema.parse(params)
# <class 'tinyschema.schema.ParentsSchema'>: ['father', 'mother']

ubuntuでsolr動かしてみる。

solr

全文検索エンジンのサービスを提供するサーバ(動作させるにはjetty,tomcatなどhttpserverが必要)

jettyで動かしてみることにした。

agenda

  • install
  • jettyの設定変更
  • schema.xmlを変更
  • pythonからsolrの機能を利用

install

# $ sudo apt-get install openjdk-6-jdk
$ sudo apt-get install solr-common solr-jetty

jettyの設定変更

jettyを/etc/init.dから起動できるようにする。ついでにadmin画面にアクセスしてsolrがjetty上で動いているか確認

$ sudoedit /etc/default/jetty
# NO_START=0に変える

## jettyを立ち上げる
$ sudo /etc/init.d/jetty start

## browserで見てみる(defaultは以下のurl)
$ x-www-browser http://localhost:8080/solr

schema.xmlを変更

schema.xmlについて

solrでどのようなフィールドを格納するかの設定を記述するファイル

変更手順

ubuntuのaptでインストールした場合、schema.xmlは/etc/solr/conf/schema.xmlに置かれる。
(これは,/var/lib/dpkg/info/solr*.listの中を見ればわかる。)
schema.xmlは以下のように変更した.

<?xml version="1.0" encoding="UTF-8" ?>
<schema name="example" version="1.4">
  <types>
    <!-- The StrField type is not analyzed, but indexed/stored verbatim. -->
    <fieldType name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>
    <fieldType name="long" class="solr.TrieLongField" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
    <fieldType name="text_ja" class="solr.TextField" positionIncrementGap="100">
      <analyzer>
        <tokenizer class="solr.CJKTokenizerFactory"/>
      </analyzer>
    </fieldType>
  </types>
  <fields>
    <field name="id" type="string" indexed="true" stored="true" required="true" /> 
    <field name="title" type="text_ja" indexed="true" stored="true" />
    <field name="body" type="text_ja" indexed="true" stored="true" />
    
  </fields>
  <uniqueKey>id</uniqueKey>
  <defaultSearchField>body</defaultSearchField>
  <solrQueryParser defaultOperator="AND"/>
</schema>

上記の設定は以下のようなフィールドを登録可能にしている。

field name type -
id int id
title text_ja 日本語の文章タイトル(text_jaはn-gram)
body text_ja 日本語の文章タイトル(text_jaはn-gram)
$ sudo /etc/init.d/jetty restart
# $ sudo less /var/log/jetty/*.stderrout.logなどを見てエラーが起きていないことを確認する

pythonからsolrの機能を利用。

solrpyを使った。

## install
$ easy_install solrpy

## try it
$ python
>>> import solr
>>> s = solr.SolrConnection("http://localhost:8080/solr")
>>> s.add(id=1, title=u"ja", body=u"日本語通る?")
'<?xml version="1.0" encoding="UTF-8"?>\n<response>\n<lst name="responseHeader"><int name="status">0</int><int name="QTime">5</int></lst>\n</response>\n'
>>> s.commit()
'<?xml version="1.0" encoding="UTF-8"?>\n<response>\n<lst name="responseHeader"><int name="status">0</int><int name="QTime">41</int></lst>\n</response>\n'
>>> s.query(u"body:日本語").results
[{u'body': u'\u65e5\u672c\u8a9e\u901a\u308b\uff1f', u'score': 1.8472979, u'id': u'1', u'title': u'ja'}]
>>> s.query(u"title:fo").results
[]
>>> s.query(u"title:ja").results

distributeのextra_commandの作り方

pythonのsetup.pyを使って以下のように実行可能なコマンドを作ってみる

$ python setup.py <command name> <args> ...

(作業した内容のgist)

setup.py

pythonのパッケージはsetup.pyを持っている。

このsetup.pyは提供されるパッケージに関するコマンドを持っている。

例えば、以下のような機能がある。

  • packageをpypiにuploadしたり
  • テストランナーを走らせたり
  • 配布用のバイナリを作ったり
extra_command

pythonのsetup.pyが提供するコマンドを自分で作成することができる。
利用できるコマンドは以下のようにすると分かる。

# $ ls | grep setup.py
# setup.py

$ python setup.py --help-command
# Standard commands:
# ...
# 
# Extra commands:
#   rotate            delete older distributions, keeping N newest files
#   develop           install package in 'development mode'
#   setopt            set an option in setup.cfg or another config file
#   saveopts          save supplied options to setup.cfg or other config file
#   egg_info          create a distribution's .egg-info directory
#   nosetests         Run unit tests using nosetests
#   alias             define a shortcut to invoke one or more commands
#   easy_install      Find/get/install Python packages
#   bdist_egg         create an "egg" distribution
#   install_egg_info  Install an .egg-info directory for the package
#   test              run unit tests after in-place build
# 
# usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
# ...

rotateとかdevelopとか定義されている。

自分でコマンドを追加してみよう

行う手順は以下のとおり

  • entry_pointに追加する
  • 呼ばれるコマンドを作成
entry_pointに追加する。

setup.pynのetnry_pointに追加する。
今回のディレクトリ構成は以下のとおり。foo/scripts.pyに追加する予定

.
├── foo
│   ├── __init__.py
│   └── scripts.py ## ここにfoo_commandを作成
│
└── setup.py

setup.pyにentry_pointsを追加.(scripts.pyのfoo_command)

setup(
    # ...
    entry_points = {
        "distutils.commands": [
            "foo = foo.scripts:foo_command",
        ],
    },
)
commandを作成する

(試しに関数を書いてみる)

# scripts.py
def foo_command(*args, **kwargs):
    pass

このパッケージをインストールしたときfooコマンドが使えるようになれば良い

$ python setup.py develop
$ python setup.py --help-commands | grep foo
#  foo               (no description available)

## あった。実行してみる
$ python setup.py foo
# ..
# TypeError: issubclass() arg 1 must be a class

実行できたけど、エラーで落ちた。次は真面目に実装してみる

真面目にコマンドを実装

調べてみると、distributeのextra_commandsは以下の要件を満たしている必要があるらしい。

  • setuptools.Commandのsubclass
  • run,initialize_options,finalize_optionsを実装
  • その他細かな事
    • user_optionsでオプショナル引数の指定
      • (末尾が=のものは引数をとる)
      • command_consumes_arguments=Trueにすると引数を受け取れるようになる。
from setuptools import Command

class foo_command(Command):
    description = "echo foo"
    
    ## user_option = [(long, short, help-message),  ....]
    # e.g. 
    #  long  :: "verbsoe"
    #  short :: "v"
    #  help  :: "verbose output"
    #
    user_options = [("user=", "u", "echo user")]
    command_consumes_arguments = True

    def run(self):
        print self.args
        print "user: %s foo" % self.user

    def initialize_options(self):
        self.args = []
        self.user = None

    def finalize_options(self):
        pass

実行可能になった。

$ python setup.py foo -u xxx 1 2 3 4
# running foo
# ['1', '2', '3', '4']
# user: xxx foo
 
$ python setup.py foo 1 2 3 4
# running foo
# ['1', '2', '3', '4']
# user: None foo

appendix コマンドにaliasを付ける。(setup.cfgでデフォルトの設定を付加する)

ついでに、setup.cfgを書いてdefaultの設定を付加してみることにする。
ここでは、上で作ったfooコマンドの-uの値を"User"に設定してみる。

setup.pyで結びつけたコマンド名をセクションにして設定を記述する。

#setup.cfg
[foo]
user = "User"

こうすると、python setup.py fooを実行したとき、デフォルトで-u "user"が渡された状態になる。

$ python setup.py foo 1
running foo
['1']
user: "User" foo

MixinについてAdapterについて

pyramidでmixinを使ってコードを書いていた。これは適切なコードの書き方では無かったそう。
もう少し具体的に言うと、component architectureの上では、mixinを使わない方が望ましいらしい。
代わりにadapter*1という機能を使うそう。
adapterのことをちょっとだけ調べてみたらおもしろかったので日記にまとめてみる。

実は。。

adapterの機能を詳しく知らなかった。動きは大まかにはあくしていたのだけれど、何に使えるのか分かっていなかったような感じ。

ここでは、adapterという言葉をZCA(zope component architecture)の中でのadapterに意味を限って使うことにする。
調べる前のadapterの認識は以下の程度の認識だった。
「何か、渡したオブジェクトをメンバーとして持つオブジェクト返してくれる機能」。。

良く見かける例はだいたいこんな感じで終わってる。

  • Groupに所属するMemberがいるような感じ
  • それぞれIGroup,IMemberを実装している。
  • GroupはMemberをオブジェクトのメンバーとして持つ。

adapterは、オブジェクトを渡してあげると、それをオブジェクトのメンバーとしてもつオブジェクトを返してくれるもの。という感じの認識だった。

from zope.interface import implements
from zope.interface import Interface
from zope.interface import Attribute

class IGroup(Interface):
    pass

class Group(object):
    implements(IGroup)

    def __init__(self, member):
        self.member = member

class IMember(Interface):
    name = Attribute("member name")

class Member(object):
    implements(IMember)
    def __init__(self, name):
        self.name = name

foo = Member("foo")

##
from pyramid.registry import global_registry as registry
registry.registerAdapter(Group, (IMember, ), IGroup,)
print registry.queryAdapter(foo, IGroup)
# <__main__.Group object at 0x1240490>

Memberをメンバに持つGroupをAdapterを通して生成できた。
GroupはIGroupの実装でIMemberが必要。

動きはおえるのだけど、使い道が分からない。。

mixinについて

一方で、mixinはわかりやすい。複数の機能を持ったオブジェクトを作成するときに、機能ごとにブロックで区切って実装すること。
ただし、コンストラクタを作成しない。
だいたいこんな感じ。

class MeasureSkillMixin(object):
    def has_empty_area(self, target):
        return True

class SaveSkillMixin(object):
    def save(self, target):
        return "ok"

class DownLoader(MeasureSkillMixin, 
                 SaveSkillMixin):
    def download(self, target):
        if self.has_empty_area(target):
            return self.save(target)
        
dlr =  DownLoader()
print dlr.download("anything") # => ok

余裕があるか調べる機能と保存する機能を分けて定義。Downloaderでmixinしてる。

mixinが解決してくれること

mixinに分けることで、細かく機能を追加できるようになった。
そして各mixinが提供する機能が直交してるなら、それぞれを個別につなぎあわせて便利なオブジェクトを作ることができる。

また、ポリシー毎にmixinを用意して、そのポリシーを利用するオブジェクトには、mixinで機能を付加させることで、
綺麗な形で複数のオブジェクト間の関係を保つことができる。

mixinが解決してくれないもの
  • 異なるポリシーを持つオブジェクト間の機能の共有。

どういうことか少しだけ説明。

今自分たちが作っているプロジェクトでは、容量に余裕が計測が必要クラスについて、
実際に作業(saveなど)を行う前に、has_empty_areaを実行して、作業可能かどうか判断する
というのポリシーとして持っているとする。

そのために

  • MeasureableみたいなInterfaceを用意している
  • MeasureSkillMixinみたいなその機能を提供してくれるMixinを用意している。
  • 提供されているMixinを利用して各開発者は機能を実装していく。

ここで、このポリシーが守られている場合は、一定の秩序を保つことができる。

ここで、別のプロジェクトで使われていた巨大なライブラリ/システムを組込むことになった。
別のプロジェクトでは、上述したポリシーに基づいて設計されていることは期待できない。

例えば、これら複数のポリシーを持ついろいろなオブジェクトに対して、作業の実施コストを尋ねるような処理を書きたいとする。
こういう状況下におかれてるとする、さてどうしよう?

mixinの場合は、結局、実装をどちらかに寄せるということになると思う。
現在のプロジェクトに参加する全てのオブジェクトは、統一的なインターフェイスになるように新たにラッパーがたくさん作られることになると思う。

各オブジェクトに統一的な振る舞いを要求する感じなので、トップダウンの父権的な感じ。

adapterの方法

ZCAでのadapterの方法はこれとは少し違う。
こちらでは、各オブジェクトに統一的なインターフェイスを課すのではなく、興味を持った側面(価値基準)で切り分ける。
そして切り分けた時の関係を登録しておくという感じにする。

切り分けた側面から、それを手にするAPIがあるかのようにmappingを行い欲しいものを取り出すというような感じ。

具体例

もう少し具体的に書いてみる。
例えば、大きさ(size)を計算する必要があるものについて、2つのポリシーを持つもの同士を混ぜ合わせて利用したいとき。

  • 一方は大きさをsizeとして扱っている
  • 一方は大きさをlengthとして扱っている。
Mixinの例

例えば、全てをsizeとして扱うようにして、obj.lengthでアクセスするオブジェクトをラップするようなクラスを作って使う。
でも、どこかで明示的にラッパークラスで包んだりしないといけない。

#Aproject

from zope.interface import Interface
from zope.interface import Attribute

class IAProjectObject(Interface):
    size = Attribute("size of object")

class IBProjectObject(Interface):
    length = Attribute("length of object")

class HasSizeDecorator(object):
    """ a object that obj.length object treats as obj.size object.
    """
	implements(IAProjectObject)
    def __init__(self, has_length):
        self.obj = has_length
        
    @property
    def size(self):
        return self.obj.length

    @property
    def __getattr__(self, k):
        return getattr(self, k)

## とかUtilityをつくる.(条件が複雑になると死ねる?かも?)
def size(o):
    return o.size if hasattr(o, "size") else o.length

結局、どちらがわに実装を寄せるとか、実際大きさを調べたい時適宜使い分けるなどする。

Adapterを使った場合はどうするか。

サイズが欲しいので、sizeという概念で区切る。ここではISizeということにする。
そういうインターフェイスを実装するクラスを作ってあげる。

作ったクラスを登録してあげる。元のクラスにはまったく手を加えない。

実際に利用する際には、オブジェクトがAprojectのものなのか。Bprojectのものなのか考える必要はなくなる。
利用する際に考えるのは「サイズが欲しい」だけなので。

from zope.interface.registry import Components
component = Components("my")

class ASize(object):
    implements(ISize)
    def __init__(self, a):
        self.size = a.size
        self.obj = a

class BSize(object):
    implements(ISize)
    def __init__(self, b):
        self.size = b.length
        self.obj = b

component.registerAdapter(BSize, (IBProjectObject, ), ISize)
component.registerAdapter(ASize, (IAProjectObject, ), ISize)

print component.queryAdapter(B(10), ISize).size #10
print component.queryAdapter(A(10), ISize).size #10

おもしろい。
機能を合成したり一ヶ所にまとめる瞬間が存在しなくて、緩やかな関係が裏側に存在するような感じ。

*1:ZCAの中の話。adapterパターンとかと関連があるかは良く分かんない。ごめんなさい。

virtualenv上のpythonをquickrunで呼ぶ

quickrun

現在のバッファの内容をファイル名に結びついた言語のコマンドで実行してくれる(e.g. foo.pyならpython)
https://github.com/syohex/emacs-quickrun

motivate

pythonを書くときには、virtualenvでプロジェクトごとに環境を分けている。
なので以下のようなことがquickrunでもできたら良いなと思った。

  • ~/venvs/poject1/foo/bar.pyでは~/venvs/project1/bin/pythonを利用して欲しい。
  • ~/venvs/poject2/boo/boo.pyでは~/venvs/project2/bin/pythonを利用して欲しい。

一方で、quickrunのpythonの設定を見ると、:command "python"とpython決め打ちだったのであまり良くない。

how to solve it

実際にコードを読んでみたら、`quickrun/eval-paramater'が期待どおりの実装になっていた。
どういうことかというと、:commandに関数を渡せる。(渡した場合渡された関数を評価した結果が使われる)

こういうことができる。*1

("python" . ((:command . (lambda () (print "hey") "python")) (:compile-only . "pyflakes %s")
             (:description . "Run Python script")))
code

現在のディレクトリからvirtualenv上のpythonを探し出して使うように設定を変更。

`quickrun/virtualenv-setup'を評価してあげると有効になる。

e.g.

以下のような場合でもM-x quickrunが動作するようになった。

以下のような場合

普通のpythonだとモジュールが足りなくて実行できないような例。virtualenv上のpythonだと実行できる。

$ pwd
/home/podhmo/.virtualenvs/my/sandbox

$ python joinmodel.py
Traceback (most recent call last):
  File "joinmodel.py", line 1, in <module>
    import sqlahelper
ImportError: No module named sqlahelper

$ ~/.virtualenvs/my/bin/python joinmodel.py
name Person.name foo
age Person.age 20
skill Person.skill fire
id Person.id None

*1:もちろん表示されるheyには何の意味もない

テーブルをjoinした結果をモデルにmappingできる。

__table__にsqlalchemy.orm.joinした結果を代入すると、複数のテーブルをjoinした結果をmappingしたモデルが作れる。
(idのconflictを避けるためにorm.column_propertyを使うことが多い)

import sqlahelper
import sqlalchemy as sa
import sqlalchemy.orm as orm

Base = sqlahelper.get_base()
DBSession = sqlahelper.get_session()

person_skillset = sa.Table(
    "person_skill",Base.metadata,
    sa.Column("id",sa.Integer,primary_key=True),
    sa.Column("skill",sa.String(255)) #in real, this is foreignkey
    )

person_information = sa.Table(
    "person_info",Base.metadata,
    sa.Column("id",sa.Integer,primary_key=True),
    sa.Column("name",sa.String(255)),
    sa.Column("age",sa.Integer))

class Person(Base):
    __table__= orm.join(person_skillset,person_information, person_skillset.c.id==person_information.c.id)
    id = orm.column_property(person_skillset.c.id, person_information.c.id)

利用例

engine = sa.create_engine("sqlite://")
sqlahelper.add_engine(engine)
Base.metadata.create_all()

def _column_iter(model, obj):
    from sqlalchemy.sql.operators import ColumnOperators
    for k, v in model.__dict__.items():
        if isinstance(v, ColumnOperators):
            yield k, v, getattr(obj, k, None)

for k,f,v in _column_iter(Person,Person(name="foo",age=20,skill="fire")):
    print k,f,v

# name Person.name foo
# age Person.age 20
# skill Person.skill fire
# id Person.id None