pyrtm の Python 3 対応

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

PythonRemember 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

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

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

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

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

番長とは

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

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

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

CTO とは

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

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

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

http://blog.pasonatech.co.jp/ootani/17831.html

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

http://blog.pasonatech.co.jp/ootani/199/14879.html

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

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

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

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

実は仲良し?疑惑

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

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

よく出る話題としては、

番長は怖い人だ。

と、

CTO が本当は怖い。

です。

私はどっちも怖いです。

アルバイトは見た!

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

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

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

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

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

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

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

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

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

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

。。。

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

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

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

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

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

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

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

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

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

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

QuickCheck: An Automatic Testing Tool for HaskellPython 実装である 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 の発表をされていたので楽しみです。

Sphinx 社内勉強会

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

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

$ python setup.py build_sphinx 

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

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

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

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

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

テスト駆動開発Jython

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

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

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

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

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で動かす方法

Mac OS X で プロキシサーバーを使う

という要望を聞いたので調べてみました。

まずはデバッグのためにローカルの開発環境にプロキシサーバーを構築します。

ちょっとしたテストに squid : Optimising Web Delivery をインストールするのも面倒なので名前から軽量な印象を受ける Tinyproxy - A light-weight HTTP/HTTPS proxy をインストールします。

$ # MacPorts からインストールします
$ sudo port install tinyproxy

正常にインストールしたら設定を行います。デフォルトのままでも HTTP/HTTPS 共に有効になります。

$ # デフォルトの設定ファイルをそのままコピーします
$ cd /opt/local/etc/
$ cp -p tinyproxy.conf.default tinyproxy.conf

$ # 設定内容は man で調べます
$ man tinyproxy.conf

$ # log/run ディレクトリを作成して、環境にあわせて適切なパーミッションを設定してください
$ sudo mkdir /opt/local/var/log/tinyproxy
$ sudo chmod 775 /opt/local/var/log/tinyproxy
$ sudo mkdir /opt/local/var/run/tinyproxy
$ sudo chmod 775 /opt/local/var/run/tinyproxy

設定が完了したらプロキシサーバーを起動してみましょう。

最初は "-d" オプションを付けてフォアグランドで実行すると良いです。設定不備等でエラーがあると、そのメッセージがコンソールに出力されるからです。

$ # -d オプションで何もメッセージが出力されなかったら正常起動
$ tinyproxy -d

ログを確認してみましょう。

root で実行していないのですが、root で実行しないようにと WARNING が出てますね、、、ここでは気にしないことにします。

$ cat /opt/local/var/log/tinyproxy/tinyproxy.log
INFO      Sep 25 20:08:49 [48275]: Initializing tinyproxy ...
INFO      Sep 25 20:08:49 [48275]: Reloading config file
INFO      Sep 25 20:08:49 [48275]: Setting "Via" header to 'tinyproxy'
INFO      Sep 25 20:08:49 [48275]: Adding Port [443] to the list allowed by CONNECT
INFO      Sep 25 20:08:49 [48275]: Adding Port [563] to the list allowed by CONNECT
WARNING   Sep 25 20:08:49 [48275]: Not running as root, so not changing UID/GID.
...

動作を確認してみましょう。デフォルトでは tinyproxy はポート番号 8888 を使います。

$ python
Python 2.7.2 (default, Aug 22 2011, 13:53:27) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.environ["http_proxy"] = "localhost:8888"
>>> import urllib2
>>> r = urllib2.urlopen("http://www.python.org")
>>> r.headers.headers
['Via: 1.1 tinyproxy (tinyproxy/1.8.3)\r\n', ...]

以下のようなログが出力されます。

$ tail -f /opt/local/var/log/tinyproxy/tinyproxy.log
CONNECT   Sep 25 20:24:40 [48403]: Connect (file descriptor 9): localhost [127.0.0.1]
CONNECT   Sep 25 20:24:40 [48403]: Request (file descriptor 9): GET http://www.python.org HTTP/1.1
INFO      Sep 25 20:24:40 [48403]: No upstream proxy for www.python.org
CONNECT   Sep 25 20:24:40 [48403]: Established connection to host "www.python.org" using file descriptor 10.
INFO      Sep 25 20:24:41 [48403]: Closed connection between local client (fd:9) and remote client (fd:10)

ikazuchi のプロキシサーバー対応

開発のためのデバッグ環境は構築できました。

さて、本題の ikazuchi のプロキシサーバー対応です。結論から言って、先の Python の対話シェルで設定したように、環境変数の HTTP_PROXY と HTTPS_PROXY に設定しておくと、urllib2 モジュール内部でプロキシ設定をデフォルトで行ってくれます。そのため、従来のバージョンでも環境変数に設定することでプロキシサーバーに対応しています。

また、以下のように明示的に ProxyHandler の設定もできます *1

>>> import urllib2
>>> proxy = urllib2.ProxyHandler({"http": "localhost:8888"})
>>> opener = urllib2.build_opener(proxy)
>>> urllib2.install_opener(opener)
>>> r = urllib2.urlopen("http://www.python.org")
>>> r.headers.headers
['Via: 1.1 tinyproxy (tinyproxy/1.8.3)\r\n', ...]

とはいえ、せっかく調べたので ikazuchi.conf で明示的に http_proxy と https_proxy を設定できるように修正した ikazuchi 0.5.3 をリリースしました *2