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

元ネタ:

最近、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 で書くと、こんな感じです (たぶん) 。

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(MyIterable):
    ...

data = MyData();

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

JavaPython で一体何が違うのか。

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

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

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

パーフェクトJava (PERFECT SERIES) (PERFECT SERIES 2)

パーフェクトJava (PERFECT SERIES) (PERFECT SERIES 2)

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

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

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

今北産業

なぜか CTO が本家からあぶれてしまい、元祖を立ち上げて所謂、骨肉の争いに突入しました。
そして、ダークサイドに落ちたありえるたんの *3 、闇えるたん (@) も現れ、三竦みの陣容を取っています。
私は日々、3強の影に怯えながら、ひっそり開発を継続しています。

番長の暗躍

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

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

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

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

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

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

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

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

闇えるたんの胎動

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

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

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

CTO との軋轢

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

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

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

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

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

もうやりたい放題です。

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

組長の真実

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

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

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

まとめ

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

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

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

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 というドキュメントにちょっとしたテストを兼ね備えた仕組みもあります。