Python で書く代数 - pytest で test class 編

前回 は pytest を使ってパラメータ化されたテストを実行しました.

実際に使ったテスト対象コードは, 群を Python で実装したコードでした. ABC モジュールと pytest によるテストを使って, 代数の公理的な定義を擬似的に表現しました.

ただし前回のテストコードではテスト関数が多く, 分類したい気持ちになってきます.

pytest のドキュメント を眺めると, 2.4.5 Parametrizing test methods through per-class configuration にクラスを使ってテスト関数をまとめる方法が書いてあります. 早速これを使います.

test hook

ここで使用するのはテストの前に実行される pytest_generate_tests 関数です. この関数でテスト実行前に, モジュールの中にあるクラスとそのメソッドからパラメータ化されたテストを生成しています.

def pytest_generate_tests(metafunc):
    """Collect and parameterize classes in this module as test cases.

    This test-case collecting function ignores module level functions.

    c.f. 2.4.5 Parametrizing test methods through per-class configuration
    """

    if metafunc.cls is None: # ignore module level functions
        return
    funcarglist = metafunc.cls.params[metafunc.function.__name__]
    argnames = list(funcarglist[0])
    metafunc.parametrize(argnames, [[funcargs[name] for name in argnames]
                                    for funcargs in funcarglist])

テストクラスには, テストコードを記述した関数をメソッドとして記述します.

class TestEquality:
    params = {
        'test_equals_reflexive_law': single_params,
        'test_equals_symmetric_law': double_params,
        'test_equals_transitive_law': triple_params
        }

    def test_equals_reflexive_law(self, a):
        assert a == a

    def test_equals_symmetric_law(self, a, b):
        if a == b:
            assert b == a

    def test_equals_transitive_law(self, a, b, c):
        if a == b and b == c:
            assert a == c

パラメータ化されたテストを実装するためにテストクラスには仕掛けが施されています. その仕掛けは params というクラスフィールドで, テストメソッドとそれが使用するパラメータの対応付けが辞書で設定されています. params 辞書の値は辞書のリストになっていて, 個々のテストケースが列挙されています.

single_params = [{'a': a} for a
                 in integer_values +
                 rational_values +
                 intint_values]

(ここでは single_params の書き方だけを載せましたが, モジュール全体は BitBucket のレポジトリ を参照してください)

前回の pytest.mark.parametrize デコレータとはパラメータ値の指定の方法が異るので注意してください.

pytest_generate_tests やその引数 metafunc の裏側については, また調べてブログに書くことにします.

それでは.

OSQA on Heroku

processes

$ mkdir osqa
$ cd osqa

$ brew install mysql
$ unset TMPDIR
$ mysql_install_db --verbose --user=`whoami` --basedir="$(brew --prefix mysql)" --datadir=/usr/local/var/mysql --tmpdir=/tmp
$ mysql.server start
$ mysql -uroot
mysql> CREATE USER 'osqa'@'localhost' IDENTIFIED BY '***';
mysql> CREATE DATABASE osqa DEFAULT CHARACTER SET UTF8 COLLATE utf8_general_ci;
mysql> GRANT ALL ON osqa.* to 'osqa'@'localhost';

$ svn co http://svn.osqa.net/svnroot/osqa/trunk .
$ virtualenv venv --distribute
$ source ./venv/bin/activate
$ pip install South django==1.3 django-debug-toolbar mysql-python markdown html5lib python-openid gunicorn
$ cp settings_local.py.dist settings_local.py

$ emacs settings_local.py
import os.path
import os
import sys
import urlparse
...
↓消す
DATABASES = {
...
↓heroku 用の DB 設定を追加
# Register database schemes in URLs.
urlparse.uses_netloc.append('mysql')

try:

    # Check to make sure DATABASES is set in settings.py file.
    # If not default to {}

    if 'DATABASES' not in locals():
        DATABASES = {}

    if 'CLEARDB_DATABASE_URL' in os.environ:
        url = urlparse.urlparse(os.environ['CLEARDB_DATABASE_URL'])

        # Ensure default database exists.
        DATABASES['default'] = DATABASES.get('default', {})

        # Update with environment configuration.
        DATABASES['default'].update({
            'NAME': url.path[1:].split('?')[0],
            'USER': url.username,
            'PASSWORD': url.password,
            'HOST': url.hostname,
            'PORT': url.port,
        })
        if url.scheme == 'postgres':
            DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql_psycopg2'

        if url.scheme == 'mysql':
            DATABASES['default']['ENGINE'] = 'django.db ...

スタート代数 #3

報告

数学の勉強会をしてきました.

http://partake.in/events/1009ce0b-a3ca-448e-bd83-8819bc6b1326

テキストは↓これです.

今回はテキストの「1.3 分解体」のところを扱いました.

ガロア理論はこの分解体の感覚が付くとだいぶ後が楽になるので, 丁寧に解説や指摘を入れました.

準備のポイント

数学のゼミの準備をするときは,

  • その命題, 定理が何を言っているかを, 自分の言葉で説明できるまで考える
  • 具体例を挙げる
  • 仮定の条件を1つ外したり, 別のものに変えたりして, 結論が成り立つか調べる
  • 成り立たない場合は反例を作る (それが条件が必要である根拠となる)

というあたりに気を付けて, 準備すると良いです. これをやっておくと単なるテキストの朗読ではなく自分の言葉で説明できますし, 講師役の人に突っ込まれてもすぐ答えられるようになっているはずです.

メモ

質問や議論ができる場所を作った方が良いかも, という話が出ました. LaTeX で数式が書けることが条件となると思うので, 色々調べないといけないですね. Wiki 感覚で LaTeX が書けると嬉しいのですが.

次回

次回は 7/23, 24 の 19:00--21:00 に「1.4 代数的閉体」「1.5 分離拡大体、非分離拡大体」を取り扱います. まだ日程は仮ですが2日間の集中勉強会の形式で行う予定です.

興味があれば是非参加してみてください.

それでは.

Python で書く代数 - pytest 編

テストについて

前回は群を Python で書いてみました. 群は公理というルールを使って定義されていました. この公理を Python で表現するのに, テストが使われていました.

Python でのテストと言えば unittest や nose が有名ですが, pytest という新しいテストライブラリのことを知ったので, これを使ってみました.

この pytest の大きな特徴はテストメソッドで使用する値を簡単にパラメータ化できることです.

人間がテストケースを考える場面では, ただ闇雲に適当な値を渡すのはテストとしては良いものではありません. とりあえずで値を列挙すると無駄な意味の無いテストケースを作ってしまうことが多く, テストの実行自体に時間がかかります. そしてエラーが起きたとしても, そのエラーがいったいどんな原因によって起きたのか考えないといけません. (個人的にはあまり頭を使わず安直にエイヤというのは好きではないです.)

ランダムな値を渡す手法は fuzzing と呼ばれていて, 人間がテストケースを想定する範囲の外にあるバグを見付けるのに使われるそうです. 詳しくはこのスライド http://www.slideshare.net/TokorotenNakayama/fuzzing-pyfes を見ると良いと思います.

群の公理は「群の任意の元」という表現を含んでいるため, 具体値を使用するテストでは無限の時間が必要になってしまい, 全てをテストすることは不可能です. なので, 今回の Group クラスのサブクラスのテストとしては, パラメータ化されたテストで特殊値の付近だけをテストすることにしました. 例えば, Integer クラスでは整数を表現しているので特殊値として 0 があります. 0 の付近の値でテストが通れば, 高い確率で他の値でも動くものと期待できます.

pytest によるテスト

pytest は easy_install もしくは pip コマンドでインストールできます. ここでは virtualenvwrapper を使って, pytest という名前の個別の環境にインストールしています.

pip, virtualenv, virtualenvwrapper についてはこの記事 http://d.hatena.ne.jp/rudi/20110107/1294409385 に詳しく載っています. そちらを参照してください.

$ echo $VIRTUALENV_USE_DISTRIBUTE
True
$ mkvirtualenv pytest
(pytest)$ pip install pytest

テストケースの実行は pytest コマンドで行います.

$ pytest tests/grouptest.py

pytest にテストケースとして認識させるには, メソッドの先頭を test にすれば良いようです. ここは nose などと仕様を合わせているようです. (ドキュメントには明記されていなかった気がしますが.)

肝心のテスト関数にパラメータを渡すところはデコレータを使用して以下のように書きます.

@pytest.mark.parametrize('cls', groups)
def test_ZERO(cls):
    cls.ZERO
    return True

pytest.mark.parametrize の第1引数にはテスト関数の引数の名前を, 第2引数にはテスト関数に渡す値の list を入れます. この list の部分を generator で書いてしまうとエラーが出て, テストが実行できません. どうやら pytest が最初にテストケースをカウントしていて, 長さが測れるものでないといけないようです. 最初エラーメッセージの意味が分からず, 長いことハマっていました.

複数の引数を持つテスト関数を書く場合は以下のようにします.

integer_values = [Integer(i) for i in range(-2, 3)]

@pytest.mark.parametrize(
    ('a', 'b'),
    [(a, b) for a in integer_values for b in integer_values]
)
def test_equals_symmetric_law(a, b):
    if a == b:
        assert b == a

第1引数が tuple になり, パラメータの値も tuple になっています.

今回使用したテストコードの完全なソースコードは https://bitbucket.org/cocoatomo/pyarith/changeset/08e381828aeb#chg-tests/grouptest.py を参照してください.

過去のリビジョンの URL を張っているのは, 実は現在は別の形式にしているからです. その話はまた次の記事で書こうと思います.

まとめ

この記事では pytest を使ってテスト関数の引数をパラメータ化し, そこに入れる値をリストとして書きました. これによって同じテスト関数を色々な値で実行できるようになりました.

pytest にはまだまだ機能があるので, 興味がある方は本家のドキュメントを読んでみてください.

Mac OS X で maven の出力が文字化けする

JUnit4 の Parametrized を Groovy で使う

概略

現在こっそり開発しているツールのテストに Groovy を使用しています. テスト対象は Java なのですが, テストには柔軟に書ける Groovy が便利です.

プロジェクト自体は Maven3 で管理していて, ビルド, テスト, パッケージングを全て pom.xml で設定しています. そこのちょっとした設定の間違いで数時間ハマってしまったので, 記録として残しておきます.

Groovy テストスクリプト

JUnit4 ではパラメータ化されたテストを実行することができます.

クラスに org.junit.runner.RunWith アノテーション, パラメータを供給するメソッドに org.junit.runners.Parameterized.Parameters アノテーションを付けると, そのクラスはパラメータ化されたテストを実行できるようになります.

詳しいことは http://groovy.codehaus.org/Using+JUnit+4+with+Groovy あたりを見てください.

pom.xml

pom をそのまま書くのが面倒なので,

<aaa>
  <bbb>
    <ccc>value</ccc>
  </bbb>
</aaa>

というのを

aaa
`-bbb
    `-ccc=value

と書くことにします.

プロジェクトの build 要素以下はこのようになっています.

build
`-plugins
  `-plugin
    `-groupId=org.codehaus.gmaven
    `-artifactId=gmaven-plugin
    `-version=1.4
    `-configuration
    | `-providerSelection=1.8
    `-executions
    | `-execution
    |   `-goals
    |     `-goal=generateStubs
    |     `-goal=compile
    |     `-goal=generateTestStubs
    |     `-goal=testCompile
    `-dependencies
      `-dependency
        `-groupId=org.codehaus.groovy
        `-artifactId=groovy-all
        `-version=1.8

この pom で mvn clean test を実行すると以下のようなエラーが出ました.

シンボルを見つけられません。
シンボル: クラス Parameterized$Parameters
場所    : org.junit.runners の パッケージ

Groovy で書いたテストスクリプトが Java クラスに変換され, さらに javac でコンパイルされます. その段階で上のエラーが出ています.

確かに Groovy から変換された Java ソースを見ると @Parameter アノテーションが @org.junit.runners.Parametrized$Parameter に変換されてしまっています. Java ソースとしては $ ではなく . が正しいはずなのでコンパイルエラーが出た理由が分かりました.

しかし, いくらググってもこの Groovy スクリプトのマズいところが見付かりません.

ふと思い立って goal=generateTestStubs の部分を消してみました. 「変な Java ソースを生成するくらいなら, そこを消してしまえ」と, 思い立ったというよりヤケクソでというのが正しい表現な気もします. すると無事テストが実行されました.

なぜかと考えてみると, そもそも generateTestStubs で生成された Java ソースは, メソッドの本体が return null のような意味の無いクラスでまさにスタブでした. なぜこのようなクラスが必要になるのかと言うと, おそらく Java 側からこの Groovy スクリプトを呼び出すコードをコンパイルするためのクラスなのでしょう. 今回, そのような Java クラスは無いので実はこのゴールは不要なのでした.

まとめ

ということで答えは「余計なゴールがあったために, 変な Java ソースをコンパイルしなくてはならなくなり, そこでエラーが起きていた」ということでした.

う〜ん, 我ながら嫌なハマり方をしましたが, 解決できたときはスッキリしました.

そして教訓は「ツールについてきちんと理解しよう」ということでした. maven は便利なツールですが, まだ自分にとって複雑なツールです. まだまだ勉強しないとですね.

それでは.

Python で書く代数

代数は数学の中でもプログラミング言語と近しい存在です. 歴史的な事実関係は逆でプログラミング言語が抽象代数的な発想の中から生まれてきた, と言えるでしょう.

この記事では代数学上の事柄について, プログラミング言語の Python を使って記述していきます.

代数

現代の抽象的な代数は公理による定義を行います. 「公理」というのはひらたく言うとルールのことで, そのルールから証明という手続きを経て, 命題や定理を導き出します.

公理では

  • あるべき対象
  • あるべき演算
  • 満たすべき性質

について記述します.

プログラミングで言えば

  • あるべきオブジェクトや持っているべきプロパティ
  • あるべきメソッドやオペレータ
  • オブジェクトやメソッドが持つべき性質

にあたり, これらはテストでチェックしたり, Java の interface および abstract class 相当の仕組みを利用して, 実装しなければならないメソッドを表現します.

この対応関係を着想として, まずは代数的対象のうち「群 (group)」を Python で表現してみます.

ここで載せるソースコードは全て https://bitbucket.org/cocoatomo/pyarith にあります. 順次アップデートしていきます.

群を扱った Python スクリプトは https://bitbucket.org/cocoatomo/pyarith/src/1b3a578e1678/group.py です. テストスクリプトは https://bitbucket.org/cocoatomo/pyarith/src/1b3a578e1678/tests/grouptest.py です.

クラス Group を継承したクラスが群となり, そのクラスのインスタンスが群の元に相当します.

abstract method として実装してある __eq__, __add__, __neg__ メソッドが, 群であるために必須のメソッドとなります. a = b は実は a.__eq__(b) のことであり, a + b, -a もそれぞれ a.__add__(b), a.__neg__() に相当します. またテストの方でその存在をチェックしている ZERO というフィールドも, それが存在することが群に課せられた条件です. つまり, 群には「等号比較」「加法」「零元」「逆元」が必須の条件なのです. (等号の性質に関しては通常は群の公理に含めません. ここでは等号も実装しているので, 等号の性質も一緒にテストしています.)

これらは好き勝手に実装して良いのではなく, 満たすべき制約条件があります. このテストスクリプトでは, それをテスト関数の形で表現しています. (演算によって群が閉じていることのテストが抜けていますね…… 追加しておきます.)

テストケースとその意味
test_equals_reflexive_law 等号の反射律
test_equals_symmetric_law 等号の対称律
test_equals_transitive_law 等号の推移律
test_add_ZERO 零元の性質
test_add_negative 逆元の性質
test_add_commutative_law 加法の可換性
test_add_associative_law 加法の結合律

それぞれの制約がどういうものかはスクリプトを見ればすぐに分かると思います. 群の公理 (ルール) はこれで全てです. http://ja.wikipedia.org/wiki/%E7%BE%A4_(%E6%95%B0%E5%AD%A6) と見比べて確認してみてください.

(可換性は全ての群が備えているものとは限りません. 可換性を持つ群を「可換群」, 可換性を持たない群を「非可換群」と呼びます.)

まとめ

この記事では, 群の公理を abstract method とテスト関数を使って記述しました. Python スクリプトとして記述することで少しだけ理解しやすくなれば嬉しく思います.

これだけの少ないルールから群の様々な性質が証明されていくのは不思議ですね.

次回は環や体を対象としたり, テストを行うのに使用した pytest モジュールについて書こうと思います.

それでは.

bitbucket の証明書あたりで怒られたので, その対処

http://blog.elliptium.net/2012/01/Mac-PyGame で pygame のインストール方法を書いたけど, 今日やってみたら fingerprint がなんたらかんたらと怒られた.

$ pip install hg+http://bitbucket.org/pygame/pygame
Downloading/unpacking hg+http://bitbucket.org/pygame/pygame
  Cloning hg http://bitbucket.org/pygame/pygame to /var/folders/9n/czcxj1b562v8njptzxxp01q40000gq/T/pip-h8eK40-build
中止: bitbucket.org の証明書(fingerprint は 24:9c:45:8b:9c:aa:ba:55:4e:01:6d:58:ff:e4:28:7d:2a:14:ae:3b)が不正
  Complete output from command /usr/local/bin/hg clone --noupdate -q http://bitbucket.org/pygame/pygame /var/folders/9n/czcxj1b562v8njptzxxp01q40000gq/T/pip-h8eK40-build:

----------------------------------------
Command /usr/local/bin/hg clone --noupdate -q http://bitbucket.org/pygame/pygame /var/folders/9n/czcxj1b562v8njptzxxp01q40000gq/T/pip-h8eK40-build failed with error code 255

https://groups.google.com/forum/#!msg/bitbucket-users/R2GqjuchNc0/GrGFnTsrkyUJ のコメントに従って, http://mercurial.selenic.com/wiki/CACertificates#Mac_OS_X_10.6_and_higher の通りにダミーの証明書を作成する.

$ openssl req -new -x509 -extensions v3_ca -keyout /dev/null -out dummycert.pem -days 3650
(色々聞かれるけど Enter 空打ち. pass phrase だけは4文字以上入れないといけないので適当に入力)
$ sudo cp dummycert.pem /etc/hg-dummy-cert.pem

後は ~/.hgrc あたりに

[web]
cacerts = /etc/hg-dummy-cert.pem

を追記.

だめだったーー!!!

良く考えれば fingerprint で怒られてるんだから, fingerprint 直せば良いんじゃなかろうか?

ってことで作業を取り消して, ~/.hgrc にある fingerprint を修正. (一応, Safari からも証明書を確認して fingerprint が合ってることは確かめた. これでいいんだよね??)

これで

$ pip install hg+http://bitbucket.org/pygame/pygame

が成功した.

sheaf, category and topos

訳あって「層・圏・トポス」を読んでいて, せっかくなので何考えながら読んでるかを晒してみる. 以下の地の文は俺の心の中のつぶやきである.

第1章 層

2. 前層

ここで前層の第一の定義が出てくるのだが, 与え方が形式的すぎてすぐには分からん. ただ, どうも集合 \(A\) で位相 \(\mathcal{O}(X)\) の元 (すなわち開集合) を関数 \(E\) によって parametorize しているっぽい雰囲気だ. \(\rceil\) は集合 \(A\) の元に開集合が右から作用してるのかなぁ. 3) の式を見るとそんな感じ. 集合の共通部分を取るのは集合の積と看做せるし.

とするとなんかどうも \(\rceil\) は集合の共通部分を取る操作に似ている. じゃあ \(E\) として恒等関数を取って, \(\rceil\) = \(\cap\) と見てみよう. 公理を全部満たしているので, \(X\) の位相は \(X\) の前層でもある.

1つ目の定義はなんとなく分かった.

MathJax test

inline -> \(x^2 + y^2\) <- inline

displayed $$ \sum $$

Licenses