ブログトップ 記事一覧 ログイン 無料ブログ開設

forest book Twitter

2012-02-09

データ駆動テストを nose と pytest でやってみた

| 07:31 | データ駆動テストを nose と pytest でやってみたを含むブックマーク データ駆動テストを nose と pytest でやってみたのブックマークコメント

pytest で初めてテストを書いてみました。

nose と比べて、データ駆動テスト *1 *2 の違いが大きかったのでまとめてみます。

準備

以下の素数判定を行うテスト対象関数があるとします。

PRIME = {2: True, 3: True, 4: False, 5: True, 6: False, 7: True}

def is_prime(num):
    return PRIME[num]

bitbucket にこの記事で紹介するテストコードを置きました。興味のある方は試してみてください。

Python 2.7 でテストしています。

(test)$ pip freeze
distribute==0.6.24
nose==1.1.2
py==1.4.7
pytest==2.2.3
virtualenv==1.7
wsgiref==0.1.2

最も簡単なデータ駆動テスト

nose でデータ駆動テストを行う場合、ジェネレーターでテストケースを生成します *3 。以下のようにループを使ったテストコードになります。

from nose.tools import ok_

def test_is_prime():
    for num in [3, 4, 5]:
        yield ok_, is_prime(num)

実行結果。

(test)$ nosetests -v test_nose-data-driven.py 
test_nose-data-driven.test_is_prime(True,) ... ok
test_nose-data-driven.test_is_prime(False,) ... FAIL
test_nose-data-driven.test_is_prime(True,) ... ok

======================================================================
FAIL: test_nose-data-driven.test_is_prime(False,)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/tools.py", line 25, in ok_
    assert expr, msg
AssertionError

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

3, 4, 5 という入力データに対して、num = 4 のときにテストが失敗します。num = 4 のときにテストが失敗しても 3 と 5 のテストは実行されているので、一回の実行で全ての入力データに対するテストが行えて便利です。もっとたくさんの入力データを取るテストの場合、何度も実行する手間隙が省けます。

しかし、テストが失敗したとき、この結果レポートでは、テストコードのどこで、どんな入力データに対して失敗したのか分かりません。パッと見て何のエラーなのかよく分かりません。

ここで pytest でも同じことをやってみます。

pytest でデータ駆動テストを行う場合、デコレーターで入力データを与えます *4 。ループを書かなくて良いのでテストコードもすっきりしますね。

import pytest

@pytest.mark.parametrize("num", [3, 4, 5])
def test_is_prime(num):
    assert is_prime(num)

実行結果。

(test)$ py.test -v test_pytest-data-driven.py 
============================= test session starts ==============================

test_pytest-data-driven.py:10: test_is_prime[3] PASSED
test_pytest-data-driven.py:10: test_is_prime[4] FAILED
test_pytest-data-driven.py:10: test_is_prime[5] PASSED

================================= FAILURES =================================
_______________________________ test_is_prime[4] _______________________________

num = 4

    @pytest.mark.parametrize("num", [3, 4, 5])
    def test_is_prime(num):
>       assert is_prime(num)
E       assert is_prime(4)

test_pytest-data-driven.py:12: AssertionError
====================== 1 failed, 2 passed in 0.02 seconds ======================

nose の結果レポートと比べると、驚くほど懇切丁寧なレポートです。テストコードの、どこで、どんな入力データに対してテストが失敗したのか一目瞭然です。

この結果レポートの違いを見るだけでも pytest を使う価値があります。

デバッガを使う

pytest はデコレーターで入力データを渡せましたが、この違いはデバッグのやり方にも影響します。nose と pytest 共にテストが失敗したときに pdb デバッガを起動するオプションがあります。

まずは nose でテストが失敗したときにデバッガを起動します。"--pdb-failure" オプションを指定します。

(test)$ nosetests -v --pdb-failure test_nose-data-driven.py
test_nose-data-driven.test_is_prime(True,) ... ok
test_nose-data-driven.test_is_prime(False,) ... > /Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/tools.py(25)ok_()
-> assert expr, msg

(Pdb) locals()
{'msg': None, 'expr': False}

(Pdb) w
  /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/case.py(327)run()
-> testMethod()
  /Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/case.py(197)runTest()
-> self.test(*self.arg)
> /Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/tools.py(25)ok_()
-> assert expr, msg

(Pdb) u
> /Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/case.py(197)runTest()
-> self.test(*self.arg)

(Pdb) locals()
{'self': test_nose-data-driven.test_is_prime(False,)}

(Pdb) u
> /opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/unittest/case.py(327)run()
-> testMethod()

(Pdb) locals()
{'self': test_nose-data-driven.test_is_prime(False,), 
 'orig_result': <nose.result.TextTestResult run=2 errors=0 failures=0>,
 'testMethod': <bound method FunctionTestCase.runTest of test_nose-data-driven.test_is_prime(False,)>,
 'success': False, 'result': <nose.result.TextTestResult run=2 errors=0 failures=0>}

デバッガを起動したものの、これは test_is_prime() のコンテキストではありません。そのため、入力値 (num) の情報もありません。

次に pytest でテストが失敗したときにデバッガを起動します。"--pdb" オプションを指定します。

(test)$ py.test -v --pdb test_pytest-data-driven.py 
... (snip)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /Users/t2y/work/repo/misc/data_driven_test/test_pytest-data-driven.py(12)test_is_prime()
-> assert is_prime(num)

(Pdb) locals()
{'@py_format4': 'assert False\n{False = is_prime(4)\n}', 'num': 4, '@py_assert2': False}

(Pdb) w
... (snip)
> /Users/t2y/work/repo/misc/data_driven_test/test_pytest-data-driven.py(12)test_is_prime()
-> assert is_prime(num)

test_is_prime() のコンテキストであることから 'num': 4 が確認できました。

より複雑なテストで失敗したとして、テスト関数のコンテキストにおける情報をみたいときに

import pdb; pdb.set_trace()

のようにテストコードを修正しなくても pytest なら "--pdb" オプションを指定してデバッグできます。

やはり pytest 良いですね。

テストフィクスチャを使ったデータ駆動テスト

wikipedia:XUnit スタイルの setup/teardown を使ったデータ駆動テストもやってみます。

nose も pytest もクラス内の setup()/teardown() メソッドを認識して、テスト関数の前後で実行してくれます *5 *6 。テストコード内の print() で出力したメッセージを表示するには、どちらも "-s" オプションを指定します。

実行順序を分かりやすくするためにデバッグメッセージを出力するように変更します。

def is_prime(num):
    print("called is_prime: {0}".format(num))
    return PRIME[num]

まずは pytest からテストします。

class TestPrimeNumber(object):
    def setup(self):
        print("\ncalled setup")

    def teardown(self):
        print("\ncalled teardown")

    @pytest.mark.parametrize("num", [3, 4, 5])
    def test_is_prime(self, num):
        assert is_prime(num)

    def test_function(self):
        print("called test function")
        assert True

実行結果。

(test)$ py.test -v -s test_pytest-data-driven.py 
============================= test session starts ==============================
collected 4 items 

test_pytest-data-driven.py:23: TestPrimeNumber.test_is_prime[3] 
called setup
called is_prime: 3
PASSED
called teardown

test_pytest-data-driven.py:23: TestPrimeNumber.test_is_prime[4] 
called setup
called is_prime: 4
FAILED
called teardown

test_pytest-data-driven.py:23: TestPrimeNumber.test_is_prime[5] 
called setup
called is_prime: 5
PASSED
called teardown

test_pytest-data-driven.py:27: TestPrimeNumber.test_function 
called setup
called test function
PASSED
called teardown
================================= FAILURES =================================
_______________________ TestPrimeNumber.test_is_prime[4] _______________________

self = <test_pytest-data-driven.TestPrimeNumber object at 0x1017ddc50>, num = 4

    @pytest.mark.parametrize("num", [3, 4, 5])
    def test_is_prime(self, num):
>       assert is_prime(num)
E       assert is_prime(4)

test_pytest-data-driven.py:25: AssertionError
====================== 1 failed, 3 passed in 0.02 seconds ======================

前節の普通の関数と同じ感覚でテストが記述できて、その結果レポートも期待したものが表示されます。良いですね。

次に nose でテストします。

class TestPrimeNumber(object):
    def setup(self):
        print("\ncalled setup")

    def teardown(self):
        print("called teardown")

    def test_is_prime(self):
        for num in [3, 4, 5]:
            yield ok_, is_prime(num)

    def test_function(self):
        print("called test function")
        ok_(True)

やはり先ほどと同様に記述して、一見テストも実行できるのですが、、、

(test)$ nosetests -v -s test_nose-data-driven.py 
... (snip)
test_nose-data-driven.TestPrimeNumber.test_function ... 
called setup
called test function
called teardown
ok

called is_prime: 3
test_nose-data-driven.TestPrimeNumber.test_is_prime(True,) ... 
called setup
called teardown
ok
... (snip)

is_prime()setup() の前に実行されています。このテストコードは意図した順番で実行されません。昔、これではまりました (> <)

nose のテストジェネレーターはテストケースを生成するものなので、正しくは以下のように記述します。

def test_factory(): 
    class FactoryTestCase(object): 
        def __init__(self, num):
            self.num = num

        def __call__(self): 
            ok_(is_prime(self.num))

        def setup(self): 
            print("\ncalled setup")

        def teardown(self): 
            print("called teardown")

    for num in [3, 4, 5]:
        yield FactoryTestCase(num)

実行結果。

(test)$ nosetests -v -s test_nose-data-driven.py 
test_nose-data-driven.test_factory ... 
called setup
called is_prime: 3
called teardown
ok
test_nose-data-driven.test_factory ... 
called setup
called is_prime: 4
FAIL
called teardown
test_nose-data-driven.test_factory ... 
called setup
called is_prime: 5
called teardown
ok
======================================================================
FAIL: test_nose-data-driven.test_factory
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/Users/t2y/work/repo/misc/data_driven_test/test_nose-data-driven.py", line 41, in __call__
    ok_(is_prime(self.num))
  File "/Users/t2y/.virtualenvs/test/lib/python2.7/site-packages/nose/tools.py", line 25, in ok_
    assert expr, msg
AssertionError
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)

何となくテスト関数が少し分かり難くなった気がします。

さらに補足として、テストジェネレーターは unittest.TestCase のサブクラスでは動作しません。

関数単位のテストフィクスチャを使ったデータ駆動テスト

前節では、テスト関数の入力データに対するそれぞれの呼び出し毎に setup()/teardown() が呼ばれていました。似て非なる例として、テスト関数に対して一回だけ呼び出すテストをやってみます。

nose だと関数に対する with_setup() デコレーターで指定します。テストジェネレーターで実行するときは、やはり FactoryTestCase を定義しないといけません。

from nose.tools import with_setup

def mysetup():
    print("called mysetup")

def myteardown():
    print("called myteardown")

@with_setup(mysetup, myteardown)
def test_is_prime_with_setup_teardown():
    class FactoryTestCase(object): 
        def __init__(self, num):
            self.num = num

        def __call__(self): 
            ok_(is_prime(self.num))

    for num in [3, 4, 5]:
        yield FactoryTestCase(num)

実行結果。

(test)$ nosetests -v -s test_nose-data-driven.py 
called mysetup
test_nose-data-driven.test_is_prime_with_setup_teardown ...
called is_prime: 3
ok
test_nose-data-driven.test_is_prime_with_setup_teardown ...
called is_prime: 4
FAIL
test_nose-data-driven.test_is_prime_with_setup_teardown ...
called is_prime: 5
ok
called myteardown
... (snip)

mysetup()/myteardown() が一回だけ呼ばれていますね。

次に pytest です。pytest は、もう少し汎用的な仕組みで、テストモジュール内の、全ての関数に対してフックする setup_function()/teardown_function() を定義する方法があります。

これまでの例と違う点として、このテストは pytest.mark.parametrize に入力値のリストを渡して、テスト関数内にループを記述しています。

def setup_function(function):
    print("\ncalled setup: {0}".format(function))

def teardown_function(function):
    print("\ncalled teardown: {0}".format(function))

@pytest.mark.parametrize("nums", ([3, 4, 5],))
def test_is_prime2(nums):
    for num in nums:
        assert is_prime(num)

実行結果。

(test)$ py.test -v -s test_pytest-data-driven.py 
============================= test session starts ==============================
platform darwin -- Python 2.7.2 -- pytest-2.2.3 -- /Users/t2y/.virtualenvs/test/bin/python
collected 1 items 

test_pytest-data-driven.py:39: test_is_prime_with_setup_teardown[.0] 
called setup: <function test_is_prime_with_setup_teardown at 0x1017d0758>
called is_prime: 3
called is_prime: 4
FAILED
called teardown: <function test_is_prime_with_setup_teardown at 0x1017d0758>
... (snip)

実行できましたが、このテスト方法はループ内に assert 文を記述しているので num = 4 で失敗するとテストが終了します。さらに setup_function()/teardown_function() は全ての関数に対してフックしてしまうので、使い勝手が悪いかもしれません。

もう1つの方法として、テスト関数の引数に対してフックする方法があります *7 *8 。これは nose にはない仕組みで、ちょっと驚きました。

def mysetup():
    print("\ncalled mysetup")
    return [3, 4, 5]

def myteardown(nums):
    print("\ncalled myteardown: {0}".format(nums))

def pytest_funcarg__nums(request):
    return request.cached_setup(setup=mysetup, teardown=myteardown)

def test_is_prime_funcarg_setup_teardown(nums):
    for num in nums:
        assert is_prime(num)

"nums" という引数に対してフックする pytest_funcarg__nums() を定義して、その中で任意の setup/teardown 関数を指定します。そして、mysetup() がテスト関数の入力データになる "nums" を返します。

実行結果は先ほど同じです。こちらの方が任意の関数に対しては使いやすそうですが、テストコードが分かり難くなってしまうので使い方は限定されるように思います。

(test)$ py.test -v -s test_pytest-data-driven.py 
... (snip)
test_pytest-data-driven.py:55: test_is_prime_funcarg_setup_teardown 
called mysetup
called is_prime: 3
called is_prime: 4
FAILED
called myteardown: [3, 4, 5]

まとめ

データ駆動テストを nose と pytest で行うときの違いをまとめました。

複数の入力データを与えるテストを行う場合、どちらのライブラリも機能的には同じようにテストできますが、結果レポートの分かりやすさ・デバッグのしやすさを考慮すると pytest の方が使い勝手が良いと私は思いました。

2012-02-01

アリエル・ネットワークでアルバイトをしてきました

| 13:29 | アリエル・ネットワークでアルバイトをしてきましたを含むブックマーク アリエル・ネットワークでアルバイトをしてきましたのブックマークコメント

3ヶ月半という短い期間でしたが、アリエル・ネットワーク (以下アリエル) でプラグラマーとしてアルバイトしてきました。

よく見かける、どこそこでインターンシップをしてきました風な記事を書いてみます。普通はそういった記事を学生さんが書いているものですが、この記事は普通の無職の人が書いています。

最終日に「開発方法論提案 改めアリエル開発の所感」というタイトルで発表しました。

私が過去にいくつかの会社で働いてきた中で、アリエル開発で改善したら良いと思うことがあったら提案してほしいと言われていました。とはいえ、私の経験よりもアリエル開発の方がずっとレベルが高かったため、釈迦に説法な気がして、そんなタイトルに落ち着きました。この記事では、その内容からいくつか抜き出して紹介します。

私がやったこと

アリエル・エンタープライズ という Web アプリなグループウェアの開発に携わりました。

Trac でアサインされたチケット (バグや機能拡張) に対して修正するといった感じです。4-5年ぐらい開発を継続しているらしく、製品としての中核や部品、UI はかなり作り込まれていました。そのため、自分でコードをガリガリ書くというよりは、製品の動作や仕組みを理解するためにコードを読んで原因を特定したり、既存のコードを再利用して機能拡張するといった開発作業がほとんどでした。

製品の規模もコード量が数十万行というオーダーの、私にとってはこれまでで最も大規模なアプリケーションでした。開発を通して、大規模アプリの設計、ソースコードの読み方、Java のイディオムも少しずつ分かってきておもしろかったです。

アリエルの開発は、プロダクト系の開発だと Java、UI 系の開発だと Javascript がメインのようです。ただプロダクトと連携するツール類は Ruby や Python で書かれたものもいくつかありました。なので、業務で Java か Javascript しか書けないというわけでもありません。試験的な取り組み、プロトタイプ的なツールなどは、開発者の好みの言語で実装できそうに思えました。実際、私自身も Python でプロダクトと連携するツールを実装しました。

アリエル開発の良かったこと

f:id:t2y-1979:20120201100024p:image

開発体制/インフラ

雑誌や書籍で見かける類いの開発手法を当たり前のように業務の中に取り入れてました。開発インフラが整っていると、開発者はこんなに楽なんだということを実感しました。

また、開発者にとっての業務の中心は Trac です。チケット管理、wiki、ドキュメント、レポーティング (グラフ)、技術メモなど、かなり活用されてました。会社として開発情報を統合的に一元管理しようという意図をもって Trac に集約していて良い考えだなと思いました。

そんな中、チケットが分裂・派生していき、手動で関連チケットのリンクを張るのが煩わしくなって TracTicketReferencePlugin - Trac Hacks - Plugins Macros etc. - Trac も作ってみました。これはアリエルの 10% ルールという枠組みの中で開発したものです。アリエルの Trac で耐えたら、世の中では大丈夫だろうと個人的に思っています。

仕様の決め方

f:id:t2y-1979:20120201111915p:image

f:id:t2y-1979:20120201111916p:image

アリエルの開発における仕様の決め方に、最初は戸惑ったのですが、これはこれで良いこともありそうだと後になって考え直した次第です。緩い感じに仕様を策定していきます。

バグ修正的なチケットは、バグを直せばいいだけです。大きな機能追加は全く違った開発プロセスを歩むので、ここでは書きません。適切な粒度の機能追加的なチケットは、大まかな方向性は決まっていますが、詳細は決まっていません。基本は担当開発者がまず、外部仕様策定から始めます。仕様は、チケットをあげたヒトやプロジェクトマネージャ、プロダクトマネージャと相談しながら決まっていきます。プロダクトマネージャがこうしたいと思っても、表向きは担当開発者が決める形になっています。ただ、あまりにも紛糾していて物事が進まないときは、偉いヒトの強権が発動されます。

とある開発の風景 | 仕事術 | 踊るプログラマ物語 | あすなろBLOG

然るべき人が、ちゃんとした仕様を決めて、一方的に押し付けるというよりは、そのチケットの関係者が議論しながらより良い仕様に作り上げていくといった感じです。この過程を厳密にシステム化、もしくはプロセス化しないことで関係者間のコミュニケーションを促進している面もあると私は思いました。困ったら関係者のところへ行って「これってどういうことですか?」と聞くしかありません。そこで要件の齟齬や考え方の違いを認識して、やり取りから開発者も学ぶ、視野が広がるといったことに繋がるように思いました。

組織的な牽制機構

その緩い仕様の決め方にはもう1つのメリットもあります。

f:id:t2y-1979:20120201111917p:image

f:id:t2y-1979:20120201111918p:image

f:id:t2y-1979:20120201111919p:image

組織的な牽制機構が働かないと、ほとんどの組織は堕落します。

組織が1つだけだと、なぁなぁになってしまいます。組織が2つだと、けんかして対立関係になりがちです。組織が3つで、相互に牽制する関係が私の開発経験の中では、結果的にうまくいっていたように思います。アリエルもそういった3つ巴的な体制になっていて良さそうに私は思いました。但し、組織が分かれると、情報共有されなかったり、生産性が落ちたりするので、横の連携の工夫が必要になります。

会議がなかった

f:id:t2y-1979:20120201111920p:image

私がアルバイトだったこともあるかもしれませんが、会議がほとんどありません。こんなに会議がない会社も初めてでした。週報も書かないので、週例ミーティングがなく、グループミーティングのようなものもありませんでした。意図的に会議を減らしているように思いますが、開発に集中してたら1日中全く話さなくて終わるような日もあったかもしれません。

しかし、月に1回の開発部の全体会議がいまいちでした。私からの唯一の提案は、どうせやるならちゃんと全体会議をやった方が良いですという当たり前の意見を述べてみました。

また、Trac の活用度が高いため、開発の進捗状況やいろんな数字が出せるので、そういった数字からビジョンを語ると良いのではないかと偉そうなことを言ってみました。実際に Trac 上ではロードマップを管理して、バーンダウン・チャートも出力されていました。

f:id:t2y-1979:20120201120059p:image

パッケージ開発は長期的な展望をもって開発するので、普通の開発者は何を目指して開発しているのか、いまどういう状況なのかがよく分からなくなることがあります。何かしら開発者の意思統一を図るというか、共有するためにビジョンが必要です。ビジョンを語るのはとても難しいことですが、かっこいいビジョンばかり語っても本当の意味ではよく分からないので、実際の数字から現状分析や今後の方向性を語ると分かりやすくて良いのではないかと私は思います。

f:id:t2y-1979:20120201120100p:image

まとめ

・・・

この記事そのものは特に何かを伝えたいわけでもないのですが、アリエル開発の雰囲気が少しでも伝われば良い、、、のかな。

発表後に CTO がやってきて「その資料、公開しても良いです。公開するしないの判断は任せます」という謎なコメントを残して去っていきました *1 。そのまま公開しても中の人でないと、半分ぐらいは意味が分からないので補足を加えながら、自分の考えも整理してみました。

アリエルの文化はアドベントカレンダーを辿ってみると、おもしろいと思います。

開発者も募集してるようです

私から見ると、開発者にとっては居心地の良い開発環境、職場だと思います。いやな人がいない。

まずはアルバイトからも受け入れているようです。興味のある方は応募してみると良いと思います *2

*1勝手にクックパッド vs. アリエル | ありえるえりあ

*2:アリエル・ネットワーク社の選考や採用に私は全く関係ありません

2012-01-06

イテレーターという抽象概念にもの思い

| 09:53 | イテレーターという抽象概念にもの思いを含むブックマーク イテレーターという抽象概念にもの思いのブックマークコメント

元ネタ:

最近、Java のコードばかり書いています。

Java には、Python にはない wikipedia:抽象型 としてのインターフェースがあります。

パーフェクト Java から引用すると、インターフェースは、

オブジェクトにそのインターフェースの決めた振る舞いを期待することの表明

だと説明されています。また Java でプログラミングする上で、インターフェースを意識した抽象化が、複雑さの軽減、保守しやすいコードを書くのに重要だとあります *1

以前、EuroPython 2011 のイベントレポートを執筆した際に *2 、私はイテレーターという用語の扱い方に何となく腑に落ちていませんでした。Python では抽象クラスやインターフェースが言語機能として提供されていません。そのとき @ に「イテレーターはインターフェースだと考えたら良い」と教えてもらって、分かったような分かってないような日々を過ごしていました。

wikipedia の定義によると、

イテレータ (Iterator) とは、プログラミング言語において配列やそれに類似するデータ構造の各要素に対する繰返し処理の抽象化である。

イテレータ - Wikipedia

とあります。イテレーターと聞いて Python で言う iterable なオブジェクトであるリストやタプルをイメージしてしまっていたのが理解を妨げていました。

また Java の話しに戻ります。

最近、Java のコードばかり、、、あっ、違う。

閑話休題。Java5 から利用できる拡張 for 文という構文を使うと、Python の for in 文と同じような感覚でイテレーターを扱えます。例えば、インターフェース java.util.List の定義を調べてみます。すると、java.util.Iterator を返すインターフェース java.util.Iterable を継承していることが分かります。UML で書くと、こんな感じです (たぶん) 。

f:id:t2y-1979:20120106083356p:image

Java の Iterable インターフェースの iterator() メソッドが、Python でイテレーター型をサポートするための __iter__() メソッド、Iterator インターフェースの next() メソッドが同じく Python で言う next() メソッドに相当します *3 。つまり、どちらの言語もほとんど同じような仕組みになっているわけです。

java.util.List で宣言したオブジェクトであれば、イテレーターのインターフェースを備えている (と期待できる) ので、どんなデータ型であっても拡張 for 文で繰り返し処理できます。言い換えれば、Java.util.List と宣言することが、繰り返し処理できることの表明であり、プログラマへもその意図を伝えられます。

インターフェースにより、型の違いや実装の詳細を意識させずに抽象化できることが Java におけるパラダイムの1つなんだと少し分かってきました。つまり、あるデータ型のオブジェクトが与えられたとき、

List<Item> data = new myData();

data オブジェクトの詳細を知らなくても iterable なオブジェクトだと分かります。もちろん Python でも似たようなことはできます。

class MyIterable(object):
    def __iter__(self):
        return self
    def next(self):
        return self.next()

class MyData(Iterable):
    ...

data = MyData();

この場合、継承している MyIterable クラスの実装を覗き見ることで、あぁこれは iterable なオブジェクトなんだと分かるわけですね。

Java と Python で一体何が違うのか。

Java はイテレーターという抽象化された概念とその実装がインターフェースを使うことで明確に分離されています。しかし、Python の場合、分離されるかどうかは実装依存になってしまいます。MyIterable クラスを定義せずに直接、MyData クラスに __iter__() と next() メソッドを実装しても構いません。このことが私にとっては、イテレーターと呼び方を変えても、結局のところはリストやタプルのようなオブジェクトのことなんでしょ、、、といった、抽象概念としてのイテレーターと iterable なオブジェクトとの違いを混同して考えてしまう要因となっていました。

Python でプログラミングするだけなら、混同して考えてもそう害はないのかな?という気もしますが、プログラミング一般の概念として他の人と話したときにちょっと違和感を感じたものの正体がようやく分かってきた気がします。また Java を勉強していたら Python の特徴をより理解した気持ちになってちょっと嬉しかったです。

まとめると、最近、Java のコードばかり書いているんです。

2011-12-25

本当の本当は誰が怖い!?

| 13:55 | 本当の本当は誰が怖い!?を含むブックマーク 本当の本当は誰が怖い!?のブックマークコメント

好評につき続編です。はい、嘘です。

先日、 本家Ariel Advent Calendar 2011 *1 の記事を書きましたが、今回は 元祖Ariel Advent Calendar 2011 *2 を書きます。このブログに読者という方々がいるならば、一体何をやってるんだろうか?と首を傾げたくなるでしょうが、大丈夫です。私自身、その意味も意図も、理由すら分からずに書いています。

今北産業

なぜか CTO が本家からあぶれてしまい、元祖を立ち上げて所謂、骨肉の争いに突入しました。

そして、ダークサイドに落ちたありえるたんの *3 、闇えるたん (@) も現れ、三竦みの陣容を取っています。

私は日々、3強の影に怯えながら、ひっそり開発を継続しています。

番長の暗躍

なるべく私は目立たないように開発していますが、たまに番長に見つかってしまうときがあります。

これは Trac のチケット管理のやり取りです。

とめさんをいじめて退職させる

f:id:t2y-1979:20111224140459p:image

とめさんは要領は良くないものの、がんばってテストをしていました。何とも残念なお話です。

リグレッションを起こした開発者への指導

f:id:t2y-1979:20111224141354p:image

私がある機能拡張を行った際に、そのスキーマ定義が完全ではなくて、別の箇所でリグレッションを起こしてしまいました。一旦は、私宛に担当を割り振ったものの、私が直す前に番長に修正されてしまいました。

つまらんミスをしやがって。もうお前にコードは触らせない。

と、言われているようなものです。

闇えるたんの胎動

行動がまったく読めません。

インドへ旅立った CTO を追いかけて行って闘いを繰り広げているようです。

闇えるたんよ。覚悟なさい | ありえるえりあ によると、会社ブログもクラックされたようです。

CTO との軋轢

もちろん CTO と社員との諍いも絶えません。

悪のりするときは、とことん悪のりしたら良い。

という総帥の教えに従います。

もともとアドベントカレンダーのイメージはこうでした *4

f:id:t2y-1979:20111224152102p:image

CTO がインドへ行った間隙を突いてコラージュされました。

f:id:t2y-1979:20111224152103p:image

もうやりたい放題です。

CTO は、きっと反攻の策を練っていることだと思います。

組長の真実

どうやら組長と呼ばれる人もいるそうです、まぁ、私なんですが。

一説には組長も怖いという風評があるようですが、それは明らかな間違いです。

組長は、ただ朝早いだけで人畜無害です。朝早くきて、ひっそりと開発に明け暮れてるだけのようです。

まとめ

かなり内輪感の強い内容を紹介しました。

開発というお仕事は、時にどうして良いか分からなくて不安に陥ったりします。世の中、本当に怖いことはたくさんあります。どんな状況であっても、心にゆとりをもつこと、ユーモアを受け入れること、おもしろいと思えることが大事です。

アリエルという会社の、そんな雰囲気が伝われば良いなと思います。

2011-12-18

pyrtm の Python 3 対応

| 16:12 | pyrtm の Python 3 対応を含むブックマーク pyrtm の Python 3 対応のブックマークコメント

connpass-(第14回)Python mini Hack-a-thon に参加してきました。

Python で Remember The Milk API を扱う pyrtm 0.4 をリリースしました *1

やったこと

Python 2/3 両対応です。

たまたま Python3 対応の Pull リクエスト が来ていました。やろうと思えば、いつでもできる類いの修正ですが、こういうリクエストが来るということそのものが嬉しいですね。リクエストをもらったことをきっかけに取り組みました。

Pull リクエストの内容は、2to3 をベースに Python 3 で動作するようにした修正でした。Python 2/3 両対応するにあたって、

  • 2.x 系と 3.x 系のソースコードを完全に分離する
  • 2.6 以上を対象に Python 3 の構文を使う (2.5 以下はサポートしない)

の2通りのやり方があります。周りの開発者にどっちが良いのさ?と聞いてみたら後者の方を支持する意見が多かったのでそちらで対応しました。

Python 3 対応については ライブラリをPython3対応に書き換える がとても参考になります。

実際に pyrtm でやってみて遭遇した 2 と 3 の違いをいくつか取り上げます。Python 2.6 は Python 3 の構文をサポートしているとは言っても、何かしらエラーが起きることもあるので、ちゃんと両方でテストする必要がありそうです。

  • ライブラリ名/構造の違いによるインポート系の修正
try:                                                                                
    from urllib.request import urlopen                                              
    from urllib.parse import urlencode                                              
except ImportError:                                                                 
    from urllib import urlencode, urlopen

こういうのはどうしようもないんですよね?

  • print 関数への file 引数
-        print('Usage: rtm_appsample APIKEY SECRET [TOKEN]', file=sys.stderr)
+        sys.stderr.write('Usage: rtm_appsample APIKEY SECRET [TOKEN]\n')

sys.stderr じゃないといけない理由はないのですが、オリジナルのコードを尊重してみました。

  • sort メソッドの key 引数
-    keys.sort()
+    keys.sort(key=str)

Python 3 だと、異なるオブジェクトが入ったリストで sort() メソッドを呼び出すと TypeError になります。key 引数で比較関数へ渡すキーの変換方法を明示する必要があるようです。

>>> [1, "a"].sort()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: str() < int()
まとめ

Mac OS X と Fedora15 の Python 2.6 と 3.2 でテストしました。pyrtm ぐらいのツールなら、対応そのものは難しくありませんが、テストするのがちょっと面倒ですね。

2012/2/4 追記

上述した例で print() 関数の file 引数がエラーになっているのは、Python 2.6 では print 文として解釈されているからでした。Python 2.6 以降で print() 関数を使うには、以下の記述が必要でした。

from __future__ import print_function

shimizukawashimizukawa 2011/12/19 16:50 > ライブラリ名/構造の違いによるインポート系の修正

http://packages.python.org/six/ を使うとある程度は吸収してくれます。
しかし残念ながらurllib関連はサポート外、ってURLのページに明記されてますね><

t2y-1979t2y-1979 2011/12/20 05:59 コメントありがとうございます。
six はそういったところも見てくれるんですね。
six の依存関係ができるのと、ちょっとソースの見通しが悪くなるのとどちらが良いか次第かな。
それにしても urllib なー。

2011-12-13

本当はどっちが怖い!? 番長 vs CTO

| 12:00 | 本当はどっちが怖い!? 番長 vs CTOを含むブックマーク 本当はどっちが怖い!? 番長 vs CTOのブックマークコメント

アリエル・ネットワーク さんでアルバイトをしています。

以前、経営者が「エンジニアの楽園を目指す」と喧伝していた会社で働いたことがありますが、社内の、開発者の雰囲気はそれに近いものがあります。

会社として Ariel Advent Calendar 2011 : ATND を行っているので、私も社内の雰囲気が分かるエピソードを書いてみます。アリエルの開発者の記事は ありえるえりあ | 上から読んでも下から読んでも・・・ で読めます。

番長とは

アリエルの開発マネージャー (?) で、Python コミュニティでは 番長 と呼ばれている方がいます。

私が初めて出会ったのは、Python Code Reading 10 という勉強会で発表したときでした。その後、番長が発表された 第10回InfoTalk 「Python Twistedフレームワークで始める非同期ネットワークプログラミング」 に参加したり、勉強会やイベントで会ったりしたのが縁でアルバイトさせてもらっています。

社内ではラスボスと 呼ばれている ようです。いいえ、実際には呼ばれていません。みんな 怖くて そう呼べないので、文章中でのみ登場する想像上のあだ名です。ラスボスを倒さないとエンディングに辿り着けません。アリエルの開発者たちは、そのために技術力を磨き、仲間を集い、日々がんばっています。

CTO とは

そんな番長にも怖い存在がいるようです。それは CTO です。

私は以前から番長のブログを読んでいたのですが、ことあるごとに CTO は 怖い と書かれています。いくつか紹介します。

僕は昔から優しいんですがね。アリエルではCTOが一番怖いですよ。

年末は今年を振り返るモノらしいけど・・・ | | 踊るプログラマ物語 | あすなろBLOG

CTOは歴代のプロジェクトマネージャの中でも異色のマネージメントで現場に混乱と狂気をもたらしたのです

いろいろなプロジェクトマネージメント | その他(IT) | 踊るプログラマ物語 | あすなろBLOG

先日、私も CTO の怖さを発見しました。

アリエルのスタート地点は、Javaと聞いて、ふっと鼻で笑える地点です。

新卒向けカリキュラムで出す課題 | ありえるえりあ

ちなみに新卒の最初の課題は wikipedia:エイト・クイーン だそうです。

実は仲良し?疑惑

番長と CTO は、よく一緒にお昼ご飯を食べに行っています。

私もちょくちょく同行していますが、ぱっと見た感じでは結構仲良しです。お二方の名誉のために断っておきますが、お昼ご飯を食べに行って、怖い思いをすることはありません。ご飯を食べているときにエイト・クイーンを実装しろとは言われないので、安心してください。

よく出る話題としては、

番長は怖い人だ。

と、

CTO が本当は怖い。

です。

私はどっちも怖いです。

アルバイトは見た!

先日、現場を見てしまいました。

社内でのメールのやり取りで CTO がこんなことを言いました。削除されないよう、証拠としてスクリーンキャプチャを取りました。

f:id:t2y-1979:20111213083615p:image

なんと! CTO は自分が Devil *1 だと言い出しました。これが混乱と狂気の始まりか。もうエイト・クイーンを実装しても許してくれそうにありません。

Devil 宣言をして、開発者を蹂躙し始めた CTO に対して、番長の反撃はこうでした。

f:id:t2y-1979:20111213083616p:image

アリエルは 怖くて楽しい 会社です。

明日のアドベントカレンダーは @ です。

2011-12-11

Python3 の関数アノテーションを使って自動テストする

| 16:56 | Python3 の関数アノテーションを使って自動テストするを含むブックマーク Python3 の関数アノテーションを使って自動テストするのブックマークコメント

先日、引数に @Nullable アノテーションが付いた引数をもつ関数をリファクタリングして、関数分割してコミットしたら、ビルドサーバーに仕掛けられた FindBugs™ - Find Bugs in Java Programs に、@Nullable が付いてるのに Null チェックしてないよと怒られました (; ;)

Java のコードに慣れないため、Eclipse のお告げに従ってリファクタリングし、Eclipse がチェックできなかったものを見逃してしまったわけです。もちろん修正するのは簡単だけど、何か恥ずかしい。

ちょっと調べたら、Eclipse プラグインもあるようです *1 。Eclipse に FindBugs プラグインをインストールしてみようー。

。。。

( ゚д゚)ハッ! 間違えた!

今日は 2011 Pythonアドベントカレンダー(Python3) を書くよ!

Python も関数アノテーションが書けるようになりました

PEP 3107 -- Function Annotations によると、Python3 から関数アノテーションを書けるようになりました。

def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):
    ...

このサンプルを見ると、式を記述できることを意図してるのか (?)、普通に intstr といった型を表す方が一般的な用途かなと思います。そして、func.__annotations__ にシグネチャがディクショナリとして保持されます。

{'a': 'x',
 'b': 11,
 'c': list,
 'return': 9}

また Python2orPython3 - PythonInfo Wiki によると、関数アノテーションは Python 2.x にはバックポートされないようです。Python3 でしか利用できないため、実際に関数アノテーションを書いているコードを私は見たことがありませんでした。

関数アノテーションがあると何が嬉しいの?

そういう方は、先にアドベントカレンダーの3日目 *2 を書かれた @Python 3.0 Hacks:第7回 関数アノテーションでスマートにプラスアルファの実現|gihyo.jp … 技術評論社 を読みましょう。

この記事の中では、関数アノテーションを使うと、以下のようなことが簡潔に表現できて嬉しいと紹介されています。

  • それ自体がドキュメントになる
  • 自動型変換に利用する
  • overloading(多重定義)を定義する

但し、現在のところ、関数アノテーションは単に情報として保持しているだけです。そのため、このシグネチャをどう使うかはプログラマー次第、そしてサードパーティーのライブラリを待ちましょうという段階のようです。

まだ Python3 が普及していないせいか、関数アノテーションを使って型チェックやバリデーションをしてくれる anntools も開発が活発ではないようです。anntools を使うと、Python 2.x 系もデコレーターで関数アノテーションを追加することができます。とはいえ、この類いの拡張は、 (必要なら) 自分で実装済みだと思うので、そうではない既存のコードをわざわざ修正しようというインセンティブは低いかなと思います。

シグネチャを使って何をするか?

最も分かりやすい利用例としてはテストですね。そこで、ランダム自動テストをやってみましょう。

QuickCheck: An Automatic Testing Tool for Haskell の Python 実装である paycheck が Python3 対応しています。paycheck を使うと、データ駆動テストを簡単に実装できます。本稿では paycheck と nose を使ってランダムなデータ駆動テストをやってみます。

その前に開発環境を作らないと、、、

そう言えば virtualenv も Python3 対応していました。仮想環境を作って、paycheck と nose をインストールします。

$ /opt/local/Library/Frameworks/Python.framework/Versions/3.2/bin/virtualenv --distribute ~/.virtualenvs3/advent
$ ~/.virtualenvs3/advent/bin/easy_install paycheck nose
$ source ~/.virtualenvs3/advent/bin/activate
(advent)$ which python
/Users/t2y/.virtualenvs3/advent/bin/python
(advent)$ python
Python 3.2.2 (default, Nov  5 2011, 19:51:07) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import paycheck

それから IPython も使いたいですね。

$ sudo port install py32-ipython # ipython は MacPorts を使ってる

IPython に virtualenv 環境を考慮したライブラリパスを設定します。このコードはどっかからのコピペです。print 文ではなく print 関数ですよ。

(advent)$ vi ~/.ipython/profile_python3/ipython_config.py
import site
from os import environ
from os.path import join
from sys import version_info

if 'VIRTUAL_ENV' in environ:
    virtual_env = join(environ.get('VIRTUAL_ENV'),
                       'lib',
                       'python%d.%d' % version_info[:2],
                       'site-packages')
    site.addsitedir(virtual_env)
    print('VIRTUAL_ENV ->', virtual_env)
    del virtual_env
del site, environ, join, version_info

(advent)$ ipython3-3.2 
...
VIRTUAL_ENV -> /Users/t2y/.virtualenvs3/advent/lib/python3.2/site-packages
In [1]: import paycheck

はい。準備が整いました。ちゃんとした Python3 環境がなかったんです(> <)

とにかく関数アノテーションを実際に書いてみる

試しに書いてみる。型のみを記述するなら、そんなに気持ち悪くないかな (違和感を感じない) 。

(advent)$ vi others.py
__all__ = ["foo", "bar", "baz"]
 
def foo(a: str, b: int, c: {str: int}, d: float) -> tuple:
    return a, b, c, d

def bar(a: str, b: int, k: str="keyword") -> str:
    return "'{}' + '{}' + '{}'".format(a, str(b), k)

def baz(a: str, b: int, *args: tuple, **kwargs: dict) -> list:
    return [a, b, args, kwargs]

__annotations__ の中身も覗いてみます。

In [2]: foo.__annotations__
Out[2]: {'a': str, 'b': int, 'c': {str: int}, 'd': float, 'return': tuple}

In [3]: bar.__annotations__
Out[3]: {'a': str, 'b': int, 'k': str, 'return': str}

In [4]: baz.__annotations__
Out[4]: {'a': str, 'args': tuple, 'b': int, 'kwargs': dict, 'return': list}

普通のデータ駆動テストをやってみる

先に paycheck の使い方を覚えておきましょう。

(advent)$ vi tests/test_with_paycheck_sample.py 
# -*- coding: utf-8 -*-

from paycheck import with_checker

@with_checker(str, str, number_of_calls=3, verbose=True)
def test_func(a, b):
    assert(isinstance(a + b, str))

こんな感じにコードを書くと test_func の引数にランダムな str 型の文字列を渡してくれます。verbose オプションを True にすると、ランダムに生成された入力値が標準エラー出力に表示されます。

(advent)$ nosetests tests/test_with_paycheck_sample.py 
0: ('64+p57P8:G]NI.B5K', 'b#-O9SS#0#Ohq')
1: ('\\l<?[f$:}ld|1|Y<rd;XEi/^{)`', 'F*#(W,v6h2')
2: ('-9PBxyd(0y6j~/', 'CJMZPEIRn^>~#2')
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK

応用としては、irangefrange でその型の範囲指定を行ったり、choiceof で任意のリストから値を選択できます。

from paycheck import choiceof, irange, with_checker

@with_checker(irange(1, 10), number_of_calls=3, verbose=True)
def test_func2(i):
    assert(i <= 10)
# 実行結果
0: (9,)
1: (2,)
2: (3,)

@with_checker(choiceof([3, 5]), number_of_calls=3, verbose=True)
def test_func3(i):
    assert(i == 3 or i == 5)
# 実行結果
0: (3,)
1: (5,)
2: (5,)

その他にも positive_floatnon_negative_float といったものもあるようです。期待値の実行結果のデータ駆動テストにも便利そうです。

ランダムデータ駆動テストを自動化する

さらにモジュールを自動的に探してきて、そのモジュールで提供されている関数をテストしてくれると便利だったりしないかな?シグネチャさえ分かればできるよ!ようやく関数アノテーションの出番です。

サンプル実装として以下のようなものを作ってみました。テストディレクトリの親ディレクトリから "*.py" ファイルを探してきて、そのモジュールの __all__ で提供されている関数のシグネチャからテストを実行します。

  1 # -*- coding: utf-8 -*-                                                           
  2                                                                                   
  3 import glob                                                                       
  4 import imp                                                                        
  5 import inspect                                                                    
  6 import sys                                                                        
  7 from os.path import (abspath, dirname)                                            
  8                                                                                   
  9 from nose.tools import *                                                          
 10 from paycheck import with_checker                                                 
 11                                                                                   
 12 CHECKER_PARAMETER = {                                                             
 13     "number_of_calls": 3,                                                         
 14     "verbose": True,                                                              
 15 }                                                                                 
 16                                                                                   
 17 def debug(msg: str) -> None:                                                      
 18     sys.stderr.write("{}\n".format(msg))                                          
 19                                                                                   
 20 def get_modules(target_dir: str):                                                 
 21     for pyfile in glob.glob("{}/*.py".format(target_dir)):                        
 22         mod_name = pyfile.split("/")[-1].replace(".py", "")                       
 23         mod = imp.load_module(mod_name, *imp.find_module(mod_name))               
 24         yield mod                                                                 
 25                                                                                   
 26 def get_functions_with_ann(modules):                                              
 27     funcs = (getattr(mod, name) for mod in modules for name in mod.__all__)       
 28     for func in funcs:                                                            
 29         if hasattr(func, '__annotations__'):                                      
 30             yield func                                                            
 31                                                                                   
 32 def test_random_with_paycheck() -> None:                                           
 33     def tester(*args, **kwargs):                                                 
 34         result = func(*args, **kwargs)                                            
 35         ok_(isinstance(result, ret_type))                                         
 36                                                                                   
 37     base_dir = dirname(dirname(abspath(__file__)))                                
 38     for func in get_functions_with_ann(get_modules(base_dir)):                    
 39         debug("target function: {}".format(func.__name__))                        
 40         spec = inspect.getfullargspec(func)                                       
 41         args = spec.args                                                          
 42         if spec.varargs:                                                          
 43             args.append(spec.varargs)                                             
 44         if spec.varkw:                                                            
 45             args.append(spec.varkw)                                               
 46         ret_type = spec.annotations.get("return")                                
 47         types = [spec.annotations[arg] for arg in args]                          
 48         with_checker(*types, **CHECKER_PARAMETER)(tester)()

ディレクトリ構成は以下です。実行してみましょう。

(advent)$ tree .
.
├── others.py
└── tests
    ├── test_with_annotation.py

(advent)$ nosetests tests/test_with_annotation.py 
target function: foo
0: ("O3FND..fOSWv{KWeW:gl8'%k|L", 7607741906685156877, {'': 8791364593896247432, 'A': 7981434242837100514, '>KbMIsq#0kV;U?yxj2s~g,[%LQyrE': -190598769762457072, 'S7J:Um?<{ZtN:L@': -7691133294110638585, '0eV71S07lh~e>rb5P_6zE;5': 1101451838899520496, '*qU4~J*': 6338273523869299236, '|wMLD^\\ysKOw\\c6&S!Be3|hcz': 5053081943822034822, '{C<': 1734444387651285061, '$As^l,_C/av)}1R&HNz7sYd\\1d;.ex': -885374290895090654, '(qs$Ej]f': -8267062632669025484, ")'lOY533cm;jjHP5oI{LVCmRR[": -8668668576751442202, '=rACn7|@C': 478968652357174282, '5SNk0l\\4': -867212168323926037, 'fbB3#+xwU|': 8473818803708212295, 'd2.xgfT.V*<(y': -6515904853273909217, "KGDeofip:[_~M+K~>!'": -3589212816856071640, 'ZgM~': -602505023626250450, "|IJGj~';YFE-1wPPrEs%\\'-h": 4094644477640025745, "r!%n%'qohCttnXe8=7SDi^|t3": 427941587074733809, 'h%': -1809851284353770487}, -0.00023076482227383914)
1: ('/Qhp"NzOc.[|5CiJ', 5190099172656242926, {'': 6382145368304854615, 'x.0?lg@l': -4519850178140629357, 'u?B\\D2': -6081180918953419200, 'w+8inf3XnQ)wF+R8Mx;': -5279979493522305960, '=x0Y"{v': -1051360238739264279, 'LXZv<vV': 8490996434245906021, 'Sa$H*ed^,`$-EZ_%': -6937052124172693463, 'Q);n5': 60653761990170108, "\\`F{`aQ5w'": 1358220429869542064, 'j,,EVP=2WXua8)<oW-W[UngZ8p': 6151527201046578895, "HjY4H:oC'38?.aCO": -5710875614350879758, '0': -3166246628482595309, '#PIc2.': -615037772330393927, 'k%/': -8539311459395790283, 'tx<1': -7016431055285318858, 'Y$"L}EDq&A@msm': -7487772718733717165, 'Epz<eD=qzxRP': -5309516819741565453, 'B>Z95&ON:G>\\rgakkK/XQ^J#': 1080556375731418693, '!x': -8305477197940126401, 'b"m|\\`.$LQ)x`w+q%L6s_a,9\'': -5627647156759687669, 'c': -8050980599323942487, 'K4m\\^HW\\Ki>x_Tr': 1451298324637113436, '9;5uPcy43@7qr[': 7557790634460355432, 'jV': -6775386229302154514, '5Mu[,g': 7789805996343655479, 'ln1MH2qtO-(#8@l_W]P': 7934835116394274442, 'Di64M>{;(t\\/YJ4=Q*"X^>qowh': 3744629399181575512, '7].i': -1231696801069995861}, 0.021354448475725422)
2: ('@KGvLsf{CXEkwudbb$&a>t?`q&-tL', 2813673244267029793, {'m4#3<\\^8=tK': 2445679757000420077}, -0.03955141006906784)

target function: bar
0: ('X9|wG.n+xJ1Uzj?`q]+\\6>C"8_', 7102757083111770696, '%Qd|@')
1: ('fw"F', -508039826724708831, 'v0W6a}u[""@#?o;ziXOd-eFv=+"')
2: ('AUI6|BTLp%1K$u', -3393106434267748224, 'O.')

target function: baz
.
----------------------------------------------------------------------
Ran 1 test in 0.005s

OK

ちゃんとカレントディレクトリの others.py を探し出してテストを実行してくれました。

おや!?

foo と bar はテストが実行されているけど、baz のテストは実行されていないようです。

def foo(a: str, b: int, c: {str: int}, d: float) -> tuple:
  ...

def baz(a: str, b: int, *args: tuple, **kwargs: dict) -> list:
  ...

詳しく調べていませんが、paycheck に渡すタプルやディクショナリは (int, int) や {str: str} といった記述をしないと、入力となるテストデータを生成してくれないようです。

次にテスト関数をみてみます。

 33     def tester(*args, **kwargs):
 34         result = func(*args, **kwargs)
 35         ok_(isinstance(result, ret_type))

このテストで検証できるのは、様々な入力データに対して以下の内容です。

  • 関数を実行してエラーが発生しない
  • 期待した の返り値が取得できる

つまり、予期していない入力データによるエラーがないことをテストできます。

また with_checker へ渡す型情報の引数は、テストする関数に指定された引数の順番通りに指定する必要があります。

...
 40         spec = inspect.getfullargspec(func)                                       
 41         args = spec.args
...
 47         types = [spec.annotations[arg] for arg in args]                          
 48         with_checker(*types, **CHECKER_PARAMETER)(_tester)()

inspect.getfullargspec を使うと、アノテーションも含めた関数の全情報を取得できます。引数の順番が保持されたリストを取得したり、可変長引数 (*args や **kwargs) の有無も分かります。

In [12]: inspect.getfullargspec(baz)
Out[12]: FullArgSpec(args=['a', 'b'], varargs='args', varkw='kwargs', defaults=None, kwonlyargs=[], kwonlydefaults=None, 
         annotations={'a': <class 'str'>, 'b': <class 'int'>, 'args': <class 'tuple'>, 'return': <class 'list'>, 'kwargs': <class 'dict'>})

まとめ

関数アノテーションはドキュメントとしても有用ですし、静的解析のテクニックを応用したライブラリ等も今後出てくるでしょう。ふと気付いたことで、ジェネレーターを表すアノテーションが分かりませんでした。まだ決まってないのかな。

それと、初めて paycheck を使ってみましたが、関数アノテーションと組み合わせて相性の良さそうなところが見えました。1点だけ残念だったのは、with_checker 内でエラーが発生すると、例外を発生させて、そこでテストが終了してしまう点です。データ駆動テストとしては、ある入力データのテストがエラーになっても、その他の全入力データの結果もまとめて見れた方が便利です。ちょっと使ってみて、その点を改善できると良いなと思いました。あとドキュメントもほしいです。

次のアドベントカレンダーは @ です。以前から Python3 の発表をされていたので楽しみです。

2011-11-19

Sphinx 社内勉強会

| 12:41 | Sphinx 社内勉強会を含むブックマーク Sphinx 社内勉強会のブックマークコメント

Sphinx について何か話せというお題をもらって紹介してきました。

私自身、Sphinx をちょっと使った程度のレベルなのであんまり突っ込んだ内容ではありません。大体こんな感じの全体像で、こんな拡張ツールがあるよといった紹介をしました。プレゼン資料では分かりませんが、実際にデモで Sphinx のプロジェクトを作って、reST を書きながらビルドするとか、tex ファイルを作ってから pdf をビルドするとか、

$ python setup.py build_sphinx 

で簡単にパッケージングと連携することもできるとか、私が使ってきた中で知ってることを紹介しました。

興味をもってくれる人がいたら嬉しいなぁ。

おまけで ikazuchi のデモもしてみた (^ ^;;

2011-11-06

Jython プログラミング

| 16:45 | Jython プログラミングを含むブックマーク Jython プログラミングのブックマークコメント

ずっと積ん読になっていました。

購入したのはおよそ3年前。最近、私が Java を勉強し始めたという動機もあり、3日で読んでしまいました。3年も読めなかった本をいま読めてしまうというのは、いつか読むかもしれないから気になった書籍は取りあえず買っとけという気にさせてくれますね (^ ^;;

さて、本書は Jython の入門本であると同時にプログラミングの抽象概念を、著者の考察と簡潔な論理で説いてくれるので、とても分かりやすく参考になります。私は何となくそういうもんだと解釈していたことが、意図するものの背景を知って、新たな発見がいくつもありました。

Java の世界と Python の世界を行き来する

序盤は Eclipse の使い方や外部ライブラリのパスの通し方も丁寧に紹介されています。私は Eclipse を使い始めたばかりなので、そういった気遣いも嬉しいです。

Jython とは、Java で実装された Python の処理系です。JavaVM 上で Python の処理系が動くので、ほんの数行で Java の世界と Python の世界を行ったり来たりすることができます。本書を読む方は、どちらかの言語をある程度、知っていないと読んでいて混乱するかもしれません。本書内でも Java プログラマ向けに書かれたものだと説明されています。

Python から見たオブジェクト指向

5章まるまる1つの章がオブジェクト指向について著者の考察がサンプルコードと共に展開されます。冒頭の目的から引用します。

世の中の「オブジェクト指向」という言葉の使われ方を見ていると、おおざっぱに言って2つの「思想」があるように思えます。1つは「関連する変数や関数がコードのあちこちに散らばるのはよくない。ひとかたまりのモノとして扱えるべき。」という思想です。もう1つは「整数や関数などのいろいろなモノが、モノによって扱い方が違うのはよくない。統一的な方法で扱えるべき。」という思想です。この2つの思想はまったくの別物です。しかし、同じ「オブジェクト指向」という言葉で語られることが多く、まぎらわしいです。

私にとっては、5章が最もおもしろくて、とても勉強になりました。プログラミングは、ただ単に要件や仕様に沿ってコーディングするということじゃなくて、背景に「いろいろ」あって、効率良くプログラミングするために「いろいろ」あって、うまく言えないんだけど「いろいろ」考えとかないと後で大変なんだよー (> <)

。。。

といった「いろいろ」あるモノの1つが5章で分かりやすく説明されています。実際、私も経験して初めて実感したり、学べば学ぶほど新たな発見があったり、プログラミングって本当におもしろいものです。

オブジェクト指向にクラスは必須ではない

オブジェクト指向とは、クラスを作ってインスタンス化を指すのではないということを説明するために、Python のディクショナリと関数でクラスもどきを実装しています。

以前、私も同じようなことをハンズオン *1 で体験しましたが、当時の私には全く分かっていませんでした。クラスを使わずにクラスもどきを実装することで、オブジェクト指向の本質的な概念を理解するのに有用だと本章を読んで気付かされました。

  • インスタンス化はなぜ必要なのか?
  • 継承とは何なのか?

タプルはなぜ生まれたか

Python のタプルから私が連想するものは、

  • 不変オブジェクト
  • ディクショナリのキーになる
  • 関数の返り値を1つするためにパックされるもの

といったものでした。厳密には、ディクショナリのキーになるのは、特殊メソッド __hash__ を実装して、ハッシュ値が算出できるオブジェクトという定義の方が正しいようです。

ディクショナリの検索は、ハッシュ値からキーに対する値を取得できますが、人間はハッシュ値ではなく、キーの中身で認識します。キーの中身が同じでハッシュ値が異なるものや、キーの中身が違うのにハッシュ値が同じものがあっては困ります。だから、リストではなく、タプルが必要なんだと、実際のコードサンプルと一緒に紹介されています。

継承は慎重に。多重継承はもっと慎重に。

「多重継承を使ったほうがいいコード」とは限りません。むしろ「使わないほうがいいのに多重継承を使ったコードはとても悪いコード」です。Java の作者が「いっそ多重継承は禁止にしてしまえ」と考えるほどです。きちんと理解せずに使うと大変なことになる、そんな危険性を秘めたテクニックだということを、まずは肝に銘じてください。

多重継承を考える際に、問題なのは菱形継承 (ダイヤモンド継承) であると著者は述べています。

Java はその複雑さを取り除くために、クラスの多重継承を禁止して、インターフェースのみ多重継承できるように解決しました。Python は C3 線形化アルゴリズム で MRO (メソッド解決順序) を解決します *2

とはいえ、Python においても (理由なく) 菱形継承を行わない方が良さそうです。そういった課題はあるものの、多重継承ができるおかげで Mixin をうまく使って MRO を簡単に切り替えるというテクニックも使えます *3

テスト駆動開発に Jython を

Python は、wikipedia:グルー言語 としても便利なので、他の何かを扱うのも得意です。

本書では、JyConsole という補完機能をもつ Jython の対話的コンソールを Java プログラム (テストプログラム) 内に組み込むことで、「テストの清書」をする前に「テストのラフスケッチ」をすることを奨めています。

テストのような、地道に何度も実行して確認したりする用途には、Java でやるよりも Python の方がお手軽で良さそうです。対話的コンソールで、テストを書く前に試しに動かしてみる、色んな引数を与えてみるといったことも簡単にできます。おそらくそのお手軽さに反対する人はいないでしょう。

他にも Python には doctest というドキュメントにちょっとしたテストを兼ね備えた仕組みもあります。

リファレンス

Jythonプログラミング

Jythonプログラミング

2011-10-17

Oracle 11g XE を Ubuntu 11.10 にインストールする

| 22:29 | Oracle 11g XE を Ubuntu 11.10 にインストールするを含むブックマーク Oracle 11g XE を Ubuntu 11.10 にインストールするのブックマークコメント

最近リリースされたばかりの Ubuntu 11.10 に Oracle 11g XE をインストールしようとしたら少しはまったので原因と対策を簡単にまとめておきます。

Emerge Technology: Oracle 11g XEをUbuntuにインストール の手順通りでほぼできますが、最後の configure スクリプトを実行したとき、データベース設定に失敗します。

$ sudo /etc/init.d/oracle-xe configure
...
Starting Oracle Net Listener...Done
Configuring database...
Database Configuration failed.  Look into /u01/app/oracle/product/11.2.0/xe/config/log for details

これからインストールする人にとって、分かりやすいように、ここでは最初からの手順を記載しておきます。

Oracle 11g XE のダウンロードと展開

Oracle Database Express Edition 11g Release 2のダウンロード からダウンロードします。ダウンロードするには、ユーザー登録が必要なようです。会社名等も登録の必須項目なので少し面倒です。

インストールに必要なパッケージ群をインストールします。

$ sudo aptitude install alien libaio1 unixodbc

ダウンロードした Oracle 11g XE は RPM パッケージなので deb パッケージに変換します。

$ ls oracle-xe-11.2.0-1.0.x86_64.rpm.zip 
oracle-xe-11.2.0-1.0.x86_64.rpm.zip
$ unzip oracle-xe-11.2.0-1.0.x86_64.rpm.zip 
Archive:  oracle-xe-11.2.0-1.0.x86_64.rpm.zip
   creating: Disk1/
   creating: Disk1/upgrade/
  inflating: Disk1/upgrade/gen_inst.sql  
   creating: Disk1/response/
  inflating: Disk1/response/xe.rsp   
  inflating: Disk1/oracle-xe-11.2.0-1.0.x86_64.rpm    
$ cd Disk1/
$ sudo alien --to-deb --scripts oracle-xe-11.2.0-1.0.x86_64.rpm 
oracle-xe_11.2.0-2_amd64.deb generated

おそらく RPM パッケージがベースになっているために chkconfig コマンドを作ってあげないといけないようです。この辺は面倒なのでそのまま従っておきましょう。

元記事はシェバングが /sbin/bash になっていますが /bin/bash の間違い?
$ sudo vi /sbin/chkconfig
#!/bin/bash
# Oracle 11gR2 XE installer chkconfig hack for Debian by Dude
file=/etc/init.d/oracle-xe
if [[ ! `tail -n1 $file | grep INIT` ]]; then
   echo >> $file
   echo '### BEGIN INIT INFO' >> $file
   echo '# Provides:             OracleXE' >> $file
   echo '# Required-Start:       $remote_fs $syslog' >> $file
   echo '# Required-Stop:        $remote_fs $syslog' >> $file
   echo '# Default-Start:        2 3 4 5' >> $file
   echo '# Default-Stop:         0 1 6' >> $file
   echo '# Short-Description:    Oracle 11g Express Edition' >> $file
   echo '### END INIT INFO' >> $file
fi
update-rc.d oracle-xe defaults 80 01
$ sudo chmod 755 /sbin/chkconfig 

awk のシンボリックリンクも作成しておきます
$ sudo ln -s /usr/bin/awk /bin/awk

これで準備は整いました。deb パッケージをインストールします。

$ sudo dpkg --install ./oracle-xe_11.2.0-2_amd64.deb 

インストールできましたね。最後の configure スクリプトです。

$ sudo /etc/init.d/oracle-xe configure
...
Starting Oracle Net Listener...Done
Configuring database...
Database Configuration failed.  Look into /u01/app/oracle/product/11.2.0/xe/config/log for details

何か失敗してしまいました (> <)

configure スクリプトの原因調査

Cannot Install Oracle 11gR2 Express Edition on Ubuntu Linux 11.04 (64-bit) Howto によると、以下のログを調べろと言ってるので調べてみます。

$ sudo cat /u01/app/oracle/diag/rdbms/xe/XE/trace/alert_XE.log 
Mon Oct 17 20:54:11 2011
Starting ORACLE instance (normal)
WARNING: You are trying to use the MEMORY_TARGET feature. This feature requires 
the /dev/shm file system to be mounted for at least 419430400 bytes.
/dev/shm is either not mounted or is mounted with available space less than this size.
Please fix this so that MEMORY_TARGET can work as expected. Current available is 0 and
used is 0 bytes. Ensure that the mount point is /dev/shm for this directory.
memory_target needs larger /dev/shm

memory_target とか /dev/shm が悪さをしてそうだと分かります。

ubuntu 11.10 ORA-00845: MEMORY_TARGET not supported on this system によると、Ubuntu 11.10 から /dev/shm が /run/shm へ移行されたことが原因のようだと分かります *1

$ ls -l /dev/shm
lrwxrwxrwx 1 root root 8 2011-10-17 20:44 /dev/shm -> /run/shm

/dev/shm は /run/shm のシンボリックリンクになっていますが、configure スクリプトはこの状態だとうまく動作しないようです。フォーラムのやり取りの中で memory_target のパラメーターを削除して、pga_aggregate_target と sga_target を追加しなさいとあります。

以下の2つのファイルに同じ設定をする必要があるようです。メモリのサイズは適当なので、環境に応じて適切な値を設定してください *2

$ sudo vi /u01/app/oracle/product/11.2.0/xe/config/scripts/init.ora
$ sudo vi /u01/app/oracle/product/11.2.0/xe/config/scripts/initXETemp.ora 

#memory_target=418381824
pga_aggregate_target=200540160
sga_target=601620480

先ほどの configure スクリプトの実行で Listener は起動しているので kill しておきます。

$ ps ax|grep oracle
15318 ?        Ssl    0:00 /u01/app/oracle/product/11.2.0/xe/bin/tnslsnr LISTENER -inherit
$ sudo kill 15318

再度、configure スクリプトを実行します。

$ sudo /etc/init.d/oracle-xe configure
...
Configuring database...Done
Starting Oracle Database 11g Express Edition instance...Done
Installation completed successfully.

今度は成功しましたね!sqlplus でも接続してみましょう o(^ ^)o

$ source /u01/app/oracle/product/11.2.0/xe/bin/oracle_env.sh
$ sqlplus system/oracle
SQL*Plus: Release 11.2.0.2.0 Production on 月 1017 21:57:17 2011
Copyright (c) 1982, 2011, Oracle.  All rights reserved.
Oracle Database 11g Express Edition Release 11.2.0.2.0 - 64bit Production
に接続されました。
SQL> 

2011/10/26 追記

Ubuntu 11.04 にインストールしたのを 11.10 にアップグレードしたときの解決方法のようです。

Emerge Technology: Ubuntu 11.04に入れたOracle XE 11gを11.10で動かす方法